import json import re from typing import Any from app.llm.client import LLMClient def strip_markdown_json(text: str) -> str: text = text.strip() fenced = re.search(r"```(?:json)?\s*(.*?)\s*```", text, re.DOTALL | re.IGNORECASE) if fenced: return fenced.group(1).strip() return text def slugify_branch(title: str, max_len: int = 40) -> str: text = title.lower() text = re.sub(r"[^a-z0-9а-яё]+", "-", text, flags=re.IGNORECASE) text = re.sub(r"-+", "-", text).strip("-") return text[:max_len] or "task" async def structure_work_item( raw_text: str, projects: list[dict[str, Any]], ) -> dict[str, Any]: project_lines = "\n".join( f"- {p['slug']}: {p['name']} (id={p['taiga_id']})" for p in projects ) system_prompt = f""" Ты технический ассистент. Преобразуй сырое описание фичи или бага в строгий JSON. Отвечай только JSON, без markdown. Доступные проекты Taiga: {project_lines} Схема: {{ "project_slug": "slug проекта из списка", "title": "короткое название", "description": "понятное описание", "issue_type": "feature|bug", "priority": "low|normal|high", "tags": ["tag1"], "acceptance_criteria": ["критерий 1"], "children": [ {{"title": "подзадача", "description": "описание", "type": "Task"}} ], "questions": ["уточняющий вопрос если данных мало"] }} Правила: - Пиши на русском. - project_slug выбери из списка; если неясно — первый подходящий или спроси в questions. - acceptance_criteria проверяемые. - children — технические подзадачи. """.strip() llm = LLMClient() result = await llm.complete( [ {"role": "system", "content": system_prompt}, {"role": "user", "content": raw_text}, ] ) content = strip_markdown_json(result.get("content") or "") return json.loads(content) def format_story_description(task: dict[str, Any], raw_text: str) -> str: lines = [task.get("description") or raw_text] acceptance = task.get("acceptance_criteria") or [] if acceptance: lines.append("") lines.append("## Acceptance criteria") for item in acceptance: lines.append(f"- {item}") questions = task.get("questions") or [] if questions: lines.append("") lines.append("## Вопросы") for item in questions: lines.append(f"- {item}") lines.append("") lines.append("## Исходное описание") lines.append(raw_text) return "\n".join(lines).strip() def format_gitea_body( task: dict[str, Any], raw_text: str, taiga_ref: int, taiga_url: str, branch: str, ) -> str: body = format_story_description(task, raw_text) body += f"\n\n---\n**Taiga:** #{taiga_ref} — {taiga_url}\n" body += f"**Ветка:** `{branch}`\n" body += "\nЗакрытие: `Closes gitea #N, taiga #REF` в коммите" return body