File size: 4,429 Bytes
8a2dcce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
"""
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
        )
    # OUT_OF_SCOPE
    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,
    }