Skip to content

Commit 5a1de80

Browse files
committed
feat: Phase 24 - Export PDF/CSV + Multi-turn Conversations
- Export module: PDF export (query report, analytics), CSV export (query history) - Export API: /export/query, /export/analytics, /export/queries/csv, /export/list - Multi-turn context: conversation history included in LLM prompts (last 10 messages) - Session management: create/switch/delete conversation sessions - Conversation API: /conversation/context, /conversation/session, /conversation/sessions - Frontend export buttons: CSV and Analytics PDF export in dashboard header
1 parent b17e1d0 commit 5a1de80

4 files changed

Lines changed: 418 additions & 5 deletions

File tree

PROGRESS.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ caa3f98 feat: expand data to 7 companies, 2075 chunks
129129
- **Cost trend chart** — Daily cost visualization with configurable period
130130
- **Token efficiency** — Cost per 1K tokens by model
131131

132+
### 🔄 Phase 24: Export + Multi-turn Conversations
133+
- **Export module** — PDF export (query report, analytics), CSV export (query history)
134+
- **Export API** — GET /export/query, /export/analytics, /export/queries/csv, /export/list
135+
- **Multi-turn context** — Conversation history included in LLM prompts (last 10 messages)
136+
- **Session management** — Create/switch/delete conversation sessions
137+
- **Conversation API** — GET /conversation/context, POST /conversation/session, GET /conversation/sessions, DELETE /conversation/session/{id}
138+
- **Frontend export buttons** — CSV and Analytics PDF export in dashboard header
139+
132140
---
133141

134142
## API Endpoints
@@ -153,6 +161,14 @@ GET /analytics/models - Model comparison metrics
153161
GET /analytics/routing - Routing breakdown and efficiency
154162
GET /analytics/trend - Cost trend over time
155163
GET /analytics/tokens - Cost per token analysis
164+
GET /export/query - Export query to PDF
165+
GET /export/analytics - Export analytics to PDF
166+
GET /export/queries/csv - Export queries to CSV
167+
GET /export/list - List exported files
168+
GET /conversation/context - Get conversation context
169+
POST /conversation/session - Create/switch session
170+
GET /conversation/sessions - List all sessions
171+
DELETE /conversation/session/{id} - Delete session
156172
```
157173

158174
---
@@ -263,6 +279,7 @@ cost-aware-agentic-rag/
263279
│ ├── ml/
264280
│ │ ├── feedback.py # NEW: Feedback storage and aggregation
265281
│ │ ├── cost_analytics.py # NEW: Model comparison and cost optimization
282+
│ │ ├── export.py # NEW: PDF/CSV export
266283
│ │ ├── evaluation.py # ML evaluation
267284
│ │ └── routing.py # CostAwareRouter (trained classifier)
268285
│ │ ├── models.py # SQLAlchemy models (deprecated notice)

api/main.py

Lines changed: 141 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
)
4646
from src.ml.feedback import store_feedback, get_feedback_stats, get_recent_feedback
4747
from src.ml.cost_analytics import CostAnalytics
48+
from src.ml.export import QueryExporter
4849

4950
logger = 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\nQuestion: {req.query.strip()}\n\nProvide 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\nQuestion: {req.query.strip()}\n\nProvide 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+
498634
def _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

Comments
 (0)