feat(todo-form): 冒泡待办支持form_schema动态表单——ask_question可声明上传/填写字段,人类在待办里直接完成
This commit is contained in:
parent
52550c9e69
commit
14cbf208b7
@ -551,7 +551,7 @@ AGENT_TOOLS = [
|
|||||||
{"name":"git_clone","description":"克隆git仓库到机构工作空间(应用→apps/、模块→modules/)","params":{"repo_url":"仓库URL","repo_name":"仓库目录名(可选,默认从URL推断,_app后缀→apps/)","branch":"分支(可选,默认main)"}},
|
{"name":"git_clone","description":"克隆git仓库到机构工作空间(应用→apps/、模块→modules/)","params":{"repo_url":"仓库URL","repo_name":"仓库目录名(可选,默认从URL推断,_app后缀→apps/)","branch":"分支(可选,默认main)"}},
|
||||||
{"name":"git_status","description":"查看git仓库状态","params":{"repo_dir":"仓库子目录(可选,默认 apps/modules 下第一个)"}},
|
{"name":"git_status","description":"查看git仓库状态","params":{"repo_dir":"仓库子目录(可选,默认 apps/modules 下第一个)"}},
|
||||||
{"name":"git_commit_push","description":"git add + commit + push(develop 用它提交模块仓库本地提交作为产出证据;无远程只 commit 不 push,非 git 目录自动 init)","params":{"message":"提交信息","repo_dir":"仓库子目录(可选)"}},
|
{"name":"git_commit_push","description":"git add + commit + push(develop 用它提交模块仓库本地提交作为产出证据;无远程只 commit 不 push,非 git 目录自动 init)","params":{"message":"提交信息","repo_dir":"仓库子目录(可选)"}},
|
||||||
{"name":"ask_question","description":"向用户提问(缺少信息时使用)","params":{"question":"问题"}},
|
{"name":"ask_question","description":"向用户提问(缺少信息时使用)。需要用户提供文件或结构化信息时用 form 声明动态表单,用户可在待办里直接上传/填写完成","params":{"question":"问题","form":"可选,JSON:{\"fields\":[{\"name\":\"字段名\",\"type\":\"file|text|textarea\",\"label\":\"提示\",\"target\":\"文件目标相对路径(如 env/test.json,目录以/结尾保留原文件名)\",\"required\":true}]}"}},
|
||||||
{"name":"deliver","description":"提交最终交付件(代码/文档文件已用write_file写好时调用)","params":{"deliverable_type":"交付件类型(如code_files/design_doc)","summary":"概述","result":"交付件正文","files":"JSON数组[{\"path\":\"modules/{模块}/src/x.py\",\"content\":\"代码内容\"}](可选)"}},
|
{"name":"deliver","description":"提交最终交付件(代码/文档文件已用write_file写好时调用)","params":{"deliverable_type":"交付件类型(如code_files/design_doc)","summary":"概述","result":"交付件正文","files":"JSON数组[{\"path\":\"modules/{模块}/src/x.py\",\"content\":\"代码内容\"}](可选)"}},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -978,13 +978,18 @@ async def _bubble_deploy_env_missing(sor, project_id, task_id, task_title, missi
|
|||||||
_own = await sor.sqlExe("SELECT created_by FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
|
_own = await sor.sqlExe("SELECT created_by FROM sd_projects WHERE id=${pid}$", {"pid": project_id})
|
||||||
await sor.sqlExe("COMMIT", {})
|
await sor.sqlExe("COMMIT", {})
|
||||||
owner_id = (getattr(_own[0], 'created_by', '') if _own else '') or 'user-01'
|
owner_id = (getattr(_own[0], 'created_by', '') if _own else '') or 'user-01'
|
||||||
|
# env_file 是绝对路径({pdir}/env/{env}.json),target 取项目内相对路径
|
||||||
|
_rel = 'env/'
|
||||||
|
_m = re.search(r'/env/([^/]+)$', env_file or '')
|
||||||
|
if _m:
|
||||||
|
_rel = 'env/' + _m.group(1)
|
||||||
question = (
|
question = (
|
||||||
f"部署任务「{task_title or task_id[:8]}」无法开始:真实部署环境信息尚未提供。"
|
f"部署任务「{task_title or task_id[:8]}」无法开始:真实部署环境信息尚未提供。"
|
||||||
"这属于外部输入(agent 无法自行产出),因此不回退需求/设计/开发阶段,只等这里补齐。\n\n"
|
"这属于外部输入(agent 无法自行产出),因此不回退需求/设计/开发阶段,只等这里补齐。\n\n"
|
||||||
f"配置文件:{env_file}\n\n"
|
f"配置文件:{env_file}\n\n"
|
||||||
"缺失字段:\n" + "\n".join(f" · {m}" for m in missing) +
|
"缺失字段:\n" + "\n".join(f" · {m}" for m in missing) +
|
||||||
"\n\n请补齐以上字段后提交回答(回答内容会随任务上下文交给执行 agent,"
|
"\n\n**请直接在下方上传该环境配置文件**(含完整部署信息的 JSON;"
|
||||||
"任务将自动恢复执行、重新校验环境信息)。"
|
"提交后任务自动恢复执行、重新校验环境信息)。"
|
||||||
)
|
)
|
||||||
await raise_problem(
|
await raise_problem(
|
||||||
"need_info", question, "system.orchestrator",
|
"need_info", question, "system.orchestrator",
|
||||||
@ -992,6 +997,11 @@ async def _bubble_deploy_env_missing(sor, project_id, task_id, task_title, missi
|
|||||||
first_handler_role="owner.superuser", first_handler_agentid=owner_id,
|
first_handler_role="owner.superuser", first_handler_agentid=owner_id,
|
||||||
context={"env_file": env_file, "missing": missing},
|
context={"env_file": env_file, "missing": missing},
|
||||||
suspend_task=True,
|
suspend_task=True,
|
||||||
|
form_schema={"fields": [{
|
||||||
|
"name": "env_file", "type": "file",
|
||||||
|
"label": f"环境配置文件({_rel})——需含以下字段:" + "、".join(missing),
|
||||||
|
"target": _rel, "required": True, "accept": ".json",
|
||||||
|
}]},
|
||||||
)
|
)
|
||||||
logger.warning(f"部署环境信息缺失冒泡(问题通道): task={task_id} missing={missing} owner={owner_id}")
|
logger.warning(f"部署环境信息缺失冒泡(问题通道): task={task_id} missing={missing} owner={owner_id}")
|
||||||
|
|
||||||
@ -1783,6 +1793,45 @@ async def _rollback_task_chain(sor, project_id, task_id, rollback_role, comment)
|
|||||||
|
|
||||||
# ── Agent 工具执行 ──
|
# ── Agent 工具执行 ──
|
||||||
|
|
||||||
|
def _parse_form_schema(v):
|
||||||
|
"""解析 ask_question 的 form 参数 → 规整的 form_schema(非法/空 → None)。
|
||||||
|
|
||||||
|
LLM 可能传 JSON 字符串或 dict;字段只保留白名单键,防注入。
|
||||||
|
返回 {"fields":[{name,type,label,target,required,accept},...]} 或 None。
|
||||||
|
"""
|
||||||
|
if not v:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
schema = json.loads(v) if isinstance(v, str) else v
|
||||||
|
fields = schema.get('fields') if isinstance(schema, dict) else None
|
||||||
|
if not isinstance(fields, list) or not fields:
|
||||||
|
return None
|
||||||
|
out = []
|
||||||
|
for f in fields[:10]: # 最多 10 个字段
|
||||||
|
if not isinstance(f, dict):
|
||||||
|
continue
|
||||||
|
name = str(f.get('name') or '').strip()
|
||||||
|
ftype = str(f.get('type') or 'text').strip().lower()
|
||||||
|
if not name or ftype not in ('file', 'text', 'textarea'):
|
||||||
|
continue
|
||||||
|
target = str(f.get('target') or '').strip()
|
||||||
|
if target:
|
||||||
|
# target 只允许相对路径字符,禁绝对路径与 .. 穿越
|
||||||
|
if target.startswith('/') or '..' in target.split('/'):
|
||||||
|
target = ''
|
||||||
|
out.append({
|
||||||
|
'name': name[:40],
|
||||||
|
'type': ftype,
|
||||||
|
'label': str(f.get('label') or name)[:120],
|
||||||
|
'target': target[:200],
|
||||||
|
'required': bool(f.get('required')),
|
||||||
|
'accept': str(f.get('accept') or '')[:60],
|
||||||
|
})
|
||||||
|
return {"fields": out} if out else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _parse_agent_action(raw):
|
def _parse_agent_action(raw):
|
||||||
raw = (raw or "").strip()
|
raw = (raw or "").strip()
|
||||||
if raw.startswith("```"):
|
if raw.startswith("```"):
|
||||||
@ -2239,6 +2288,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
|||||||
|
|
||||||
deliverable = None
|
deliverable = None
|
||||||
ask_question = None
|
ask_question = None
|
||||||
|
ask_form = None # ask_question 附带的动态表单声明(form_schema)
|
||||||
written_files = [] # 本任务执行期间 write_file 实际写入的文件(无 deliver 时的产出兜底)
|
written_files = [] # 本任务执行期间 write_file 实际写入的文件(无 deliver 时的产出兜底)
|
||||||
|
|
||||||
# ── Tool Loop(原生 function calling)──
|
# ── Tool Loop(原生 function calling)──
|
||||||
@ -2287,6 +2337,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
|||||||
break
|
break
|
||||||
if tool == "ask_question":
|
if tool == "ask_question":
|
||||||
ask_question = params.get("question", "")
|
ask_question = params.get("question", "")
|
||||||
|
ask_form = _parse_form_schema(params.get("form"))
|
||||||
break
|
break
|
||||||
|
|
||||||
if tool == "load_skill":
|
if tool == "load_skill":
|
||||||
@ -2309,6 +2360,7 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
|||||||
break
|
break
|
||||||
elif act.get('action') == 'ask':
|
elif act.get('action') == 'ask':
|
||||||
ask_question = act.get('question', '')
|
ask_question = act.get('question', '')
|
||||||
|
ask_form = _parse_form_schema(act.get('form'))
|
||||||
break
|
break
|
||||||
elif act.get('action') == 'tool_call':
|
elif act.get('action') == 'tool_call':
|
||||||
tool = act.get('tool', '')
|
tool = act.get('tool', '')
|
||||||
@ -2333,7 +2385,8 @@ async def role_agent_run(project_id, role, agent_id=None, model_name=None):
|
|||||||
from_agentid=agent_id,
|
from_agentid=agent_id,
|
||||||
tenant_id=project_id, task_id=task_id,
|
tenant_id=project_id, task_id=task_id,
|
||||||
first_handler_role="agent.main_agent",
|
first_handler_role="agent.main_agent",
|
||||||
context={"title": title})
|
context={"title": title},
|
||||||
|
form_schema=ask_form)
|
||||||
return {"status": "need_info", "task_id": task_id, "question_id": qid, "question": ask_question}
|
return {"status": "need_info", "task_id": task_id, "question_id": qid, "question": ask_question}
|
||||||
|
|
||||||
if not deliverable:
|
if not deliverable:
|
||||||
|
|||||||
@ -40,7 +40,7 @@ def _get_db():
|
|||||||
async def raise_problem(problem_type, question, from_role, from_agentid="",
|
async def raise_problem(problem_type, question, from_role, from_agentid="",
|
||||||
tenant_id=None, task_id=None, context=None,
|
tenant_id=None, task_id=None, context=None,
|
||||||
first_handler_role=None, first_handler_agentid="",
|
first_handler_role=None, first_handler_agentid="",
|
||||||
suspend_task=True) -> str:
|
suspend_task=True, form_schema=None) -> str:
|
||||||
"""提出问题。首处理方由调用方(读 skill 后)显式指定。
|
"""提出问题。首处理方由调用方(读 skill 后)显式指定。
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -51,6 +51,10 @@ async def raise_problem(problem_type, question, from_role, from_agentid="",
|
|||||||
first_handler_role: 首处理角色(不传默认 main_agent)
|
first_handler_role: 首处理角色(不传默认 main_agent)
|
||||||
first_handler_agentid: 首处理方具体 agent(空=该角色任意 agent)
|
first_handler_agentid: 首处理方具体 agent(空=该角色任意 agent)
|
||||||
suspend_task: 是否把任务置 waiting(角色提问挂起等回答)
|
suspend_task: 是否把任务置 waiting(角色提问挂起等回答)
|
||||||
|
form_schema: 动态表单声明(可选)。{"fields":[{"name","type":"file|text|textarea",
|
||||||
|
"label","target","required","accept"}]}——待办详情按它渲染输入界面,
|
||||||
|
人类在待办里直接上传文件/填写信息完成任务(文件落项目工作空间,
|
||||||
|
路径随回答回流给 agent)。存进 context.form_schema。
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
question_id
|
question_id
|
||||||
@ -65,6 +69,10 @@ async def raise_problem(problem_type, question, from_role, from_agentid="",
|
|||||||
hr = first_handler_role or "agent.main_agent"
|
hr = first_handler_role or "agent.main_agent"
|
||||||
ha = first_handler_agentid or ""
|
ha = first_handler_agentid or ""
|
||||||
qid = getID()
|
qid = getID()
|
||||||
|
# form_schema 并入 context 存储(待办详情按它渲染动态表单)
|
||||||
|
if form_schema:
|
||||||
|
context = dict(context or {})
|
||||||
|
context['form_schema'] = form_schema
|
||||||
ctx_json = json.dumps(context, ensure_ascii=False, default=str) if context else None
|
ctx_json = json.dumps(context, ensure_ascii=False, default=str) if context else None
|
||||||
await sor.C('pipeline_agent_questions', {
|
await sor.C('pipeline_agent_questions', {
|
||||||
'id': qid,
|
'id': qid,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user