from __future__ import annotations from .shared import * from .charter_seed_utils import _localize_seed_to_zh, _needs_seed_localize_zh, _to_bullet_text def _has_non_empty_module_data(content: dict[str, Any]) -> bool: modules = content.get("modules") if not isinstance(modules, dict): return False for module in modules.values(): if not isinstance(module, dict): continue for value in module.values(): if str(value or "").strip(): return True return False def _has_complete_module_data(content: dict[str, Any]) -> bool: modules = content.get("modules") field_status = content.get("field_status") if not isinstance(modules, dict): return False if not isinstance(field_status, dict): return False for module_key, required_fields in _CHARTER_REQUIRED_FIELDS.items(): module_payload = modules.get(module_key) status_payload = field_status.get(module_key) if not isinstance(module_payload, dict): return False if not isinstance(status_payload, dict): return False for field_key in required_fields: if not str(module_payload.get(field_key) or "").strip(): return False if str(status_payload.get(field_key) or "").strip() != "valid": return False return True def _merge_prefill_content( existing: dict[str, Any], incoming: dict[str, Any], *, overwrite_existing: bool ) -> dict[str, Any]: merged: dict[str, Any] = dict(existing) for key, value in incoming.items(): if overwrite_existing or key not in merged: merged[key] = value continue current = merged.get(key) if isinstance(current, dict) and isinstance(value, dict): merged[key] = _merge_prefill_content( current, value, overwrite_existing=overwrite_existing ) continue if isinstance(current, list): if not current and value: merged[key] = value continue if current is None or not str(current).strip(): merged[key] = value return merged def _default_ai_validations(module_name: str) -> str: return _to_bullet_text( [ f"校验「{module_name}」与信号源证据链一致", "校验字段可验证、可执行、可追踪", "发现冲突时自动标注并等待人工否决/确认", ] ) def _default_escalation_trigger() -> str: return _to_bullet_text( [ "置信度低于 0.75", "关键字段缺失或来源证据冲突", "超过模型能力边界或预算/时限约束", ] ) async def _load_charter_project_context( *, db: AsyncSession, tenant_id: int, project_id: int, ) -> dict[str, Any]: from ...models import Project row = ( await db.execute( select(Project).where( and_( Project.tenant_id == int(tenant_id), Project.id == int(project_id), ) ) ) ).scalar_one_or_none() if row is None: return {} return { "id": int(row.id), "name": str(getattr(row, "name", "") or "").strip(), "title": str(getattr(row, "title", "") or getattr(row, "name", "") or "").strip(), "description": str(getattr(row, "description", "") or "").strip(), "status": str(getattr(row, "status", "") or "").strip(), "owner_id": getattr(row, "owner_id", None), } async def _load_active_projects_context( *, db: AsyncSession, tenant_id: int, project_id: int, limit: int = 10, ) -> list[dict[str, Any]]: from ...models import Project rows = ( await db.execute( select( Project.id, Project.name, Project.title, Project.description, Project.status, Project.owner_id, ) .where( and_( Project.tenant_id == int(tenant_id), Project.status != "inactive", Project.id != int(project_id), ) ) .order_by(Project.id.desc()) .limit(max(1, min(20, int(limit)))) ) ).all() active_projects: list[dict[str, Any]] = [] for row in rows: active_projects.append( { "id": int(row.id), "name": str(row.name or "").strip(), "title": str(row.title or row.name or "").strip(), "description": str(row.description or "").strip(), "status": str(row.status or "").strip(), "owner_id": getattr(row, "owner_id", None), } ) return active_projects def _build_charter_project_context_text( project_context: dict[str, Any], active_projects: list[dict[str, Any]], ) -> str: blocks: list[str] = [] if isinstance(project_context, dict) and project_context: blocks.append( "\n".join( [ "[当前项目]", f"ID:{project_context.get('id')}", f"名称:{project_context.get('title') or project_context.get('name') or '未命名项目'}", f"描述:{str(project_context.get('description') or '暂无描述').strip()}", f"状态:{project_context.get('status') or 'unknown'}", f"负责人ID:{project_context.get('owner_id') or '未指定'}", ] ).strip() ) if active_projects: lines = ["[活跃项目(用于查重/复用)]"] for idx, project in enumerate(active_projects[:10], start=1): title = str(project.get("title") or project.get("name") or "未命名项目").strip() description = str(project.get("description") or "").strip() status = str(project.get("status") or "").strip() or "unknown" owner_id = project.get("owner_id") row_parts = [f"{idx}. {title}", f"状态:{status}"] if description: row_parts.append(f"描述:{description[:80]}") if owner_id is not None: row_parts.append(f"负责人ID:{owner_id}") lines.append(" | ".join(row_parts)) blocks.append("\n".join(lines)) return "\n\n".join(blocks).strip() async def _generate_charter_seed( *, db: AsyncSession, tenant_id: int, project_id: int, signal_context: str, ) -> dict[str, Any]: if not signal_context.strip(): return {} project_context = await _load_charter_project_context( db=db, tenant_id=tenant_id, project_id=project_id, ) active_projects = await _load_active_projects_context( db=db, tenant_id=tenant_id, project_id=project_id, ) project_context_text = _build_charter_project_context_text( project_context=project_context, active_projects=active_projects, ) active_projects_json = json.dumps(active_projects, ensure_ascii=False) try: ai_seed = await AIService().generate_charter_from_signal( signal_context, tenant_id=tenant_id, db=db, project_id=project_id, project_context=project_context_text, active_projects_json=active_projects_json, ) if isinstance(ai_seed, dict): if _needs_seed_localize_zh(ai_seed): return await _localize_seed_to_zh( db=db, tenant_id=tenant_id, project_id=project_id, seed=ai_seed ) return ai_seed return {"generation_error": "structured_charter_empty"} except Exception as exc: return {"generation_error": str(exc)}