| """ |
| Chat orchestration service. |
| |
| This is the single place that wires together everything built in the |
| previous layers for one chat turn: |
| |
| agents.router -> classify + retrieve context |
| llm.prompts -> build the right prompt for the route |
| llm.generate -> call the configured LLM provider |
| memory.mem0_client -> pull/store long-term preference memory (logged-in only) |
| history.service -> persist the transcript (logged-in only) |
| |
| Kept separate from app.py so Flask routes stay thin, and so the same logic |
| can be reused by the Gradio UI layer later without going through HTTP. |
| """ |
|
|
| import config |
| from agents.router import RouteType, route |
| from history import service as history_service |
| from history.session_manager import resolve_session |
| from llm.generate import generate |
| from llm.prompts import ( |
| build_comparison_prompt, |
| build_followup_prompt, |
| build_reject_prompt, |
| build_single_prompt, |
| ) |
| from logs.logger import get_logger |
| from memory.mem0_client import get_mem0_client |
| from rag.retriever import get_retriever |
|
|
| logger = get_logger(__name__) |
|
|
|
|
| def _build_prompt(decision, mem0_context, recent_messages): |
| if decision.route_type == RouteType.SINGLE_TOPIC: |
| return build_single_prompt( |
| decision.single_chunks, mem0_context, recent_messages, decision.query |
| ) |
| if decision.route_type == RouteType.COMPARISON: |
| return build_comparison_prompt( |
| decision.comparison_context, mem0_context, recent_messages, decision.query |
| ) |
| if decision.route_type == RouteType.FOLLOWUP: |
| return build_followup_prompt( |
| decision.followup_context, mem0_context, recent_messages, decision.query |
| ) |
| |
| return build_reject_prompt(decision.query) |
|
|
|
|
| def handle_chat_message( |
| query: str, |
| user=None, |
| session_id: int = None, |
| anonymous_history: list = None, |
| ) -> dict: |
| """ |
| Args: |
| query: the user's message. |
| user: the authenticated auth.models.User, or None if anonymous. |
| session_id: existing chat session id (logged-in users only, optional). |
| anonymous_history: client-managed recent history for anonymous users |
| (no server-side persistence — see spec: "when |
| not logged in, chat works normally, no |
| persistence, no history panel"). Ignored for |
| logged-in users, whose history is read from |
| SQLite instead. |
| |
| Returns: |
| { |
| "response": "...", |
| "route_type": "single_topic" | "comparison" | "followup" | "out_of_scope", |
| "topics": [...], |
| "session_id": int | None, # None for anonymous users |
| } |
| """ |
| is_logged_in = user is not None |
| session_obj = None |
|
|
| if is_logged_in: |
| session_obj = resolve_session(user.id, session_id, first_message=query) |
| recent_messages = history_service.get_recent_messages(session_obj.id) |
| else: |
| recent_messages = (anonymous_history or [])[-config.MAX_RECENT_HISTORY_MESSAGES :] |
|
|
| retriever = get_retriever() |
| decision = route(query, retriever, recent_messages=recent_messages) |
|
|
| mem0_context = [] |
| if is_logged_in: |
| mem0_context = get_mem0_client().get_relevant_context(str(user.id), query) |
|
|
| prompt = _build_prompt(decision, mem0_context, recent_messages) |
| response_text = generate(prompt) |
|
|
| if is_logged_in: |
| history_service.add_message( |
| session_obj.id, |
| role="user", |
| content=query, |
| route_type=decision.route_type.value, |
| retrieved_topics=decision.topics, |
| ) |
| history_service.add_message( |
| session_obj.id, |
| role="assistant", |
| content=response_text, |
| route_type=decision.route_type.value, |
| ) |
| get_mem0_client().add_interaction(str(user.id), query, response_text) |
|
|
| logger.info( |
| "Chat turn complete | user_id=%s | route=%s | topics=%s | session_id=%s", |
| user.id if is_logged_in else None, |
| decision.route_type.value, |
| decision.topics, |
| session_obj.id if session_obj else None, |
| ) |
|
|
| return { |
| "response": response_text, |
| "route_type": decision.route_type.value, |
| "topics": decision.topics, |
| "session_id": session_obj.id if session_obj else None, |
| } |
|
|