Files
xianjing-ai/engine/app/scheme_engine.py
T
Cursor Agentandhaijd 9c6afcb61a feat: 可配置方案引擎通用原型(Mock 检索)
需求未定稿时可先跑通追问/判定/出方案;槽位与判定树外置 YAML,后续可接 Ragflow。

Co-authored-by: haijd <haijd@163.com>
2026-09-18 09:12:18 +00:00

114 lines
4.0 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 typing import Any
from .config_loader import business_config
from .decision import evaluate_tree
from .retrieval import retrieve_for_scheme
from .session_store import (
Session,
looks_like_handoff,
naive_fill_slots,
next_slot_prompt,
store,
)
def create_session() -> Session:
return store.create()
def handle_message(session_id: str, message: str) -> dict[str, Any]:
session = store.get(session_id)
if not session:
raise KeyError("session_not_found")
text = (message or "").strip()
session.messages.append({"role": "user", "content": text})
if looks_like_handoff(text, business_config.handoff_phrases):
session.phase = "handoff"
reply = "已为您标记转人工。请留下称呼与联系方式,销售同事会跟进。(原型:仅记录意图,未接 Chatwoot)"
return _finish(session, reply, need_handoff=True)
# fill slots while collecting
if session.phase in ("collecting", "scheme_ready"):
session.slots = naive_fill_slots(text, business_config.slots, session.slots)
nxt = next_slot_prompt(business_config.slots, session.slots)
if nxt and session.phase == "collecting":
# first turn with empty slots: greet + first question
if len(session.messages) == 1 and not any(session.slots.values()):
# put first user utterance into first slot if they already answered something
session.slots = naive_fill_slots(text, business_config.slots, session.slots)
nxt = next_slot_prompt(business_config.slots, session.slots)
if nxt:
reply = nxt["prompt"]
return _finish(session, reply, asking_slot=nxt["id"])
# all required (+ optional asked or filled) → scheme
decision = evaluate_tree(business_config.rules, session.slots)
confidence = float(decision.get("confidence") or 0)
need_handoff = bool(decision.get("handoff")) or confidence < business_config.handoff_confidence_below
bundle = list(decision.get("product_bundle") or [])
retrieval = retrieve_for_scheme(session.slots, bundle)
scheme = {
"tech_line": decision.get("tech_line"),
"product_bundle": bundle,
"summary": decision.get("summary"),
"confidence": confidence,
"rule_id": decision.get("rule_id"),
"citations": [
{"id": c.get("id"), "title": c.get("title"), "content": c.get("content")}
for c in retrieval.chunks
],
"slots": dict(session.slots),
}
session.last_scheme = scheme
session.phase = "handoff" if need_handoff else "scheme_ready"
lines = [
business_config.closing_prompt,
"",
f"技术路线:{scheme['tech_line']}",
f"推荐组合:{', '.join(bundle)}",
f"说明:{scheme['summary']}",
f"置信度:{confidence:.0%}",
]
if scheme["citations"]:
lines.append("")
lines.append("知识依据(原型检索):")
for c in scheme["citations"][:3]:
lines.append(f"- {c['title']}:{c['content']}")
if need_handoff:
lines.append("")
lines.append("系统建议转人工确认后再对外发送方案。回复「转人工」可进入人工流程。")
else:
lines.append("")
lines.append("可继续补充约束,或回复「转人工」交给销售。")
reply = "\n".join(lines)
return _finish(session, reply, scheme=scheme, need_handoff=need_handoff)
def _finish(
session: Session,
reply: str,
*,
asking_slot: str | None = None,
scheme: dict[str, Any] | None = None,
need_handoff: bool = False,
) -> dict[str, Any]:
session.messages.append({"role": "assistant", "content": reply})
store.touch(session)
return {
"session_id": session.id,
"reply": reply,
"phase": session.phase,
"slots": session.slots,
"asking_slot": asking_slot,
"scheme": scheme or session.last_scheme,
"need_handoff": need_handoff,
}