Fixed Taiga integration
This commit is contained in:
@@ -8,9 +8,11 @@ TOOLS_INSTRUCTIONS = """
|
|||||||
- После вызова инструмента кратко объясни результат пользователю по-человечески.
|
- После вызова инструмента кратко объясни результат пользователю по-человечески.
|
||||||
- Помидоро: get_pomodoro_status, start_pomodoro, start_short_break, start_long_break,
|
- Помидоро: get_pomodoro_status, start_pomodoro, start_short_break, start_long_break,
|
||||||
stop_pomodoro, skip_pomodoro_phase, reset_pomodoro_cycle, get_pomodoro_history.
|
stop_pomodoro, skip_pomodoro_phase, reset_pomodoro_cycle, get_pomodoro_history.
|
||||||
- Задачи: sync_taiga_projects, list_taiga_projects, create_work_item, list_work_items.
|
- Taiga: sync_taiga_projects, list_taiga_projects, list_taiga_tasks, create_work_item, list_work_items.
|
||||||
- create_work_item — при «заведи баг/фичу», «добавь в таигу»; передай полный текст пользователя.
|
- «Какие задачи» / «покажи задачи проекта» → list_taiga_tasks (живые данные Taiga).
|
||||||
- Список проектов и открытых задач уже в контексте — не выдумывай, при необходимости уточни tool-вызовом.
|
- list_work_items — ТОЛЬКО задачи, созданные через create_work_item (локальная БД).
|
||||||
|
- create_work_item — при «заведи баг/фичу»; передай полный текст и project_slug.
|
||||||
|
- Снимок проектов/задач есть в контексте, но для актуализации вызывай tools. Никогда не пиши «ожидаю ответа от системы».
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
DEFAULT_CARD: dict[str, Any] = {
|
DEFAULT_CARD: dict[str, Any] = {
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ def format_pomodoro_notice(tool_name: str, raw_result: str) -> str | None:
|
|||||||
if tool_name == "list_work_items":
|
if tool_name == "list_work_items":
|
||||||
return _format_work_items_list_notice(data)
|
return _format_work_items_list_notice(data)
|
||||||
|
|
||||||
|
if tool_name == "list_taiga_tasks":
|
||||||
|
return _format_taiga_tasks_notice(data)
|
||||||
|
|
||||||
if tool_name == "sync_taiga_projects":
|
if tool_name == "sync_taiga_projects":
|
||||||
return f"📋 Синхронизировано проектов Taiga: **{len(data)}**"
|
return f"📋 Синхронизировано проектов Taiga: **{len(data)}**"
|
||||||
|
|
||||||
@@ -114,8 +117,8 @@ def _format_work_item_notice(data: dict[str, Any]) -> str | None:
|
|||||||
|
|
||||||
def _format_work_items_list_notice(data: Any) -> str | None:
|
def _format_work_items_list_notice(data: Any) -> str | None:
|
||||||
if not isinstance(data, list) or not data:
|
if not isinstance(data, list) or not data:
|
||||||
return "📋 Work items не найдены."
|
return "📋 Локальных work items (созданных ассистентом) нет."
|
||||||
lines = ["📋 **Work items:**"]
|
lines = ["📋 **Work items ассистента:**"]
|
||||||
for item in data[:15]:
|
for item in data[:15]:
|
||||||
lines.append(
|
lines.append(
|
||||||
f"- [{item.get('status')}] #{item.get('taiga_ref')} {item.get('title')} "
|
f"- [{item.get('status')}] #{item.get('taiga_ref')} {item.get('title')} "
|
||||||
@@ -124,6 +127,36 @@ def _format_work_items_list_notice(data: Any) -> str | None:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_taiga_tasks_notice(data: Any) -> str | None:
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return None
|
||||||
|
if data.get("error"):
|
||||||
|
return f"📋 {data['error']}"
|
||||||
|
|
||||||
|
blocks = data.get("projects") or []
|
||||||
|
total_stories = data.get("total_stories", 0)
|
||||||
|
total_tasks = data.get("total_tasks", 0)
|
||||||
|
|
||||||
|
if not blocks or (total_stories == 0 and total_tasks == 0):
|
||||||
|
slug = blocks[0].get("slug") if len(blocks) == 1 else None
|
||||||
|
if slug:
|
||||||
|
return f"📋 В `{slug}` нет открытых user stories и tasks в Taiga."
|
||||||
|
return "📋 Открытых задач в Taiga не найдено."
|
||||||
|
|
||||||
|
lines = [f"📋 **Открытые задачи Taiga** (stories: {total_stories}, tasks: {total_tasks}):"]
|
||||||
|
for block in blocks:
|
||||||
|
stories = block.get("stories") or []
|
||||||
|
tasks = block.get("tasks") or []
|
||||||
|
if not stories and not tasks:
|
||||||
|
continue
|
||||||
|
lines.append(f"**{block.get('name')}** (`{block.get('slug')}`):")
|
||||||
|
for s in stories:
|
||||||
|
lines.append(f"- story #{s.get('ref')} {s.get('subject')}")
|
||||||
|
for t in tasks:
|
||||||
|
lines.append(f"- task #{t.get('ref')} {t.get('subject')}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def _format_status_notice(data: dict[str, Any]) -> str:
|
def _format_status_notice(data: dict[str, Any]) -> str:
|
||||||
status = data.get("status", "idle")
|
status = data.get("status", "idle")
|
||||||
phase = data.get("phase", PHASE_WORK)
|
phase = data.get("phase", PHASE_WORK)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ from app.config import get_settings
|
|||||||
from app.integrations.taiga import TaigaClient
|
from app.integrations.taiga import TaigaClient
|
||||||
from app.projects.service import ProjectService
|
from app.projects.service import ProjectService
|
||||||
|
|
||||||
MAX_PROJECTS_IN_CONTEXT = 8
|
MAX_PROJECTS_IN_CONTEXT = 20
|
||||||
MAX_OPEN_PER_PROJECT = 5
|
MAX_OPEN_PER_PROJECT = 8
|
||||||
|
|
||||||
|
|
||||||
def get_projects_snapshot(db: Session) -> dict[str, Any]:
|
def get_projects_snapshot(db: Session) -> dict[str, Any]:
|
||||||
@@ -21,11 +21,18 @@ def get_projects_snapshot(db: Session) -> dict[str, Any]:
|
|||||||
if not projects:
|
if not projects:
|
||||||
try:
|
try:
|
||||||
projects = service.sync_taiga_projects()
|
projects = service.sync_taiga_projects()
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
projects = []
|
return {
|
||||||
|
"configured": True,
|
||||||
|
"projects": [],
|
||||||
|
"open_items": [],
|
||||||
|
"taiga_open": [],
|
||||||
|
"error": str(exc),
|
||||||
|
}
|
||||||
|
|
||||||
open_items = service.list_work_items(limit=15, status="open")
|
open_items = service.list_work_items(limit=15, status="open")
|
||||||
taiga_open: list[dict[str, Any]] = []
|
taiga_open: list[dict[str, Any]] = []
|
||||||
|
fetch_error: str | None = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
client = TaigaClient()
|
client = TaigaClient()
|
||||||
@@ -33,8 +40,7 @@ def get_projects_snapshot(db: Session) -> dict[str, Any]:
|
|||||||
stories = client.list_open_userstories(
|
stories = client.list_open_userstories(
|
||||||
proj["taiga_id"], limit=MAX_OPEN_PER_PROJECT
|
proj["taiga_id"], limit=MAX_OPEN_PER_PROJECT
|
||||||
)
|
)
|
||||||
if not stories:
|
tasks = client.list_open_tasks(proj["taiga_id"], limit=MAX_OPEN_PER_PROJECT)
|
||||||
continue
|
|
||||||
taiga_open.append(
|
taiga_open.append(
|
||||||
{
|
{
|
||||||
"slug": proj["slug"],
|
"slug": proj["slug"],
|
||||||
@@ -46,16 +52,24 @@ def get_projects_snapshot(db: Session) -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
for s in stories
|
for s in stories
|
||||||
],
|
],
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"ref": t.get("ref"),
|
||||||
|
"subject": t.get("subject", "")[:120],
|
||||||
|
}
|
||||||
|
for t in tasks
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
fetch_error = str(exc)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"configured": True,
|
"configured": True,
|
||||||
"projects": projects,
|
"projects": projects,
|
||||||
"open_items": open_items,
|
"open_items": open_items,
|
||||||
"taiga_open": taiga_open,
|
"taiga_open": taiga_open,
|
||||||
|
"error": fetch_error,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -63,13 +77,16 @@ def format_projects_context(snapshot: dict[str, Any]) -> str:
|
|||||||
if not snapshot.get("configured"):
|
if not snapshot.get("configured"):
|
||||||
return "[Taiga/Gitea]\nНе настроено (нет TAIGA_USERNAME/PASSWORD в .env)."
|
return "[Taiga/Gitea]\nНе настроено (нет TAIGA_USERNAME/PASSWORD в .env)."
|
||||||
|
|
||||||
lines = ["[Проекты и задачи — актуальный снимок для контекста]"]
|
lines = ["[Проекты и задачи — снимок на начало ответа]"]
|
||||||
|
|
||||||
|
if snapshot.get("error"):
|
||||||
|
lines.append(f"⚠ Ошибка загрузки задач из Taiga: {snapshot['error']}")
|
||||||
|
|
||||||
projects = snapshot.get("projects") or []
|
projects = snapshot.get("projects") or []
|
||||||
if not projects:
|
if not projects:
|
||||||
lines.append("Проекты Taiga: кэш пуст. Попроси sync_taiga_projects или проверь подключение.")
|
lines.append("Проекты Taiga: кэш пуст. Вызови sync_taiga_projects.")
|
||||||
else:
|
else:
|
||||||
lines.append("Проекты Taiga:")
|
lines.append(f"Проекты Taiga ({len(projects)}):")
|
||||||
for p in projects[:MAX_PROJECTS_IN_CONTEXT]:
|
for p in projects[:MAX_PROJECTS_IN_CONTEXT]:
|
||||||
gitea = (
|
gitea = (
|
||||||
f"{p.get('gitea_owner')}/{p.get('gitea_repo')}"
|
f"{p.get('gitea_owner')}/{p.get('gitea_repo')}"
|
||||||
@@ -78,32 +95,39 @@ def format_projects_context(snapshot: dict[str, Any]) -> str:
|
|||||||
)
|
)
|
||||||
lines.append(f"- `{p.get('slug')}`: {p.get('name')} · {gitea}")
|
lines.append(f"- `{p.get('slug')}`: {p.get('name')} · {gitea}")
|
||||||
|
|
||||||
open_items = snapshot.get("open_items") or []
|
|
||||||
if open_items:
|
|
||||||
lines.append("")
|
|
||||||
lines.append("Локальные work items (созданные ассистентом, открытые):")
|
|
||||||
for item in open_items[:10]:
|
|
||||||
gitea_part = f", gitea #{item.get('gitea_issue')}" if item.get("gitea_issue") else ""
|
|
||||||
lines.append(
|
|
||||||
f"- taiga #{item.get('taiga_ref')} {item.get('title')} "
|
|
||||||
f"({item.get('taiga_slug')}{gitea_part})"
|
|
||||||
)
|
|
||||||
|
|
||||||
taiga_open = snapshot.get("taiga_open") or []
|
taiga_open = snapshot.get("taiga_open") or []
|
||||||
if taiga_open:
|
if taiga_open:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Открытые user stories в Taiga:")
|
lines.append("Открытые задачи в Taiga (live):")
|
||||||
for block in taiga_open:
|
for block in taiga_open:
|
||||||
lines.append(f" [{block.get('slug')}]")
|
stories = block.get("stories") or []
|
||||||
for story in block.get("stories", []):
|
tasks = block.get("tasks") or []
|
||||||
lines.append(f" - #{story.get('ref')} {story.get('subject')}")
|
if not stories and not tasks:
|
||||||
elif projects:
|
lines.append(f" `{block.get('slug')}`: нет открытых")
|
||||||
|
continue
|
||||||
|
lines.append(f" `{block.get('slug')}`:")
|
||||||
|
for story in stories:
|
||||||
|
lines.append(f" story #{story.get('ref')} {story.get('subject')}")
|
||||||
|
for task in tasks:
|
||||||
|
lines.append(f" task #{task.get('ref')} {task.get('subject')}")
|
||||||
|
|
||||||
|
open_items = snapshot.get("open_items") or []
|
||||||
|
if open_items:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("Открытые user stories в Taiga: нет или не удалось загрузить.")
|
lines.append("Work items созданные ассистентом (локальная БД):")
|
||||||
|
for item in open_items[:10]:
|
||||||
|
gitea_part = f", gitea #{item.get('gitea_issue')}" if item.get("gitea_issue") else ""
|
||||||
|
lines.append(
|
||||||
|
f"- #{item.get('taiga_ref')} {item.get('title')} "
|
||||||
|
f"({item.get('taiga_slug')}{gitea_part})"
|
||||||
|
)
|
||||||
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append(
|
lines.append(
|
||||||
"Для создания фичи/бага из вольного текста используй create_work_item. "
|
"Правила: "
|
||||||
"Не выдумывай номера задач — опирайся на список выше."
|
"«какие задачи» → list_taiga_tasks (Taiga API), НЕ list_work_items. "
|
||||||
|
"list_work_items — только созданные через ассистента. "
|
||||||
|
"Не пиши «ожидаю систему» — сразу вызывай tool или отвечай из снимка выше. "
|
||||||
|
"create_work_item — для новых фич/багов из вольного текста."
|
||||||
)
|
)
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
@@ -366,6 +366,63 @@ class ProjectService:
|
|||||||
item.status = "closed"
|
item.status = "closed"
|
||||||
item.closed_at = datetime.now(timezone.utc)
|
item.closed_at = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
def list_taiga_open_tasks(
|
||||||
|
self,
|
||||||
|
project_slug: str | None = None,
|
||||||
|
limit: int = 20,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if not self.settings.taiga_configured:
|
||||||
|
raise ValueError("Taiga не настроена")
|
||||||
|
|
||||||
|
projects = self.list_projects()
|
||||||
|
if not projects:
|
||||||
|
projects = self.sync_taiga_projects()
|
||||||
|
|
||||||
|
if project_slug:
|
||||||
|
projects = [p for p in projects if p["slug"] == project_slug]
|
||||||
|
if not projects:
|
||||||
|
raise ValueError(
|
||||||
|
f"Проект '{project_slug}' не найден. Вызови sync_taiga_projects."
|
||||||
|
)
|
||||||
|
|
||||||
|
client = TaigaClient()
|
||||||
|
blocks: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for proj in projects:
|
||||||
|
stories = client.list_open_userstories(proj["taiga_id"], limit=limit)
|
||||||
|
tasks = client.list_open_tasks(proj["taiga_id"], limit=limit)
|
||||||
|
blocks.append(
|
||||||
|
{
|
||||||
|
"slug": proj["slug"],
|
||||||
|
"name": proj["name"],
|
||||||
|
"taiga_id": proj["taiga_id"],
|
||||||
|
"stories": [
|
||||||
|
{
|
||||||
|
"ref": s.get("ref"),
|
||||||
|
"subject": s.get("subject", ""),
|
||||||
|
"url": client.story_url(proj["taiga_id"], s.get("ref", 0)),
|
||||||
|
}
|
||||||
|
for s in stories
|
||||||
|
],
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"ref": t.get("ref"),
|
||||||
|
"subject": t.get("subject", ""),
|
||||||
|
"user_story": t.get("user_story"),
|
||||||
|
}
|
||||||
|
for t in tasks
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
total_stories = sum(len(b["stories"]) for b in blocks)
|
||||||
|
total_tasks = sum(len(b["tasks"]) for b in blocks)
|
||||||
|
return {
|
||||||
|
"projects": blocks,
|
||||||
|
"total_stories": total_stories,
|
||||||
|
"total_tasks": total_tasks,
|
||||||
|
}
|
||||||
|
|
||||||
def list_work_items(self, limit: int = 30, status: str | None = None) -> list[dict[str, Any]]:
|
def list_work_items(self, limit: int = 30, status: str | None = None) -> list[dict[str, Any]]:
|
||||||
stmt = select(WorkItem).order_by(WorkItem.created_at.desc()).limit(limit)
|
stmt = select(WorkItem).order_by(WorkItem.created_at.desc()).limit(limit)
|
||||||
if status:
|
if status:
|
||||||
|
|||||||
@@ -131,6 +131,27 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
|||||||
"parameters": {"type": "object", "properties": {}, "required": []},
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "list_taiga_tasks",
|
||||||
|
"description": (
|
||||||
|
"ОБЯЗАТЕЛЬНО при вопросах «какие задачи», «покажи задачи проекта», «что открыто в Taiga». "
|
||||||
|
"Живые user stories и tasks из Taiga API. НЕ путать с list_work_items."
|
||||||
|
),
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"project_slug": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Slug проекта, например home_assistant. Пусто = все проекты.",
|
||||||
|
},
|
||||||
|
"limit": {"type": "integer", "description": "Макс. на проект, по умолчанию 20"},
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
@@ -156,7 +177,10 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
|
|||||||
"type": "function",
|
"type": "function",
|
||||||
"function": {
|
"function": {
|
||||||
"name": "list_work_items",
|
"name": "list_work_items",
|
||||||
"description": "Список созданных work items (Taiga + Gitea связки).",
|
"description": (
|
||||||
|
"Только задачи, созданные ЭТИМ ассистентом через create_work_item (локальная БД). "
|
||||||
|
"НЕ использовать для общего вопроса «какие задачи в Taiga» — для того list_taiga_tasks."
|
||||||
|
),
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -201,6 +225,11 @@ async def execute_tool(db: Session, name: str, arguments: dict[str, Any]) -> str
|
|||||||
result = projects.sync_taiga_projects()
|
result = projects.sync_taiga_projects()
|
||||||
elif name == "list_taiga_projects":
|
elif name == "list_taiga_projects":
|
||||||
result = projects.list_projects()
|
result = projects.list_projects()
|
||||||
|
elif name == "list_taiga_tasks":
|
||||||
|
result = projects.list_taiga_open_tasks(
|
||||||
|
project_slug=arguments.get("project_slug"),
|
||||||
|
limit=arguments.get("limit", 20),
|
||||||
|
)
|
||||||
elif name == "create_work_item":
|
elif name == "create_work_item":
|
||||||
result = await projects.create_work_item(
|
result = await projects.create_work_item(
|
||||||
arguments.get("text", ""),
|
arguments.get("text", ""),
|
||||||
|
|||||||
Reference in New Issue
Block a user