Files
xianjing-ai/engine/app/scheme_engine.py
T
Cursor Agentandhaijd b7bd830da7 feat: 用 Ragflow 搭建鲜境方案客服
咨询台接 Ragflow 检索与示例知识库,支持追问、判定、引用和留资。

Co-authored-by: haijd <haijd@163.com>
2026-09-20 01:08:11 +00:00

127 lines
4.4 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"),
"retrieval_backend": retrieval.backend,
"retrieval_warning": retrieval.warning,
"citations": [
{
"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%}",
f"检索:{retrieval.backend}",
]
if retrieval.warning:
lines.append(f"注意:{retrieval.warning}")
if scheme["citations"]:
lines.append("")
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("")
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,
}