266 lines
9.2 KiB
Python
266 lines
9.2 KiB
Python
from __future__ import annotations
|
||
|
||
from .shared import *
|
||
|
||
from .charter_seed_utils import _localize_seed_to_zh, _needs_seed_localize_zh, _parse_json_object_text, _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")
|
||
if not isinstance(modules, dict):
|
||
return False
|
||
for module_key, required_fields in _CHARTER_REQUIRED_FIELDS.items():
|
||
module_payload = modules.get(module_key)
|
||
if not isinstance(module_payload, dict):
|
||
return False
|
||
for field_key in required_fields:
|
||
if not str(module_payload.get(field_key) or "").strip():
|
||
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)
|
||
|
||
prompt = (
|
||
"你是 AI 项目经理,请基于输入信号生成可落地的宪章草稿。"
|
||
"所有自然语言字段必须使用简体中文,禁止英文句子;模型名称或技术名词可保留英文。"
|
||
"必须先结合当前项目上下文与活跃项目列表判断是否重复建设或应当合并。"
|
||
"只输出 JSON,不要 markdown。\n"
|
||
"字段:\n"
|
||
"{\n"
|
||
' "summary": "一句话问题摘要",\n'
|
||
' "background": "背景与动机",\n'
|
||
' "objectives": ["目标1","目标2"],\n'
|
||
' "scope": "范围说明",\n'
|
||
' "in_scope": ["范围内事项1","范围内事项2"],\n'
|
||
' "out_of_scope": ["范围外事项1","范围外事项2"],\n'
|
||
' "success_metrics": ["指标1","指标2"],\n'
|
||
' "risks": ["风险1","风险2"],\n'
|
||
' "dependencies": ["依赖1","依赖2"],\n'
|
||
' "primary_model": "模型名",\n'
|
||
' "fallback_model": "备用模型名"\n'
|
||
"}\n\n"
|
||
f"当前项目上下文:\n{project_context_text or '无'}\n\n"
|
||
f"现有活跃项目(JSON):\n{active_projects_json or '[]'}\n\n"
|
||
f"信号输入:\n{signal_context[:6000]}"
|
||
)
|
||
try:
|
||
raw = await llm_service.generate(
|
||
prompt,
|
||
function_name="sstc_charter_prefill",
|
||
db=db,
|
||
tenant_id=tenant_id,
|
||
project_id=project_id,
|
||
)
|
||
parsed = _parse_json_object_text(raw)
|
||
if parsed:
|
||
if _needs_seed_localize_zh(parsed):
|
||
return await _localize_seed_to_zh(
|
||
db=db, tenant_id=tenant_id, project_id=project_id, seed=parsed
|
||
)
|
||
return parsed
|
||
except Exception:
|
||
pass
|
||
|
||
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 {}
|
||
except Exception:
|
||
return {}
|