"""Chat session models and routes. Provides a ChatGPT-style conversational interface backed by the Druppie SDK. Every project gets this out of the box — customize the agent logic in agent.py. Architecture: messages are processed asynchronously. When a user sends a message, the backend saves it, creates a "thinking" placeholder, and processes the agent in a background thread. The frontend polls for updates. This way navigating away doesn't cancel the AI response. """ import threading from datetime import datetime, timezone from sqlalchemy import Column, DateTime, Integer, String, Text, ForeignKey, JSON from sqlalchemy.orm import relationship from app.database import Base, get_db, SessionLocal # --------------------------------------------------------------------------- # Models # --------------------------------------------------------------------------- class ChatSession(Base): __tablename__ = "chat_sessions" id = Column(Integer, primary_key=True, autoincrement=True) title = Column(String(200), default="Nieuw gesprek") created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) updated_at = Column(DateTime, default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc)) messages = relationship("ChatMessage", back_populates="session", cascade="all, delete-orphan", order_by="ChatMessage.created_at") class ChatMessage(Base): __tablename__ = "chat_messages" id = Column(Integer, primary_key=True, autoincrement=True) session_id = Column(Integer, ForeignKey("chat_sessions.id", ondelete="CASCADE"), nullable=False, index=True) role = Column(String(20), nullable=False) # "user" or "assistant" content = Column(Text, nullable=False, default="") # "thinking" while agent processes, "done" when complete, "error" on failure status = Column(String(20), nullable=False, default="done") # Agent metadata — search terms, found documents, reasoning steps metadata_ = Column("metadata", JSON, nullable=True) created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc)) session = relationship("ChatSession", back_populates="messages") # --------------------------------------------------------------------------- # Background agent processing # --------------------------------------------------------------------------- def _process_message_background(session_id: int, assistant_msg_id: int, user_text: str, history: list[dict]): """Run the agent in a background thread with its own DB session.""" db = SessionLocal() try: from app.agent import run_agent result = run_agent(user_text, history, db) msg = db.query(ChatMessage).filter(ChatMessage.id == assistant_msg_id).first() if msg: msg.content = result.get("answer", "") msg.status = "done" msg.metadata_ = result.get("steps") session = db.query(ChatSession).filter(ChatSession.id == session_id).first() if session: session.updated_at = datetime.now(timezone.utc) db.commit() except Exception as e: db.rollback() try: msg = db.query(ChatMessage).filter(ChatMessage.id == assistant_msg_id).first() if msg: msg.content = f"Fout bij verwerking: {e}" msg.status = "error" db.commit() except Exception: pass finally: db.close() # --------------------------------------------------------------------------- # Routes # --------------------------------------------------------------------------- from flask import Blueprint, jsonify, request chat_api = Blueprint("chat_api", __name__) @chat_api.route("/chat/sessions", methods=["POST"]) def create_session(): db = next(get_db()) session = ChatSession() db.add(session) db.commit() db.refresh(session) return jsonify(_session_to_dict(session)), 201 @chat_api.route("/chat/sessions") def list_sessions(): db = next(get_db()) sessions = db.query(ChatSession).order_by(ChatSession.updated_at.desc()).all() return jsonify([_session_to_dict(s) for s in sessions]) @chat_api.route("/chat/sessions/") def get_session(session_id): db = next(get_db()) session = db.query(ChatSession).filter(ChatSession.id == session_id).first() if not session: return jsonify(error="Sessie niet gevonden"), 404 return jsonify({ **_session_to_dict(session), "messages": [_message_to_dict(m) for m in session.messages], }) @chat_api.route("/chat/sessions/", methods=["DELETE"]) def delete_session(session_id): db = next(get_db()) session = db.query(ChatSession).filter(ChatSession.id == session_id).first() if not session: return jsonify(error="Sessie niet gevonden"), 404 db.delete(session) db.commit() return jsonify(ok=True) @chat_api.route("/chat/sessions//rename", methods=["POST"]) def rename_session(session_id): db = next(get_db()) session = db.query(ChatSession).filter(ChatSession.id == session_id).first() if not session: return jsonify(error="Sessie niet gevonden"), 404 data = request.get_json(silent=True) if data and "title" in data: session.title = data["title"][:200] db.commit() return jsonify(_session_to_dict(session)) @chat_api.route("/chat/sessions//messages", methods=["POST"]) def send_message(session_id): """Send a message. Returns immediately with a 'thinking' assistant message. The agent processes in a background thread. Poll GET /sessions/{id} to check when the assistant message status changes from 'thinking' to 'done'. """ db = next(get_db()) session = db.query(ChatSession).filter(ChatSession.id == session_id).first() if not session: return jsonify(error="Sessie niet gevonden"), 404 data = request.get_json(silent=True) if not data or "message" not in data: return jsonify(error="Bericht is verplicht"), 400 user_text = data["message"].strip() if not user_text: return jsonify(error="Leeg bericht"), 400 # Save user message user_msg = ChatMessage(session_id=session_id, role="user", content=user_text) db.add(user_msg) # Auto-title from first message if session.title == "Nieuw gesprek": session.title = user_text[:80] + ("..." if len(user_text) > 80 else "") # Create placeholder assistant message assistant_msg = ChatMessage( session_id=session_id, role="assistant", content="", status="thinking", ) db.add(assistant_msg) db.commit() db.refresh(assistant_msg) # Get conversation history (excluding the new messages) history = [{"role": m.role, "content": m.content} for m in session.messages if m.id not in (user_msg.id, assistant_msg.id) and m.status == "done"] # Process in background thread thread = threading.Thread( target=_process_message_background, args=(session_id, assistant_msg.id, user_text, history), daemon=True, ) thread.start() return jsonify(_message_to_dict(assistant_msg)), 202 def _session_to_dict(s): return { "id": s.id, "title": s.title, "created_at": s.created_at.isoformat() if s.created_at else None, "updated_at": s.updated_at.isoformat() if s.updated_at else None, "message_count": len(s.messages) if s.messages else 0, } def _message_to_dict(m): return { "id": m.id, "role": m.role, "content": m.content, "status": m.status, "steps": m.metadata_, "created_at": m.created_at.isoformat() if m.created_at else None, }