feat(chat): agent 产出进度/错误时发射事件(保留持久化兜底)
This commit is contained in:
@@ -6,17 +6,21 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
from genesis.chat.intent import parse_intent_fake, parse_intent_llm
|
from genesis.chat.intent import parse_intent_fake, parse_intent_llm
|
||||||
|
from genesis.server.hub import hub as _progress_hub
|
||||||
from genesis.server.service import GenesisService
|
from genesis.server.service import GenesisService
|
||||||
|
|
||||||
|
|
||||||
class ChatAgent:
|
class ChatAgent:
|
||||||
def __init__(self, service: GenesisService, fake: bool = False, engine=None) -> None:
|
def __init__(self, service: GenesisService, fake: bool = False, engine=None,
|
||||||
|
progress_sink: "Callable[[dict], None] | None" = None) -> None:
|
||||||
self.service = service
|
self.service = service
|
||||||
self.store = service.store
|
self.store = service.store
|
||||||
self.fake = fake
|
self.fake = fake
|
||||||
self.engine = engine or service.engine
|
self.engine = engine or service.engine
|
||||||
|
self.progress_sink = progress_sink
|
||||||
|
|
||||||
# ---------- 主入口 ----------
|
# ---------- 主入口 ----------
|
||||||
|
|
||||||
@@ -51,6 +55,21 @@ class ChatAgent:
|
|||||||
"""错误回复以 role='error' 持久化,便于前端区分样式(§4.6)。"""
|
"""错误回复以 role='error' 持久化,便于前端区分样式(§4.6)。"""
|
||||||
self.store.add_message(session_id, "error", reply, action=action)
|
self.store.add_message(session_id, "error", reply, action=action)
|
||||||
|
|
||||||
|
def _emit_progress(self, session_id, item):
|
||||||
|
event = {"type": "progress", "step": item.get("step", ""),
|
||||||
|
"status": item.get("status", ""), "detail": item.get("detail", "")}
|
||||||
|
if self.progress_sink is not None:
|
||||||
|
self.progress_sink(event)
|
||||||
|
else:
|
||||||
|
_progress_hub.emit(session_id, event)
|
||||||
|
|
||||||
|
def _emit_error(self, session_id, reply, action):
|
||||||
|
event = {"type": "error", "detail": reply, "action": action}
|
||||||
|
if self.progress_sink is not None:
|
||||||
|
self.progress_sink(event)
|
||||||
|
else:
|
||||||
|
_progress_hub.emit(session_id, event)
|
||||||
|
|
||||||
def _parse_intent(self, session_id, content, rec):
|
def _parse_intent(self, session_id, content, rec):
|
||||||
if self.fake:
|
if self.fake:
|
||||||
return parse_intent_fake(content)
|
return parse_intent_fake(content)
|
||||||
@@ -73,8 +92,10 @@ class ChatAgent:
|
|||||||
try:
|
try:
|
||||||
self.service.confirm_impact(session_id)
|
self.service.confirm_impact(session_id)
|
||||||
progress.append({"step": "impact", "status": "ok", "detail": "影响调查已确认"})
|
progress.append({"step": "impact", "status": "ok", "detail": "影响调查已确认"})
|
||||||
|
self._emit_progress(session_id, progress[-1])
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
reply = f"影响确认失败:{e}"
|
reply = f"影响确认失败:{e}"
|
||||||
|
self._emit_error(session_id, reply, "confirm")
|
||||||
self._store_error(session_id, reply, "confirm")
|
self._store_error(session_id, reply, "confirm")
|
||||||
self._persist_progress(session_id, progress)
|
self._persist_progress(session_id, progress)
|
||||||
return {"reply": reply, "progress": progress, "status": "awaiting_impact_confirm"}
|
return {"reply": reply, "progress": progress, "status": "awaiting_impact_confirm"}
|
||||||
@@ -126,6 +147,7 @@ class ChatAgent:
|
|||||||
rec = self._parse_and_confirm(session_id, progress)
|
rec = self._parse_and_confirm(session_id, progress)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
reply = f"解析失败:{e}"
|
reply = f"解析失败:{e}"
|
||||||
|
self._emit_error(session_id, reply, "generate")
|
||||||
self._store_error(session_id, reply, "generate")
|
self._store_error(session_id, reply, "generate")
|
||||||
return {"reply": reply, "progress": progress, "status": rec.status}
|
return {"reply": reply, "progress": progress, "status": rec.status}
|
||||||
if rec.status == "writing":
|
if rec.status == "writing":
|
||||||
@@ -145,11 +167,13 @@ class ChatAgent:
|
|||||||
summary = json.loads(rec.structured_summary) if rec.structured_summary else {}
|
summary = json.loads(rec.structured_summary) if rec.structured_summary else {}
|
||||||
progress.append({"step": "parse", "status": "ok",
|
progress.append({"step": "parse", "status": "ok",
|
||||||
"detail": f"解析完成:{summary.get('tables', 0)} 张表"})
|
"detail": f"解析完成:{summary.get('tables', 0)} 张表"})
|
||||||
|
self._emit_progress(session_id, progress[-1])
|
||||||
rec = self.service.confirm_parse(session_id)
|
rec = self.service.confirm_parse(session_id)
|
||||||
if rec.status == "impact_running":
|
if rec.status == "impact_running":
|
||||||
rec = self.service.run_impact(session_id)
|
rec = self.service.run_impact(session_id)
|
||||||
progress.append({"step": "impact", "status": "ok",
|
progress.append({"step": "impact", "status": "ok",
|
||||||
"detail": f"影响调查完成:{self._impact_brief(rec)}"})
|
"detail": f"影响调查完成:{self._impact_brief(rec)}"})
|
||||||
|
self._emit_progress(session_id, progress[-1])
|
||||||
return self.store.get_session(session_id) # awaiting_impact_confirm
|
return self.store.get_session(session_id) # awaiting_impact_confirm
|
||||||
return rec # writing
|
return rec # writing
|
||||||
|
|
||||||
@@ -162,9 +186,10 @@ class ChatAgent:
|
|||||||
try:
|
try:
|
||||||
rec = self._parse_and_confirm(session_id, progress)
|
rec = self._parse_and_confirm(session_id, progress)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
reply = f"解析失败:{e}"
|
reply = f"解析失败:{e}"
|
||||||
self._store_error(session_id, reply, "parse")
|
self._emit_error(session_id, reply, "parse")
|
||||||
return {"reply": reply, "progress": progress, "status": rec.status}
|
self._store_error(session_id, reply, "parse")
|
||||||
|
return {"reply": reply, "progress": progress, "status": rec.status}
|
||||||
if rec.status == "awaiting_impact_confirm":
|
if rec.status == "awaiting_impact_confirm":
|
||||||
reply = f"解析与影响调查已完成:{self._impact_brief(rec)}。是否确认?(回复「确认,继续」)"
|
reply = f"解析与影响调查已完成:{self._impact_brief(rec)}。是否确认?(回复「确认,继续」)"
|
||||||
self.store.add_message(session_id, "assistant", reply, action="impact")
|
self.store.add_message(session_id, "assistant", reply, action="impact")
|
||||||
@@ -185,6 +210,7 @@ class ChatAgent:
|
|||||||
rec = self._parse_and_confirm(session_id, progress)
|
rec = self._parse_and_confirm(session_id, progress)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
reply = f"解析失败:{e}"
|
reply = f"解析失败:{e}"
|
||||||
|
self._emit_error(session_id, reply, "impact")
|
||||||
self._store_error(session_id, reply, "impact")
|
self._store_error(session_id, reply, "impact")
|
||||||
return {"reply": reply, "progress": progress, "status": rec.status}
|
return {"reply": reply, "progress": progress, "status": rec.status}
|
||||||
# _parse_and_confirm 已自动跑影响 → 直接返回反问
|
# _parse_and_confirm 已自动跑影响 → 直接返回反问
|
||||||
@@ -201,6 +227,7 @@ class ChatAgent:
|
|||||||
rec = self.service.run_impact(session_id)
|
rec = self.service.run_impact(session_id)
|
||||||
progress.append({"step": "impact", "status": "ok",
|
progress.append({"step": "impact", "status": "ok",
|
||||||
"detail": f"影响调查完成:{self._impact_brief(rec)}"})
|
"detail": f"影响调查完成:{self._impact_brief(rec)}"})
|
||||||
|
self._emit_progress(session_id, progress[-1])
|
||||||
reply = f"影响调查完成:{self._impact_brief(rec)}。是否确认?(回复「确认,继续」)"
|
reply = f"影响调查完成:{self._impact_brief(rec)}。是否确认?(回复「确认,继续」)"
|
||||||
self.store.add_message(session_id, "assistant", reply, action="impact")
|
self.store.add_message(session_id, "assistant", reply, action="impact")
|
||||||
return {"reply": reply, "progress": progress, "status": "awaiting_impact_confirm"}
|
return {"reply": reply, "progress": progress, "status": "awaiting_impact_confirm"}
|
||||||
@@ -212,16 +239,20 @@ class ChatAgent:
|
|||||||
rec = self.service.run_generate(session_id, output_language=output_language)
|
rec = self.service.run_generate(session_id, output_language=output_language)
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
reply = f"生成失败:{e}"
|
reply = f"生成失败:{e}"
|
||||||
|
self._emit_error(session_id, reply, "generate")
|
||||||
self._store_error(session_id, reply, "generate")
|
self._store_error(session_id, reply, "generate")
|
||||||
return {"reply": reply, "progress": progress, "status": rec.status}
|
return {"reply": reply, "progress": progress, "status": rec.status}
|
||||||
progress.append({"step": "generate", "status": "ok", "detail": "概要设计书已生成"})
|
progress.append({"step": "generate", "status": "ok", "detail": "概要设计书已生成"})
|
||||||
|
self._emit_progress(session_id, progress[-1])
|
||||||
try:
|
try:
|
||||||
rec = self.service.run_qa(session_id)
|
rec = self.service.run_qa(session_id)
|
||||||
progress.append({"step": "qa", "status": "ok",
|
progress.append({"step": "qa", "status": "ok",
|
||||||
"detail": f"QA: 通过={rec.qa_summary}"})
|
"detail": f"QA: 通过={rec.qa_summary}"})
|
||||||
|
self._emit_progress(session_id, progress[-1])
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
rec = self.store.get_session(session_id)
|
rec = self.store.get_session(session_id)
|
||||||
progress.append({"step": "qa", "status": "warn", "detail": f"QA 未执行:{e}"})
|
progress.append({"step": "qa", "status": "warn", "detail": f"QA 未执行:{e}"})
|
||||||
|
self._emit_progress(session_id, progress[-1])
|
||||||
reply = "概要设计书生成完成,QA 已执行。点击下方「下载 docx」获取结果。"
|
reply = "概要设计书生成完成,QA 已执行。点击下方「下载 docx」获取结果。"
|
||||||
self.store.add_message(session_id, "assistant", reply, action="generate")
|
self.store.add_message(session_id, "assistant", reply, action="generate")
|
||||||
return {"reply": reply, "progress": progress, "status": rec.status}
|
return {"reply": reply, "progress": progress, "status": rec.status}
|
||||||
@@ -238,12 +269,14 @@ class ChatAgent:
|
|||||||
try:
|
try:
|
||||||
rec = self.service.run_qa(session_id)
|
rec = self.service.run_qa(session_id)
|
||||||
progress.append({"step": "qa", "status": "ok", "detail": "QA 完成"})
|
progress.append({"step": "qa", "status": "ok", "detail": "QA 完成"})
|
||||||
|
self._emit_progress(session_id, progress[-1])
|
||||||
reply = f"QA 完成:{rec.qa_summary}"
|
reply = f"QA 完成:{rec.qa_summary}"
|
||||||
self.store.add_message(session_id, "assistant", reply, action="qa")
|
self.store.add_message(session_id, "assistant", reply, action="qa")
|
||||||
return {"reply": reply, "progress": progress, "status": rec.status}
|
return {"reply": reply, "progress": progress, "status": rec.status}
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
rec = self.store.get_session(session_id)
|
rec = self.store.get_session(session_id)
|
||||||
reply = f"QA 未执行:{e}"
|
reply = f"QA 未执行:{e}"
|
||||||
|
self._emit_error(session_id, reply, "qa")
|
||||||
self._store_error(session_id, reply, "qa")
|
self._store_error(session_id, reply, "qa")
|
||||||
return {"reply": reply, "progress": progress, "status": rec.status}
|
return {"reply": reply, "progress": progress, "status": rec.status}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import tempfile
|
||||||
|
|
||||||
|
from genesis.chat.agent import ChatAgent
|
||||||
|
from genesis.server.service import GenesisService
|
||||||
|
from genesis.server.store import ProjectsStore, SessionStore
|
||||||
|
|
||||||
|
_tmp = tempfile.mkdtemp(prefix="genesis_ws_")
|
||||||
|
|
||||||
|
|
||||||
|
def _make_agent(sink):
|
||||||
|
store = SessionStore(db_path=f"{_tmp}/sessions.db")
|
||||||
|
projects = ProjectsStore(db_path=f"{_tmp}/projects.db")
|
||||||
|
svc = GenesisService(store=store, data_root="data", engine="fake", projects=projects)
|
||||||
|
return ChatAgent(service=svc, fake=True, engine="fake", progress_sink=sink)
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_emits_progress_events():
|
||||||
|
events = []
|
||||||
|
agent = _make_agent(events.append)
|
||||||
|
sid = agent.service.create_session("u1").session_id
|
||||||
|
agent._emit_progress(sid, {"step": "parse", "status": "ok", "detail": "解析完成"})
|
||||||
|
assert events and events[0]["type"] == "progress"
|
||||||
|
assert events[0]["step"] == "parse"
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_emits_error_event():
|
||||||
|
events = []
|
||||||
|
agent = _make_agent(events.append)
|
||||||
|
sid = agent.service.create_session("u1").session_id
|
||||||
|
agent._emit_error(sid, "解析失败:boom", "generate")
|
||||||
|
errs = [e for e in events if e["type"] == "error"]
|
||||||
|
assert errs and "boom" in errs[0]["detail"]
|
||||||
Reference in New Issue
Block a user