233 lines
7.8 KiB
Python
233 lines
7.8 KiB
Python
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)}
|