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, }