4545)
4646from src .ml .feedback import store_feedback , get_feedback_stats , get_recent_feedback
4747from src .ml .cost_analytics import CostAnalytics
48+ from src .ml .export import QueryExporter
4849
4950logger = logging .getLogger (__name__ )
5051
@@ -314,15 +315,33 @@ def generate():
314315 results = retriever .retrieve (req .query .strip (), top_k = 5 )
315316 context = _build_context (results , max_chars = 2000 )
316317
317- # Build messages
318+ # Get conversation history for multi-turn context
319+ history = memory .get_history (limit = 10 )
320+ history_text = ""
321+ if history :
322+ history_parts = []
323+ for msg in history :
324+ prefix = "User" if msg ["role" ] == "user" else "Assistant"
325+ history_parts .append (f"{ prefix } : { msg ['content' ][:200 ]} " )
326+ history_text = "\n " .join (history_parts )
327+
328+ # Build messages with multi-turn context
318329 messages = [
319330 {"role" : "system" , "content" : SYSTEM_PROMPT },
320- {
321- "role" : "user" ,
322- "content" : f"Document Context:\n { context } \n \n ---\n \n Question: { req .query .strip ()} \n \n Provide a direct answer based on the context above:" ,
323- },
324331 ]
325332
333+ # Add conversation history if available
334+ if history_text :
335+ messages .append ({
336+ "role" : "user" ,
337+ "content" : f"Previous conversation:\n { history_text } \n \n ---" ,
338+ })
339+
340+ messages .append ({
341+ "role" : "user" ,
342+ "content" : f"Document Context:\n { context } \n \n ---\n \n Question: { req .query .strip ()} \n \n Provide a direct answer based on the context above:" ,
343+ })
344+
326345 # Stream response
327346 for chunk in llm .chat_stream (model = model , messages = messages ):
328347 yield f"data: { json .dumps ({'type' : 'token' , 'content' : chunk })} \n \n "
@@ -348,6 +367,10 @@ def generate():
348367 "steps_count" : 3 ,
349368 }
350369 yield f"data: { json .dumps (metadata )} \n \n "
370+
371+ # Store in conversation memory for multi-turn context
372+ memory .add_message ("user" , req .query .strip ())
373+ memory .add_message ("assistant" , full_text if 'full_text' in dir () else "" )
351374 yield "data: [DONE]\n \n "
352375
353376 except Exception as e :
@@ -495,6 +518,119 @@ def comparison_page(request: Request) -> HTMLResponse:
495518 return templates .TemplateResponse (request , "comparison.html" )
496519
497520
521+ # ── Export Endpoints ────────────────────────────────────────────────
522+ @app .get ("/export/query" )
523+ def export_query (
524+ query : str ,
525+ answer : str ,
526+ model_used : str = "unknown" ,
527+ cost_usd : float = 0.0 ,
528+ latency_ms : float = 0.0 ,
529+ steps_count : int = 0 ,
530+ ):
531+ """Export a single query result to PDF."""
532+ exporter = QueryExporter ()
533+ filepath = exporter .export_query_pdf (
534+ query = query ,
535+ answer = answer ,
536+ citations = [], # Could be passed as param
537+ model_used = model_used ,
538+ cost_usd = cost_usd ,
539+ latency_ms = latency_ms ,
540+ steps_count = steps_count ,
541+ )
542+ from fastapi .responses import FileResponse
543+ return FileResponse (
544+ path = str (filepath ),
545+ filename = filepath .name ,
546+ media_type = "application/pdf" ,
547+ )
548+
549+
550+ @app .get ("/export/analytics" )
551+ def export_analytics ():
552+ """Export analytics summary to PDF."""
553+ analytics = CostAnalytics ()
554+ data = {
555+ "models" : analytics .model_comparison ().get ("models" , []),
556+ "routing_efficiency" : analytics .routing_breakdown ().get ("routing_efficiency" , {}),
557+ }
558+ exporter = QueryExporter ()
559+ filepath = exporter .export_analytics_pdf (data )
560+ from fastapi .responses import FileResponse
561+ return FileResponse (
562+ path = str (filepath ),
563+ filename = filepath .name ,
564+ media_type = "application/pdf" ,
565+ )
566+
567+
568+ @app .get ("/export/queries/csv" )
569+ def export_queries_csv ():
570+ """Export query history to CSV."""
571+ from src .generation .cost_tracker import CostTracker
572+ tracker = CostTracker ()
573+ history = tracker .load_history ()
574+
575+ queries = []
576+ for r in history :
577+ queries .append ({
578+ "query" : r .query ,
579+ "answer" : "" , # Not stored in cost tracker
580+ "model_used" : r .model ,
581+ "complexity" : r .complexity ,
582+ "cost_usd" : r .cost_usd ,
583+ "latency_ms" : r .latency_ms ,
584+ "citations" : [],
585+ "timestamp" : r .timestamp ,
586+ })
587+
588+ exporter = QueryExporter ()
589+ filepath = exporter .export_queries_csv (queries )
590+ from fastapi .responses import FileResponse
591+ return FileResponse (
592+ path = str (filepath ),
593+ filename = filepath .name ,
594+ media_type = "text/csv" ,
595+ )
596+
597+
598+ @app .get ("/export/list" )
599+ def list_exports ():
600+ """List all exported files."""
601+ exporter = QueryExporter ()
602+ return {"exports" : exporter .list_exports ()}
603+
604+
605+ # ── Multi-turn Conversation Endpoints ───────────────────────────────
606+ @app .get ("/conversation/context" )
607+ def conversation_context ():
608+ """Get conversation context string for LLM."""
609+ context = memory .get_context_string ()
610+ return {"context" : context , "history" : memory .get_history (limit = 10 )}
611+
612+
613+ @app .post ("/conversation/session" )
614+ def create_session (session_id : str ):
615+ """Create or switch to a conversation session."""
616+ memory .current_session = session_id
617+ return {"status" : "ok" , "session_id" : session_id }
618+
619+
620+ @app .get ("/conversation/sessions" )
621+ def list_sessions ():
622+ """List all conversation sessions."""
623+ return {"sessions" : memory .get_session_ids ()}
624+
625+
626+ @app .delete ("/conversation/session/{session_id}" )
627+ def delete_session (session_id : str ):
628+ """Delete a conversation session."""
629+ if session_id in memory .conversations :
630+ del memory .conversations [session_id ]
631+ return {"status" : "deleted" }
632+
633+
498634def _build_context (results : list , max_chars : int = 2000 ) -> str :
499635 """Build context string from retrieval results."""
500636 from src .agents .graph import _build_context_from_tools
0 commit comments