Files
xianjing-ai/engine/app/scheme_engine.py
T

127 lines
4.4 KiB
Python
Raw Normal View History

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"),
2026-09-20 01:08:11 +00:00
"retrieval_backend": retrieval.backend,
"retrieval_warning": retrieval.warning,
"citations": [
2026-09-20 01:08:11 +00:00
{
"id": c.get("id"),
"title": c.get("title"),
"content": c.get("content"),
"score": c.get("score"),
}
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%}",
2026-09-20 01:08:11 +00:00
f"检索:{retrieval.backend}",
]
2026-09-20 01:08:11 +00:00
if retrieval.warning:
lines.append(f"注意:{retrieval.warning}")
if scheme["citations"]:
lines.append("")
2026-09-20 01:08:11 +00:00
lines.append("知识依据:")
for citation in scheme["citations"][:3]:
lines.append(f"- {citation['title']}:{citation['content']}")
else:
lines.append("")
lines.append("知识库未给出可引用片段,请勿把上述组合当成已确认报价。")
if need_handoff or retrieval.warning:
lines.append("")
2026-09-20 01:08:11 +00:00
lines.append("建议转人工确认后再发给客户。可在右侧留下称呼和电话。")
else:
lines.append("")
2026-09-20 01:08:11 +00:00
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,
}