chatbot / chat_service.py
anris05's picture
bot
8a2dcce
Raw
History Blame Contribute Delete
4.43 kB
"""
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,
}