chatbot / ui /callbacks.py
anris05's picture
bot
8a2dcce
Raw
History Blame Contribute Delete
6.68 kB
"""
Gradio callback functions.
These call chat_service / auth / history directly (in-process) rather than
going over HTTP, since the Gradio Blocks app is mounted into the same Flask
process (see app.py). Because Gradio callbacks run outside of a normal
Flask request context, every function that touches the database wraps its
body in `with _flask_app.app_context():`.
Auth state for the UI is NOT the Flask session cookie (that's only for the
REST API in auth/routes.py) — it's tracked per-browser-tab in a gr.State
dict: {"user_id": int, "email": str} or None when logged out.
"""
import time
from auth.db import db
from auth.models import User
from chat_service import handle_chat_message
from history import service as history_service
from logs.logger import get_logger
logger = get_logger(__name__)
_flask_app = None
def init_app(flask_app):
"""Called once from app.py so callbacks can open an app context."""
global _flask_app
_flask_app = flask_app
# ---------------------------------------------------------------------------
# Auth
# ---------------------------------------------------------------------------
def do_signup(email: str, password: str):
"""Returns (auth_state, status_message, is_error)."""
email = (email or "").strip().lower()
password = password or ""
if not email or "@" not in email:
return None, "Please enter a valid email address.", True
if len(password) < 8:
return None, "Password must be at least 8 characters.", True
with _flask_app.app_context():
if User.query.filter_by(email=email).first() is not None:
return None, "An account with this email already exists.", True
user = User(email=email)
user.set_password(password)
db.session.add(user)
db.session.commit()
auth_state = {"user_id": user.id, "email": user.email}
return auth_state, f"Account created. Welcome, {email}!", False
def do_login(email: str, password: str):
"""Returns (auth_state, status_message, is_error)."""
email = (email or "").strip().lower()
password = password or ""
with _flask_app.app_context():
user = User.query.filter_by(email=email).first()
if user is None or not user.check_password(password):
return None, "Invalid email or password.", True
auth_state = {"user_id": user.id, "email": user.email}
return auth_state, f"Logged in as {email}.", False
def do_logout():
return None, "Logged out.", False
# ---------------------------------------------------------------------------
# Chat
# ---------------------------------------------------------------------------
def send_message(user_message: str, chat_display: list, auth_state, session_state, anon_history: list):
"""
Args:
user_message: raw text from the input box.
chat_display: current [{"role": ..., "content": ...}, ...] shown in
gr.Chatbot(type="messages").
auth_state: {"user_id", "email"} or None.
session_state: current chat_session id (logged-in only) or None.
anon_history: client-side rolling history used only when logged out.
Returns:
(chat_display, session_state, anon_history, cleared_textbox)
"""
user_message = (user_message or "").strip()
if not user_message:
return chat_display, session_state, anon_history, ""
chat_display = chat_display + [{"role": "user", "content": user_message}]
is_logged_in = bool(auth_state)
with _flask_app.app_context():
if is_logged_in:
user = User.query.get(auth_state["user_id"])
result = handle_chat_message(
query=user_message,
user=user,
session_id=session_state,
)
session_state = result["session_id"]
else:
result = handle_chat_message(
query=user_message,
user=None,
anonymous_history=anon_history,
)
anon_history = (anon_history or []) + [
{"role": "user", "content": user_message},
{"role": "assistant", "content": result["response"]},
]
chat_display = chat_display + [{"role": "assistant", "content": result["response"]}]
return chat_display, session_state, anon_history, ""
def stream_last_response(chat_display: list):
"""
Fakes token-by-token streaming for the assistant's last message so the
UI feels responsive even though llm.generate() currently returns the
full string at once. Yields progressively longer chat_display copies.
"""
if not chat_display or chat_display[-1]["role"] != "assistant":
yield chat_display
return
full_text = chat_display[-1]["content"]
prefix_display = chat_display[:-1]
step = max(1, len(full_text) // 60)
for i in range(0, len(full_text) + step, step):
partial = full_text[:i]
yield prefix_display + [{"role": "assistant", "content": partial}]
time.sleep(0.012)
yield chat_display
def new_chat():
"""Resets the chat window + clears the active session id."""
return [], None, []
# ---------------------------------------------------------------------------
# History panel
# ---------------------------------------------------------------------------
def refresh_sessions(auth_state):
"""Returns gr.update(...) choices for the session list, newest first."""
import gradio as gr
if not auth_state:
return gr.update(choices=[], value=None)
with _flask_app.app_context():
sessions = history_service.list_sessions(auth_state["user_id"])
choices = [(s["title"] or f"Session {s['id']}", s["id"]) for s in sessions]
return gr.update(choices=choices, value=None)
def open_session(session_id, auth_state):
"""Loads a past session's full transcript into the chat window."""
if not auth_state or session_id is None:
return [], None
with _flask_app.app_context():
full_session = history_service.get_full_session(session_id, auth_state["user_id"])
if full_session is None:
return [], None
chat_display = [
{"role": m["role"], "content": m["content"]} for m in full_session["messages"]
]
return chat_display, session_id
def delete_session(session_id, auth_state):
"""Deletes a session and returns a cleared chat window if it was active."""
if not auth_state or session_id is None:
return [], None
with _flask_app.app_context():
history_service.delete_session(session_id, auth_state["user_id"])
return [], None