pipeline-service/pipeline_service/retrospective_capability.py

182 lines
8.2 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.

"""复盘能力 — 项目执行完后的问题收集 + 技能提议(通用、产线无关)。
职责(方案 P2/P3 的代码层):
- project_retrospective_data:四类问题源统一收集(冒泡问题/退回重做/编排缺口/Bug),
返回结构化素材给 PM 复盘任务,PM 不重复实现查询(「重复逻辑写 py」原则)。
- propose_skill:PM 复盘产出的技能提议落 skill_proposals(source=agent, status=pending),
进技能管理建议列表待人工审核,不直接改技能库(门禁守源头)。
"""
import json
import logging
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
DBNAME = "pipeline"
logger = logging.getLogger("pipeline.retrospective_capability")
def _get_db():
db = DBPools()
if not db.databases:
from appPublic.jsonConfig import getConfig
config = getConfig()
if config and config.databases:
db.databases = config.databases
return db, DBNAME
def _rows_to_dicts(recs, limit=200):
out = []
for r in (recs or [])[:limit]:
# sqlor 行是 DictObject:dict(rec) 才能取全部列(vars() 只取内部属性 → 空字典,
# 2026-08-31 测试机冒烟实测:project 名空壳即此因)。照抄 feature_capability._rec_to_dict。
if isinstance(r, dict):
out.append(dict(r))
continue
try:
out.append(dict(r))
continue
except (TypeError, ValueError):
pass
if hasattr(r, "to_dict"):
try:
out.append(r.to_dict())
continue
except Exception:
pass
out.append({})
return out
async def project_retrospective_data(project_id, who=None, agent_id=None,
task_id=None, iteration_id=None):
"""收集本项目执行中的全部问题素材(四类源合并 + 统计)。返回结构化文本(JSON)。
四类问题源(全部已落库,本函数只查不写):
① 冒泡问题 pipeline_agent_questions:question + 解决记录(answer/answered_by)
② 退回重做 audit_log:qc_reject/reject 记录(detail=退回意见,who=被退角色)
③ 编排缺口 pipeline_pm_notices:gap_text
④ Bug sd_bugs:title/description/fix_description(按项目迭代关联)
"""
if not project_id:
return "FAIL: 缺少 project_id"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
# ① 冒泡问题(含解决记录)
bubbles = _rows_to_dicts(await sor.sqlExe(
"SELECT problem_type, question, from_role, current_handler_role, status, "
"answer, answered_by, created_at FROM pipeline_agent_questions "
"WHERE tenant_id=${pid}$ ORDER BY created_at ASC LIMIT 100",
{"pid": project_id}))
# ② 退回重做(审计:谁被退、退回意见)
reworks = _rows_to_dicts(await sor.sqlExe(
"SELECT action, who, detail, created_at FROM audit_log "
"WHERE tenant_id=${pid}$ AND entity='pipeline_tasks' "
"AND action IN ('qc_reject','reject') ORDER BY created_at ASC LIMIT 100",
{"pid": project_id}))
# ③ 编排缺口
gaps = _rows_to_dicts(await sor.sqlExe(
"SELECT gap_text, status FROM pipeline_pm_notices "
"WHERE tenant_id=${pid}$ ORDER BY created_at ASC LIMIT 50",
{"pid": project_id}))
# ④ Bug(项目 → 迭代 → Bug)
iters = _rows_to_dicts(await sor.sqlExe(
"SELECT id, iteration_name FROM sd_iterations WHERE project_id=${pid}$",
{"pid": project_id}))
bugs = []
if iters:
ids = ",".join("'" + str(i.get('id', '')).replace("'", "") + "'" for i in iters)
bugs = _rows_to_dicts(await sor.sqlExe(
"SELECT title, description, severity, status, fix_description, created_at "
"FROM sd_bugs WHERE iteration_id IN (" + ids + ") "
"ORDER BY created_at ASC LIMIT 100", {}))
# 统计:工期
precs = await sor.sqlExe(
"SELECT name, created_at, status FROM sd_projects WHERE id=${pid}$",
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
proj = _rows_to_dicts(precs)
proj = proj[0] if proj else {}
problems = []
for b in bubbles:
problems.append({
"type": "bubble", "problem": b.get('question', ''),
"solution": b.get('answer', '') or '(未记录解决方法)',
"problem_type": b.get('problem_type', ''),
"roles": "%s→%s" % (b.get('from_role', ''), b.get('current_handler_role', '')),
"time": str(b.get('created_at', ''))})
for r in reworks:
problems.append({
"type": "rework", "problem": r.get('detail', '') or '(无退回意见)',
"solution": '(重做后通过,对比新旧交付件可见改法)',
"role": r.get('who', ''), "via": r.get('action', ''),
"time": str(r.get('created_at', ''))})
for g in gaps:
problems.append({
"type": "gap", "problem": g.get('gap_text', ''),
"solution": '(PM 处置记录见任务链)', "time": ''})
for bug in bugs:
problems.append({
"type": "bug", "problem": "%s:%s" % (bug.get('title', ''), (bug.get('description', '') or '')[:300]),
"solution": bug.get('fix_description', '') or '(未记录修复说明)',
"severity": bug.get('severity', ''), "status": bug.get('status', ''),
"time": str(bug.get('created_at', ''))})
data = {
"project": proj.get('name', ''), "project_status": proj.get('status', ''),
"project_created_at": str(proj.get('created_at', '')),
"problems": problems,
"stats": {
"bubble_count": len(bubbles), "rework_count": len(reworks),
"gap_count": len(gaps), "bug_count": len(bugs),
"total": len(problems),
},
}
return json.dumps(data, ensure_ascii=False, default=str)
async def propose_skill(project_id, name, description="", content="",
who=None, agent_id=None, task_id=None, iteration_id=None):
"""提交技能提议(复盘产出)。落 skill_proposals,source=agent,status=pending。
提议只是草稿:进技能管理建议列表待人工审核(pending→testing→approved→published),
不直接改技能库。content 为 SKILL.md 草稿,头部应带目标定位注释
(<!-- target: roles/agent.xxx/yyy(修改) --> 或 <!-- target: 新建 -->)。
"""
name = (name or '').strip()
content = (content or '').strip()
if not name or not content:
return False, "需要技能名和内容(content 为 SKILL.md 草稿正文)"
if len(content) < 80:
return False, "提议内容过短(<80字):SKILL.md 草稿须含触发条件/问题现象/根因/处理方法"
db, dbname = _get_db()
async with db.sqlorContext(dbname) as sor:
# org/pipeline 从项目取(提议按机构隔离 + 产线归属)
precs = await sor.sqlExe(
"SELECT org_id, pipeline_id FROM sd_projects WHERE id=${pid}$",
{"pid": project_id})
await sor.sqlExe("COMMIT", {})
org_id = '0'
pipeline_id = ''
if precs:
org_id = getattr(precs[0], 'org_id', '') or '0'
pipeline_id = getattr(precs[0], 'pipeline_id', '') or ''
pid = getID()
await sor.C('skill_proposals', {
'id': pid,
'name': name[:100],
'description': (description or '')[:500],
'content': content,
'source': 'agent',
'status': 'pending',
'org_id': org_id,
'pipeline_id': pipeline_id,
'created_by': who or 'agent.pm',
})
await sor.sqlExe("COMMIT", {})
logger.info("propose_skill: %s project=%s by=%s", name, project_id, who)
return True, f"已提交技能提议 '{name}'(待审核,暂不生效)"