import time from typing import Any from sqlalchemy.orm import Session from app.config import get_settings from app.integrations.taiga import TaigaClient from app.projects.service import ProjectService MAX_PROJECTS_IN_CONTEXT = 20 MAX_OPEN_PER_PROJECT = 8 PROJECTS_CACHE_SEC = 120 _cache: dict[str, Any] = {"data": None, "expires_at": 0.0} def invalidate_projects_snapshot_cache() -> None: _cache["data"] = None _cache["expires_at"] = 0.0 def get_projects_snapshot(db: Session, *, force: bool = False) -> dict[str, Any]: now = time.time() if not force and _cache["data"] is not None and now < _cache["expires_at"]: return _cache["data"] snapshot = _fetch_projects_snapshot(db) _cache["data"] = snapshot _cache["expires_at"] = now + PROJECTS_CACHE_SEC return snapshot def _fetch_projects_snapshot(db: Session) -> dict[str, Any]: settings = get_settings() service = ProjectService(db) if not settings.taiga_configured: return {"configured": False, "projects": [], "open_items": [], "taiga_open": []} projects = service.list_projects() if not projects: try: projects = service.sync_taiga_projects() except Exception as exc: return { "configured": True, "projects": [], "open_items": [], "taiga_open": [], "error": str(exc), } open_items = service.list_work_items(limit=15, status="open") taiga_open: list[dict[str, Any]] = [] fetch_error: str | None = None try: client = TaigaClient() for proj in projects[:MAX_PROJECTS_IN_CONTEXT]: stories = client.list_open_userstories( proj["taiga_id"], limit=MAX_OPEN_PER_PROJECT ) tasks = client.list_open_tasks(proj["taiga_id"], limit=MAX_OPEN_PER_PROJECT) taiga_open.append( { "slug": proj["slug"], "name": proj["name"], "stories": [ { "ref": s.get("ref"), "subject": s.get("subject", "")[:120], } for s in stories ], "tasks": [ { "ref": t.get("ref"), "subject": t.get("subject", "")[:120], } for t in tasks ], } ) except Exception as exc: fetch_error = str(exc) return { "configured": True, "projects": projects, "open_items": open_items, "taiga_open": taiga_open, "error": fetch_error, } def format_projects_context(snapshot: dict[str, Any]) -> str: if not snapshot.get("configured"): return "[Taiga/Gitea]\nНе настроено (нет TAIGA_USERNAME/PASSWORD в .env)." lines = ["[Проекты и задачи — снимок на начало ответа]"] if snapshot.get("error"): lines.append(f"⚠ Ошибка загрузки задач из Taiga: {snapshot['error']}") projects = snapshot.get("projects") or [] if not projects: lines.append("Проекты Taiga: кэш пуст. Вызови sync_taiga_projects.") else: lines.append(f"Проекты Taiga ({len(projects)}):") for p in projects[:MAX_PROJECTS_IN_CONTEXT]: gitea = ( f"{p.get('gitea_owner')}/{p.get('gitea_repo')}" if p.get("gitea_configured") else "Gitea не привязан" ) lines.append(f"- `{p.get('slug')}`: {p.get('name')} · {gitea}") taiga_open = snapshot.get("taiga_open") or [] if taiga_open: lines.append("") lines.append("Открытые задачи в Taiga (live):") for block in taiga_open: stories = block.get("stories") or [] tasks = block.get("tasks") or [] if not stories and not tasks: 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("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( "Правила: " "«какие задачи» → list_taiga_tasks (Taiga API), НЕ list_work_items. " "list_work_items — только созданные через ассистента. " "Не пиши «ожидаю систему» — сразу вызывай tool или отвечай из снимка выше. " "create_work_item — для новых фич/багов из вольного текста." ) return "\n".join(lines)