PMFlow/backend/app/services/sstc/charter_prefill_utils.py

233 lines
7.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)}