Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- README.md +575 -564
- backend/api/chat.py +509 -511
- backend/modules/reasoning_engine.py +6 -6
- backend/requirements.txt +68 -67
- backend/workers/compilation_worker.py +213 -210
README.md
CHANGED
|
@@ -1,564 +1,575 @@
|
|
| 1 |
-
---
|
| 2 |
-
title: Mexar
|
| 3 |
-
emoji: 🧠
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
license: mit
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
<div align="center">
|
| 12 |
-
|
| 13 |
-
# 🧠 MEXAR
|
| 14 |
-
|
| 15 |
-
### **M**ultimodal **E**xplainable **A**I **R**easoning Assistant
|
| 16 |
-
|
| 17 |
-
*Build domain-specific AI agents from your documents — with transparent, grounded, and faithful answers.*
|
| 18 |
-
|
| 19 |
-
[](https://www.python.org/)
|
| 20 |
-
[](https://fastapi.tiangolo.com/)
|
| 21 |
-
[](https://reactjs.org/)
|
| 22 |
-
[](https://groq.com/)
|
| 23 |
-
[](https://supabase.com/)
|
| 24 |
-
[](LICENSE)
|
| 25 |
-
|
| 26 |
-
<br/>
|
| 27 |
-
|
| 28 |
-
**🚀 Live App** → [mexar.vercel.app](https://mexar.vercel.app) | **📡 Backend API** → [devrajsinh2012-mexar.hf.space](https://devrajsinh2012-mexar.hf.space) | **📖 API Docs** → [/docs](https://devrajsinh2012-mexar.hf.space/docs)
|
| 29 |
-
|
| 30 |
-
</div>
|
| 31 |
-
|
| 32 |
-
---
|
| 33 |
-
|
| 34 |
-
## 📖 What is MEXAR?
|
| 35 |
-
|
| 36 |
-
MEXAR is a **full-stack, production-ready RAG (Retrieval-Augmented Generation) platform** that lets you create custom AI agents from your own documents. Unlike a simple chatbot, MEXAR is built around **explainability and faithfulness** — every answer is grounded in your source data, cited with inline references, and scored for hallucination risk using a NLI model.
|
| 37 |
-
|
| 38 |
-
**You upload documents → MEXAR compiles an agent → You chat with grounded, explainable AI.**
|
| 39 |
-
|
| 40 |
-
---
|
| 41 |
-
|
| 42 |
-
## ✨ Core Features
|
| 43 |
-
|
| 44 |
-
| Feature | Description |
|
| 45 |
-
|---|---|
|
| 46 |
-
| 🔍 **Hybrid RAG Search** | Semantic (pgvector cosine) + Keyword (BM25 tsvector) fused via Reciprocal Rank Fusion (RRF) |
|
| 47 |
-
| 🎯 **Cross-Encoder Reranking** | `sentence-transformers` cross-encoder re-scores top candidates for precision |
|
| 48 |
-
| 📎 **Inline Source Attribution** | Every answer references exact source chunks with `[1]`, `[2]` citations |
|
| 49 |
-
| ✅ **DeBERTa-v3 Faithfulness Scoring** | NLI-based hallucination detection scores answer grounding against retrieved context |
|
| 50 |
-
| 🔐 **Domain Guardrails** | TF-IDF + spaCy NER Jaccard similarity prevents out-of-domain queries (F1 = 0.9072 at threshold 0.25) |
|
| 51 |
-
| 🗣️ **Multimodal Input** | Audio (Groq Whisper), Images (Groq Vision), Video (OpenCV frame extraction) |
|
| 52 |
-
| 🔊 **Text-to-Speech** | ElevenLabs API + Web Speech API fallback |
|
| 53 |
-
| 🧠 **Explainability Panel** | Full reasoning trace: retrieval scores, confidence breakdown, sources cited, guardrail status |
|
| 54 |
-
| 📁 **5 Document Formats** | PDF, DOCX, CSV, JSON, TXT |
|
| 55 |
-
| ⚡ **Real-time WebSocket** | Streaming chat via WebSocket with progress tracking |
|
| 56 |
-
| 🔑 **JWT Auth** | Secure user accounts with bcrypt-hashed passwords and JWT bearer tokens |
|
| 57 |
-
|
| 58 |
-
---
|
| 59 |
-
|
| 60 |
-
## 🏗️ System Architecture
|
| 61 |
-
|
| 62 |
-
MEXAR is composed of four layers: Frontend, API, Intelligence, and Storage.
|
| 63 |
-
|
| 64 |
-
```
|
| 65 |
-
┌─────────────────────────────────────────────────────────────────────────────┐
|
| 66 |
-
│ USER INTERACTION LAYER │
|
| 67 |
-
│ │
|
| 68 |
-
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
| 69 |
-
│ │ React 18 Frontend ─ Vercel Edge Network │ │
|
| 70 |
-
│ │ Landing · Login · Dashboard · AgentCreation · Chat · Explainability │ │
|
| 71 |
-
│ └────────────────────────────┬─────────────────────────────────────────┘ │
|
| 72 |
-
│ │ HTTPS / WebSocket │
|
| 73 |
-
└────────────────────────────────┼────────────────────────────────────────────┘
|
| 74 |
-
│
|
| 75 |
-
┌────────────────────────────────▼────────────────────────────────────────────┐
|
| 76 |
-
│ FASTAPI BACKEND (HF Spaces / Docker) │
|
| 77 |
-
│ │
|
| 78 |
-
│ ┌─────────
|
| 79 |
-
│ │ /auth │ │ /agents │ │ /chat │ │ /compile │ │ /websocket │ │
|
| 80 |
-
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │
|
| 81 |
-
│ │
|
| 82 |
-
└────────────────────────────────┬────────────────────────────────────────────┘
|
| 83 |
-
│
|
| 84 |
-
┌────────────────────────────────▼────────────────────────────────────────────┐
|
| 85 |
-
│ CORE INTELLIGENCE LAYER │
|
| 86 |
-
│ │
|
| 87 |
-
│ ┌──────────────────┐ ┌──────────────────┐ ┌─────────────────────┐ │
|
| 88 |
-
│ │ DataValidator │ │ KnowledgeCompiler│ │ MultimodalProcessor │ │
|
| 89 |
-
│ │ PDF/DOCX/CSV/TXT │───▶│ Chunking + Embed │ │ Whisper · Vision │ │
|
| 90 |
-
│ │ /JSON parsing │ │ FastEmbed bge-384│ │ OpenCV frames │ │
|
| 91 |
-
│ └──────────────────┘ └────────┬─────────┘ └──────────┬──────────┘ │
|
| 92 |
-
│ │ Store chunks │ Text │
|
| 93 |
-
│ ┌──────────────────┐ ▼ ▼ │
|
| 94 |
-
│ │ PromptAnalyzer │ ┌───────────────────────────────────────────────┐ │
|
| 95 |
-
│ │ Intent · Domain │───▶│ ReasoningEngine (RAG Core) │ │
|
| 96 |
-
│ │ Query Rewrite │ │ │ │
|
| 97 |
-
│ └──────────────────┘ │ 1. Domain Guardrail (TF-IDF + NER Jaccard) │ │
|
| 98 |
-
│ │ 2. HybridSearcher (pgvector + BM25 RRF) │ │
|
| 99 |
-
│ ┌──────────────────┐ │ 3. CrossEncoder Reranker │ │
|
| 100 |
-
│ │ ExplainabilityGen│◀───│ 4. SourceAttributor (citation tracking) │ │
|
| 101 |
-
│ │ Reasoning trace │ │ 5. Groq LLM Answer Generation │ │
|
| 102 |
-
│ │ Confidence score │ │ 6. DeBERTa-v3 Faithfulness Scoring │ │
|
| 103 |
-
│ └──────────────────┘ └───────────────────────────────────────────────┘ │
|
| 104 |
-
│ │
|
| 105 |
-
└────────────────────────────────┬────────────────────────────────────────────┘
|
| 106 |
-
│
|
| 107 |
-
┌────────────────────────────────▼────────────────────────────────────────────┐
|
| 108 |
-
│ EXTERNAL SERVICES LAYER │
|
| 109 |
-
│ │
|
| 110 |
-
│ ┌──────────────────────┐ ┌──────────────────┐ ┌───────────────────────┐ │
|
| 111 |
-
│ │ Supabase / PostgreSQL│ │ Groq Cloud │ │ ElevenLabs │ │
|
| 112 |
-
│ │ pgvector extension │ │ Llama 3.3 · 3.1 │ │ Text-to-Speech API │ │
|
| 113 |
-
│ │ BM25 tsvector FTS │ │ Whisper v3 Large │ │ │ │
|
| 114 |
-
│ │ JWT sessions │ │ Vision (preview) │ └───────────────────────┘ │
|
| 115 |
-
│ └──────────────────────┘ └──────────────────┘ │
|
| 116 |
-
│ │
|
| 117 |
-
└─────────────────────────────────────────────────────────────────────────────┘
|
| 118 |
-
```
|
| 119 |
-
|
| 120 |
-
---
|
| 121 |
-
|
| 122 |
-
## 🔄 Request Lifecycle — Step by Step
|
| 123 |
-
|
| 124 |
-
```
|
| 125 |
-
User Query
|
| 126 |
-
│
|
| 127 |
-
▼
|
| 128 |
-
┌──────────────────────────────────────────────────────┐
|
| 129 |
-
│ 1. MULTIMODAL INPUT (optional) │
|
| 130 |
-
│ Audio → Groq Whisper STT → text │
|
| 131 |
-
│ Image → Groq Vision → described text │
|
| 132 |
-
│ Video → OpenCV frame extract → Vision │
|
| 133 |
-
└────────────────────────┬─────────────────────────────┘
|
| 134 |
-
│
|
| 135 |
-
▼
|
| 136 |
-
┌──────────────────────────────────────────────────────┐
|
| 137 |
-
│ 2. PROMPT ANALYSIS │
|
| 138 |
-
│ • Parse intent (factual / analytical / compare) │
|
| 139 |
-
│ • Detect domain topic │
|
| 140 |
-
│ • Optionally rewrite query for clarity │
|
| 141 |
-
└────────────────────────┬─────────────────────────────┘
|
| 142 |
-
│
|
| 143 |
-
▼
|
| 144 |
-
┌──────────────────────────────────────────────────────┐
|
| 145 |
-
│ 3. DOMAIN GUARDRAIL CHECK │
|
| 146 |
-
│ • TF-IDF cosine similarity vs agent signature │
|
| 147 |
-
│ • spaCy NER entity Jaccard overlap │
|
| 148 |
-
│ • Threshold = 0.25 (F1 = 0.9072) │
|
| 149 |
-
│ • If below threshold → reject with explanation │
|
| 150 |
-
└────────────────────────┬─────────────────────────────┘
|
| 151 |
-
│
|
| 152 |
-
▼
|
| 153 |
-
┌──────────────────────────────────────────────────────┐
|
| 154 |
-
│ 4. HYBRID RETRIEVAL │
|
| 155 |
-
│ • Dense: FastEmbed bge-small-en (384-dim) │
|
| 156 |
-
│ → pgvector cosine similarity search │
|
| 157 |
-
│ • Sparse: PostgreSQL tsvector BM25 FTS │
|
| 158 |
-
│ • Fuse both via Reciprocal Rank Fusion (RRF) │
|
| 159 |
-
│ score = Σ 1/(rank + 60) │
|
| 160 |
-
│ • Return top-K=20 candidate chunks │
|
| 161 |
-
└────────────────────────┬─────────────────────────────┘
|
| 162 |
-
│
|
| 163 |
-
▼
|
| 164 |
-
┌──────────────────────────────────────────────────────┐
|
| 165 |
-
│ 5. CROSS-ENCODER RERANKING │
|
| 166 |
-
│ • sentence-transformers cross-encoder │
|
| 167 |
-
│ • Re-scores top candidates for relevance │
|
| 168 |
-
│ • Selects top-5 chunks as final context │
|
| 169 |
-
└────────────────────────┬─────────────────────────────┘
|
| 170 |
-
│
|
| 171 |
-
▼
|
| 172 |
-
┌──────────────────────────────────────────────────────┐
|
| 173 |
-
│ 6. LLM ANSWER GENERATION │
|
| 174 |
-
│ • Build system prompt with retrieved context │
|
| 175 |
-
│ • Multi-model Groq inference with auto-fallback: │
|
| 176 |
-
│ llama-3.3-70b → llama-3.1-8b → mixtral-8x7b │
|
| 177 |
-
│ • Answer generated with citations embedded │
|
| 178 |
-
└────────────────────────┬─────────────────────────────┘
|
| 179 |
-
│
|
| 180 |
-
▼
|
| 181 |
-
┌──────────────────────────────────────────────────────┐
|
| 182 |
-
│ 7. SOURCE ATTRIBUTION │
|
| 183 |
-
│ • Match answer sentences → source chunks │
|
| 184 |
-
│ • Assign [1], [2], [3] reference markers │
|
| 185 |
-
│ • Track provenance per claim │
|
| 186 |
-
└────────────────────────┬─────────────────────────────┘
|
| 187 |
-
│
|
| 188 |
-
▼
|
| 189 |
-
┌──────────────────────────────────────────────────────┐
|
| 190 |
-
│ 8. FAITHFULNESS SCORING (DeBERTa-v3 NLI) │
|
| 191 |
-
│ • Extract claims from answer │
|
| 192 |
-
│ • For each claim-chunk pair, NLI inference: │
|
| 193 |
-
│ entailment → faithful │
|
| 194 |
-
│ contradiction → hallucinated │
|
| 195 |
-
│ • Batched with torch.inference_mode() (~1.2s) │
|
| 196 |
-
│ • Output: faithfulness score 0.0–1.0 │
|
| 197 |
-
└────────────────────────┬─────────────────────────────┘
|
| 198 |
-
│
|
| 199 |
-
▼
|
| 200 |
-
┌──────────────────────────────────────────────────────┐
|
| 201 |
-
│ 9. EXPLAINABILITY PACKAGING │
|
| 202 |
-
│ • Reasoning trace (step-by-step) │
|
| 203 |
-
│ • Confidence breakdown (domain + faithfulness) │
|
| 204 |
-
│ • Sources cited (with file name + chunk text) │
|
| 205 |
-
│ • Guardrail decision log │
|
| 206 |
-
└────────────────────────┬─────────────────────────────┘
|
| 207 |
-
│
|
| 208 |
-
▼
|
| 209 |
-
Response to User
|
| 210 |
-
(Answer + Citations
|
| 211 |
-
+ Faithfulness Score
|
| 212 |
-
+ Explainability Panel)
|
| 213 |
-
```
|
| 214 |
-
|
| 215 |
-
---
|
| 216 |
-
|
| 217 |
-
## 🗂️ Project Structure
|
| 218 |
-
|
| 219 |
-
```
|
| 220 |
-
Mexar-main/
|
| 221 |
-
│
|
| 222 |
-
├── backend/ # FastAPI Python backend
|
| 223 |
-
│ ├── api/ # Route handlers
|
| 224 |
-
│ │ ├── auth.py # JWT login / register
|
| 225 |
-
│ │ ├── agents.py # Agent CRUD operations
|
| 226 |
-
│ │ ├── chat.py # Chat endpoint (REST)
|
| 227 |
-
│ │ ├── compile.py # Knowledge compilation jobs
|
| 228 |
-
│ │ ├── websocket.py # Streaming WebSocket chat
|
| 229 |
-
│ │ ├── admin.py # Admin panel routes
|
| 230 |
-
│ │ └── diagnostics.py # System health checks
|
| 231 |
-
│ │
|
| 232 |
-
│ ├── modules/ # Core AI intelligence
|
| 233 |
-
│ │ ├── reasoning_engine.py # Main RAG pipeline (634 lines)
|
| 234 |
-
│ │ ├── knowledge_compiler.py # Doc ingestion + embedding
|
| 235 |
-
│ │ ├── data_validator.py # File parsing (PDF/DOCX/CSV/TXT/JSON)
|
| 236 |
-
│ │ ├── prompt_analyzer.py # Intent + domain classification
|
| 237 |
-
│ │ ├── multimodal_processor.py # Audio/Image/Video → text
|
| 238 |
-
│ │ └── explainability.py # Reasoning trace packaging
|
| 239 |
-
│ │
|
| 240 |
-
│ ├── utils/ # Utility modules
|
| 241 |
-
│ │ ├── hybrid_search.py # pgvector + BM25 + RRF fusion
|
| 242 |
-
│ │ ├── faithfulness.py # DeBERTa-v3 NLI scorer
|
| 243 |
-
│ │ ├── groq_client.py # Multi-model Groq client + fallback
|
| 244 |
-
│ │ ├── reranker.py # Cross-encoder reranking
|
| 245 |
-
│ │ ├── source_attribution.py # Citation tracking
|
| 246 |
-
│ │ ├── semantic_chunker.py # Adaptive text chunking
|
| 247 |
-
│ │ └── domain_signature.py # TF-IDF + NER signature builder
|
| 248 |
-
│ │
|
| 249 |
-
│ ├── models/ # SQLAlchemy ORM models
|
| 250 |
-
│ │ ├── user.py # User model
|
| 251 |
-
│ │ ├── agent.py # Agent + CompilationJob
|
| 252 |
-
│ │ ├── chunk.py # DocumentChunk (with vector)
|
| 253 |
-
│ │ └── conversation.py # Conversation + Message
|
| 254 |
-
│ │
|
| 255 |
-
│ ├── migrations/
|
| 256 |
-
│ │ └── hybrid_search_function.sql # PostgreSQL RRF function
|
| 257 |
-
│ │
|
| 258 |
-
│ ├── evaluation/ # Phase 3 benchmark suite
|
| 259 |
-
│ │ ├── run_all.py # Master evaluation runner
|
| 260 |
-
│ │ └── guardrail_threshold_sweep.py
|
| 261 |
-
│ │
|
| 262 |
-
│ ├── scripts/ # Data collection scripts
|
| 263 |
-
│ │ ├── fetch_pubmed.py # NCBI PubMed Open Access
|
| 264 |
-
│ │ ├── fetch_courtlistener.py # CourtListener v4 API
|
| 265 |
-
│ │ └── fetch_secedgar.py # SEC EDGAR 10-K filings
|
| 266 |
-
│ │
|
| 267 |
-
│ ├── static/index.html # HF Spaces landing page
|
| 268 |
-
│ ├── main.py # FastAPI application entry
|
| 269 |
-
│ └── requirements.txt # Python dependencies
|
| 270 |
-
│
|
| 271 |
-
├── frontend/ # React 18 frontend
|
| 272 |
-
│ └── src/
|
| 273 |
-
│ ├── pages/
|
| 274 |
-
│ │ ├── Landing.jsx # Marketing home page
|
| 275 |
-
│ │ ├── Login.jsx # Authentication
|
| 276 |
-
│ │ ├── Dashboard.jsx # Agent management hub
|
| 277 |
-
│ │ ├── AgentCreation.jsx # Upload + configure agent
|
| 278 |
-
│ │ ├── AgentList.jsx # Browse your agents
|
| 279 |
-
│ │ ├── Chat.jsx # Full chat interface (39KB)
|
| 280 |
-
│ │ └── CompilationProgress.jsx # Live compilation view
|
| 281 |
-
│ │
|
| 282 |
-
│ └── components/
|
| 283 |
-
│ ├── ExplainabilityModal.jsx # Reasoning trace viewer
|
| 284 |
-
│ ├── KnowledgeGraph.jsx # Visual knowledge graph
|
| 285 |
-
│ ├── AudioRecorder.jsx # Browser microphone input
|
| 286 |
-
│ ├── TTSPlayer.jsx # TTS playback
|
| 287 |
-
│ ├── InlineTTS.jsx # Per-sentence TTS
|
| 288 |
-
│ └── AgentSwitcher.jsx # Switch between agents
|
| 289 |
-
│
|
| 290 |
-
├── test_data/ # Real evaluation datasets
|
| 291 |
-
│ ├── medical_real/ # 31 PubMed PMC open-access papers
|
| 292 |
-
│ ├── legal_real/ # 148 CourtListener judicial opinions
|
| 293 |
-
│ ├── financial_real/ # 4 SEC EDGAR 10-K filings
|
| 294 |
-
│ └── query_sets/ # Evaluation query sets per domain
|
| 295 |
-
│
|
| 296 |
-
├── Dockerfile # Container definition (HF Spaces)
|
| 297 |
-
└── README.md
|
| 298 |
-
```
|
| 299 |
-
|
| 300 |
-
---
|
| 301 |
-
|
| 302 |
-
## 📊 Empirical Evaluation Results & Benchmarks
|
| 303 |
-
|
| 304 |
-
MEXAR has been evaluated against established baselines on real datasets sourced via public APIs.
|
| 305 |
-
|
| 306 |
-
### Knowledge Base — Real Multi-Domain Corpus
|
| 307 |
-
|
| 308 |
-
| Domain | Data Source | Files | Vector Chunks | Domain Signature Terms |
|
| 309 |
-
|---|---|:---:|:---:|:---:|
|
| 310 |
-
| 🏥 **Medical** | NCBI PubMed Central Open Access | 31 papers | **556 chunks** | 127 terms |
|
| 311 |
-
| ⚖️ **Legal** | CourtListener REST API v4 | 148 opinions | **157 chunks** | 152 terms |
|
| 312 |
-
| 📈 **Financial** | SEC EDGAR 10-K Filings | 4 filings | **68 chunks** | 119 terms |
|
| 313 |
-
|
| 314 |
-
### Table I — Multi-System Faithfulness Comparison
|
| 315 |
-
|
| 316 |
-
| System | Medical ↑ | Legal ↑ | Financial ↑ |
|
| 317 |
-
|---|:---:|:---:|:---:|
|
| 318 |
-
| Naive RAG | 0.0222 | 0.0333 | 0.0000 |
|
| 319 |
-
| BM25-only Retrieval | 0.0000 | 0.0000 | 0.0000 |
|
| 320 |
-
| LangChain RAG | 0.5000 | 0.5000 | 0.5000 |
|
| 321 |
-
| Self-RAG | 0.2380 | 0.0833 | N/A |
|
| 322 |
-
| **🧠 MEXAR (Ours)** | **0.1000** | **0.1000** | N/A |
|
| 323 |
-
|
| 324 |
-
> *Faithfulness scored via DeBERTa-v3-base NLI. Higher = better grounding.*
|
| 325 |
-
|
| 326 |
-
### Table II — Domain Guardrail Performance
|
| 327 |
-
|
| 328 |
-
| Metric | Value |
|
| 329 |
-
|---|:---:|
|
| 330 |
-
| Optimal Threshold | **0.25** |
|
| 331 |
-
| F1 Score | **0.9072** |
|
| 332 |
-
| Method | TF-IDF cosine + spaCy NER Jaccard |
|
| 333 |
-
| Mean Latency | **113.49 ms** |
|
| 334 |
-
|
| 335 |
-
### Table III — System Latency Profile
|
| 336 |
-
|
| 337 |
-
| Component | Latency |
|
| 338 |
-
|---|:---:|
|
| 339 |
-
| DeBERTa NLI Faithfulness (vectorized batch) | **~1.2s / query** |
|
| 340 |
-
| Domain Guardrail check | **113.49 ms** |
|
| 341 |
-
| Hybrid RRF Search (pgvector + BM25) | **< 100 ms** |
|
| 342 |
-
| Groq LLM inference (llama-3.1-8b) | **~800 ms** |
|
| 343 |
-
|
| 344 |
-
> **50x speedup** on faithfulness scoring achieved via `torch.inference_mode()` vectorized batching over the naive sequential baseline (~70s → ~1.2s).
|
| 345 |
-
|
| 346 |
-
### Expected Calibration Error (ECE)
|
| 347 |
-
> **ECE = 0.1000** — confidence scores are well-calibrated against empirical answer accuracy.
|
| 348 |
-
|
| 349 |
-
---
|
| 350 |
-
|
| 351 |
-
## 🚀 Quick Start
|
| 352 |
-
|
| 353 |
-
### Prerequisites
|
| 354 |
-
|
| 355 |
-
- Python 3.9+
|
| 356 |
-
- Node.js 18+
|
| 357 |
-
- PostgreSQL with `pgvector` extension (or [Supabase](https://supabase.com) free tier)
|
| 358 |
-
- [Groq API Key](https://console.groq.com) — free tier available
|
| 359 |
-
|
| 360 |
-
---
|
| 361 |
-
|
| 362 |
-
### 1. Clone & Configure
|
| 363 |
-
|
| 364 |
-
```bash
|
| 365 |
-
git clone https://github.com/devrajsinh2012/Mexar.git
|
| 366 |
-
cd Mexar-main
|
| 367 |
-
```
|
| 368 |
-
|
| 369 |
-
```bash
|
| 370 |
-
# Copy backend environment file
|
| 371 |
-
cp backend/.env.example backend/.env
|
| 372 |
-
# Fill in your credentials (see Environment Variables below)
|
| 373 |
-
```
|
| 374 |
-
|
| 375 |
-
---
|
| 376 |
-
|
| 377 |
-
### 2. Backend Setup
|
| 378 |
-
|
| 379 |
-
```bash
|
| 380 |
-
cd backend
|
| 381 |
-
pip install -r requirements.txt
|
| 382 |
-
|
| 383 |
-
# Install spaCy model required for domain guardrail
|
| 384 |
-
python -m spacy download en_core_web_sm
|
| 385 |
-
|
| 386 |
-
# Apply database migration (PostgreSQL RRF hybrid search function)
|
| 387 |
-
psql $DATABASE_URL -f migrations/hybrid_search_function.sql
|
| 388 |
-
|
| 389 |
-
# Start backend server
|
| 390 |
-
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
| 391 |
-
```
|
| 392 |
-
|
| 393 |
-
Backend available at: `http://localhost:8000`
|
| 394 |
-
Interactive API docs: `http://localhost:8000/docs`
|
| 395 |
-
|
| 396 |
-
---
|
| 397 |
-
|
| 398 |
-
### 3. Frontend Setup
|
| 399 |
-
|
| 400 |
-
```bash
|
| 401 |
-
cd frontend
|
| 402 |
-
npm install
|
| 403 |
-
|
| 404 |
-
# Set API URL
|
| 405 |
-
echo "REACT_APP_API_URL=http://localhost:8000" > .env
|
| 406 |
-
|
| 407 |
-
npm start
|
| 408 |
-
```
|
| 409 |
-
|
| 410 |
-
Frontend available at: `http://localhost:3000`
|
| 411 |
-
|
| 412 |
-
---
|
| 413 |
-
|
| 414 |
-
## 🔑 Environment Variables
|
| 415 |
-
|
| 416 |
-
```bash
|
| 417 |
-
# backend/.env
|
| 418 |
-
|
| 419 |
-
# === REQUIRED ===
|
| 420 |
-
GROQ_API_KEY=your_groq_api_key_here # https://console.groq.com
|
| 421 |
-
DATABASE_URL=postgresql://user:pass@host:5432/db
|
| 422 |
-
SECRET_KEY=your_secure_jwt_secret_key
|
| 423 |
-
SUPABASE_URL=https://your-project.supabase.co
|
| 424 |
-
SUPABASE_KEY=your_supabase_service_role_key
|
| 425 |
-
|
| 426 |
-
# === OPTIONAL ===
|
| 427 |
-
ELEVENLABS_API_KEY=your_elevenlabs_api_key # Text-to-speech
|
| 428 |
-
FRONTEND_URL=https://mexar.vercel.app # CORS origin
|
| 429 |
-
|
| 430 |
-
# === DATASET COLLECTION (scripts/) ===
|
| 431 |
-
COURTLISTENER_TOKEN=your_cl_token # courtlistener.com
|
| 432 |
-
NCBI_EMAIL=your@email.com # NCBI policy requirement
|
| 433 |
-
NCBI_API_KEY=your_ncbi_api_key # Raises rate limit 3→10 req/s
|
| 434 |
-
SEC_USER_AGENT=Firstname Lastname your@email.com # SEC EDGAR fair access
|
| 435 |
-
```
|
| 436 |
-
|
| 437 |
-
---
|
| 438 |
-
|
| 439 |
-
## 🐳 Docker / Hugging Face Spaces Deployment
|
| 440 |
-
|
| 441 |
-
The project ships with a ready-to-use `Dockerfile` and is live on HF Spaces.
|
| 442 |
-
|
| 443 |
-
```bash
|
| 444 |
-
# Build locally
|
| 445 |
-
docker build -t mexar-backend ./backend
|
| 446 |
-
docker run -p 8000:8000 --env-file backend/.env mexar-backend
|
| 447 |
-
```
|
| 448 |
-
|
| 449 |
-
For **Hugging Face Spaces**, push to the `hf` remote:
|
| 450 |
-
|
| 451 |
-
```bash
|
| 452 |
-
git remote add hf https://huggingface.co/spaces/devrajsinh2012/mexar.git
|
| 453 |
-
git push hf main
|
| 454 |
-
```
|
| 455 |
-
|
| 456 |
-
---
|
| 457 |
-
|
| 458 |
-
## 📡 API Reference
|
| 459 |
-
|
| 460 |
-
| Method | Endpoint | Description |
|
| 461 |
-
|---|---|---|
|
| 462 |
-
| `POST` | `/api/auth/register` | Register a new user account |
|
| 463 |
-
| `POST` | `/api/auth/login` | Login and receive JWT token |
|
| 464 |
-
| `GET` | `/api/agents/` | List all compiled agents |
|
| 465 |
-
| `POST` | `/api/agents/` | Create a new agent |
|
| 466 |
-
| `POST` | `/api/compile/` | Start knowledge compilation from uploaded files |
|
| 467 |
-
| `GET` | `/api/compile/{job_id}` | Poll compilation job status |
|
| 468 |
-
| `POST` | `/api/chat/` | Send a query to an agent (REST) |
|
| 469 |
-
| `WS` | `/ws/chat/{agent_id}` | Real-time streaming chat (WebSocket) |
|
| 470 |
-
| `GET` | `/api/health` | Health check |
|
| 471 |
-
| `GET` | `/docs` | Interactive Swagger UI |
|
| 472 |
-
|
| 473 |
-
Full interactive documentation: [devrajsinh2012-mexar.hf.space/docs](https://devrajsinh2012-mexar.hf.space/docs)
|
| 474 |
-
|
| 475 |
-
---
|
| 476 |
-
|
| 477 |
-
## 🧠 Groq Model Fallback Chain
|
| 478 |
-
|
| 479 |
-
MEXAR implements a resilient multi-model fallback for Groq API rate limits:
|
| 480 |
-
|
| 481 |
-
```
|
| 482 |
-
openai/gpt-oss-120b
|
| 483 |
-
│ (429 TPD quota)
|
| 484 |
-
▼
|
| 485 |
-
llama-3.3-70b-versatile
|
| 486 |
-
│ (429 TPD quota)
|
| 487 |
-
▼
|
| 488 |
-
llama-3.1-8b-instant
|
| 489 |
-
│ (429 TPD quota)
|
| 490 |
-
▼
|
| 491 |
-
mixtral-8x7b-32768
|
| 492 |
-
│ (429 TPD quota)
|
| 493 |
-
▼
|
| 494 |
-
gemma2-9b-it
|
| 495 |
-
```
|
| 496 |
-
|
| 497 |
-
This ensures zero-downtime inference even under heavy usage within free-tier quotas.
|
| 498 |
-
|
| 499 |
-
---
|
| 500 |
-
|
| 501 |
-
## 🧪 Running Evaluations
|
| 502 |
-
|
| 503 |
-
```bash
|
| 504 |
-
# Fetch real datasets (requires API keys in .env)
|
| 505 |
-
python backend/scripts/fetch_pubmed.py # NCBI PubMed
|
| 506 |
-
python backend/scripts/fetch_courtlistener.py # CourtListener
|
| 507 |
-
python backend/scripts/fetch_secedgar.py # SEC EDGAR
|
| 508 |
-
|
| 509 |
-
# Recompile domain agents from real data
|
| 510 |
-
python backend/scripts/recompile_agents_from_real_data.py
|
| 511 |
-
|
| 512 |
-
# Run full Phase 3 evaluation pipeline
|
| 513 |
-
python backend/evaluation/run_all.py
|
| 514 |
-
|
| 515 |
-
# Results saved to:
|
| 516 |
-
# backend/evaluation_outputs/full_evaluation_<timestamp>.json
|
| 517 |
-
```
|
| 518 |
-
|
| 519 |
-
---
|
| 520 |
-
|
| 521 |
-
## 🛠️ Tech Stack
|
| 522 |
-
|
| 523 |
-
| Layer | Technology |
|
| 524 |
-
|---|---|
|
| 525 |
-
| **Frontend** | React 18, React Router, Vercel |
|
| 526 |
-
| **Backend** | FastAPI 0.109, Uvicorn, Python 3.9+ |
|
| 527 |
-
| **Database** | PostgreSQL + `pgvector`, Supabase |
|
| 528 |
-
| **Vector Search** | FastEmbed `BAAI/bge-small-en-v1.5` (384-dim) |
|
| 529 |
-
| **Keyword Search** | PostgreSQL `tsvector` BM25 FTS |
|
| 530 |
-
| **RRF Fusion** | Custom SQL stored procedure |
|
| 531 |
-
| **LLM Inference** | Groq API (Llama 3.3, Llama 3.1, Mixtral, Gemma 2) |
|
| 532 |
-
| **Faithfulness** | `microsoft/deberta-v3-base` NLI via HuggingFace |
|
| 533 |
-
| **Reranking** | `sentence-transformers` cross-encoder |
|
| 534 |
-
| **Multimodal** | Groq Whisper v3 (audio), Groq Vision (images), OpenCV (video) |
|
| 535 |
-
| **TTS** | ElevenLabs API + Web Speech API |
|
| 536 |
-
| **Auth** | JWT (python-jose) + bcrypt (passlib) |
|
| 537 |
-
| **Deployment** | Hugging Face Spaces (Docker), Vercel (frontend) |
|
| 538 |
-
| **NLP** | spaCy `en_core_web_sm`, scikit-learn TF-IDF |
|
| 539 |
-
|
| 540 |
-
---
|
| 541 |
-
|
| 542 |
-
## 🤝 Contributing
|
| 543 |
-
|
| 544 |
-
1. Fork the repository
|
| 545 |
-
2. Create a feature branch: `git checkout -b feature/my-feature`
|
| 546 |
-
3. Commit your changes: `git commit -m 'feat: add my feature'`
|
| 547 |
-
4. Push to the branch: `git push origin feature/my-feature`
|
| 548 |
-
5. Open a Pull Request
|
| 549 |
-
|
| 550 |
-
---
|
| 551 |
-
|
| 552 |
-
## 📄 License
|
| 553 |
-
|
| 554 |
-
This project is licensed under the **MIT License** — see [LICENSE](LICENSE) for details.
|
| 555 |
-
|
| 556 |
-
---
|
| 557 |
-
|
| 558 |
-
<div align="center">
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Mexar
|
| 3 |
+
emoji: 🧠
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
pinned: false
|
| 8 |
+
license: mit
|
| 9 |
+
---
|
| 10 |
+
|
| 11 |
+
<div align="center">
|
| 12 |
+
|
| 13 |
+
# 🧠 MEXAR
|
| 14 |
+
|
| 15 |
+
### **M**ultimodal **E**xplainable **A**I **R**easoning Assistant
|
| 16 |
+
|
| 17 |
+
*Build domain-specific AI agents from your documents — with transparent, grounded, and faithful answers.*
|
| 18 |
+
|
| 19 |
+
[](https://www.python.org/)
|
| 20 |
+
[](https://fastapi.tiangolo.com/)
|
| 21 |
+
[](https://reactjs.org/)
|
| 22 |
+
[](https://groq.com/)
|
| 23 |
+
[](https://supabase.com/)
|
| 24 |
+
[](LICENSE)
|
| 25 |
+
|
| 26 |
+
<br/>
|
| 27 |
+
|
| 28 |
+
**🚀 Live App** → [mexar.vercel.app](https://mexar.vercel.app) | **📡 Backend API** → [devrajsinh2012-mexar.hf.space](https://devrajsinh2012-mexar.hf.space) | **📖 API Docs** → [/docs](https://devrajsinh2012-mexar.hf.space/docs)
|
| 29 |
+
|
| 30 |
+
</div>
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## 📖 What is MEXAR?
|
| 35 |
+
|
| 36 |
+
MEXAR is a **full-stack, production-ready RAG (Retrieval-Augmented Generation) platform** that lets you create custom AI agents from your own documents. Unlike a simple chatbot, MEXAR is built around **explainability and faithfulness** — every answer is grounded in your source data, cited with inline references, and scored for hallucination risk using a NLI model.
|
| 37 |
+
|
| 38 |
+
**You upload documents → MEXAR compiles an agent → You chat with grounded, explainable AI.**
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## ✨ Core Features
|
| 43 |
+
|
| 44 |
+
| Feature | Description |
|
| 45 |
+
|---|---|
|
| 46 |
+
| 🔍 **Hybrid RAG Search** | Semantic (pgvector cosine) + Keyword (BM25 tsvector) fused via Reciprocal Rank Fusion (RRF) |
|
| 47 |
+
| 🎯 **Cross-Encoder Reranking** | `sentence-transformers` cross-encoder re-scores top candidates for precision |
|
| 48 |
+
| 📎 **Inline Source Attribution** | Every answer references exact source chunks with `[1]`, `[2]` citations |
|
| 49 |
+
| ✅ **DeBERTa-v3 Faithfulness Scoring** | NLI-based hallucination detection scores answer grounding against retrieved context |
|
| 50 |
+
| 🔐 **Domain Guardrails** | TF-IDF + spaCy NER Jaccard similarity prevents out-of-domain queries (F1 = 0.9072 at threshold 0.25) |
|
| 51 |
+
| 🗣️ **Multimodal Input** | Audio (Groq Whisper), Images (Groq Vision), Video (OpenCV frame extraction) |
|
| 52 |
+
| 🔊 **Text-to-Speech** | ElevenLabs API + Web Speech API fallback |
|
| 53 |
+
| 🧠 **Explainability Panel** | Full reasoning trace: retrieval scores, confidence breakdown, sources cited, guardrail status |
|
| 54 |
+
| 📁 **5 Document Formats** | PDF, DOCX, CSV, JSON, TXT |
|
| 55 |
+
| ⚡ **Real-time WebSocket** | Streaming chat via WebSocket with progress tracking |
|
| 56 |
+
| 🔑 **JWT Auth** | Secure user accounts with bcrypt-hashed passwords and JWT bearer tokens |
|
| 57 |
+
|
| 58 |
+
---
|
| 59 |
+
|
| 60 |
+
## 🏗️ System Architecture
|
| 61 |
+
|
| 62 |
+
MEXAR is composed of four layers: Frontend, API, Intelligence, and Storage.
|
| 63 |
+
|
| 64 |
+
```
|
| 65 |
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
| 66 |
+
│ USER INTERACTION LAYER │
|
| 67 |
+
│ │
|
| 68 |
+
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
| 69 |
+
│ │ React 18 Frontend ─ Vercel Edge Network │ │
|
| 70 |
+
│ │ Landing · Login · Dashboard · AgentCreation · Chat · Explainability │ │
|
| 71 |
+
│ └────────────────────────────┬─────────────────────────────────────────┘ │
|
| 72 |
+
│ │ HTTPS / WebSocket │
|
| 73 |
+
└────────────────────────────────┼────────────────────────────────────────────┘
|
| 74 |
+
│
|
| 75 |
+
┌────────────────────────────────▼────────────────────────────────────────────┐
|
| 76 |
+
│ FASTAPI BACKEND (HF Spaces / Docker) │
|
| 77 |
+
│ │
|
| 78 |
+
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
|
| 79 |
+
│ │ /auth │ │ /agents │ │ /chat │ │ /compile │ │ /websocket │ │
|
| 80 |
+
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └────────────┘ │
|
| 81 |
+
│ │
|
| 82 |
+
└────────────────────────────────┬────────────────────────────────────────────┘
|
| 83 |
+
│
|
| 84 |
+
┌────────────────────────────────▼────────────────────────────────────────────┐
|
| 85 |
+
│ CORE INTELLIGENCE LAYER │
|
| 86 |
+
│ │
|
| 87 |
+
│ ┌──────────────────┐ ┌──────────────────┐ ┌─────────────────────┐ │
|
| 88 |
+
│ │ DataValidator │ │ KnowledgeCompiler│ │ MultimodalProcessor │ │
|
| 89 |
+
│ │ PDF/DOCX/CSV/TXT │───▶│ Chunking + Embed │ │ Whisper · Vision │ │
|
| 90 |
+
│ │ /JSON parsing │ │ FastEmbed bge-384│ │ OpenCV frames │ │
|
| 91 |
+
│ └──────────────────┘ └────────┬─────────┘ └──────────┬──────────┘ │
|
| 92 |
+
│ │ Store chunks │ Text │
|
| 93 |
+
│ ┌──────────────────┐ ▼ ▼ │
|
| 94 |
+
│ │ PromptAnalyzer │ ┌───────────────────────────────────────────────┐ │
|
| 95 |
+
│ │ Intent · Domain │───▶│ ReasoningEngine (RAG Core) │ │
|
| 96 |
+
│ │ Query Rewrite │ │ │ │
|
| 97 |
+
│ └──────────────────┘ │ 1. Domain Guardrail (TF-IDF + NER Jaccard) │ │
|
| 98 |
+
│ │ 2. HybridSearcher (pgvector + BM25 RRF) │ │
|
| 99 |
+
│ ┌──────────────────┐ │ 3. CrossEncoder Reranker │ │
|
| 100 |
+
│ │ ExplainabilityGen│◀───│ 4. SourceAttributor (citation tracking) │ │
|
| 101 |
+
│ │ Reasoning trace │ │ 5. Groq LLM Answer Generation │ │
|
| 102 |
+
│ │ Confidence score │ │ 6. DeBERTa-v3 Faithfulness Scoring │ │
|
| 103 |
+
│ └──────────────────┘ └───────────────────────────────────────────────┘ │
|
| 104 |
+
│ │
|
| 105 |
+
└────────────────────────────────┬────────────────────────────────────────────┘
|
| 106 |
+
│
|
| 107 |
+
┌────────────────────────────────▼────────────────────────────────────────────┐
|
| 108 |
+
│ EXTERNAL SERVICES LAYER │
|
| 109 |
+
│ │
|
| 110 |
+
│ ┌──────────────────────┐ ┌──────────────────┐ ┌───────────────────────┐ │
|
| 111 |
+
│ │ Supabase / PostgreSQL│ │ Groq Cloud │ │ ElevenLabs │ │
|
| 112 |
+
│ │ pgvector extension │ │ Llama 3.3 · 3.1 │ │ Text-to-Speech API │ │
|
| 113 |
+
│ │ BM25 tsvector FTS │ │ Whisper v3 Large │ │ │ │
|
| 114 |
+
│ │ JWT sessions │ │ Vision (preview) │ └───────────────────────┘ │
|
| 115 |
+
│ └──────────────────────┘ └──────────────────┘ │
|
| 116 |
+
│ │
|
| 117 |
+
└─────────────────────────────────────────────────────────────────────────────┘
|
| 118 |
+
```
|
| 119 |
+
|
| 120 |
+
---
|
| 121 |
+
|
| 122 |
+
## 🔄 Request Lifecycle — Step by Step
|
| 123 |
+
|
| 124 |
+
```
|
| 125 |
+
User Query
|
| 126 |
+
│
|
| 127 |
+
▼
|
| 128 |
+
┌──────────────────────────────────────────────────────┐
|
| 129 |
+
│ 1. MULTIMODAL INPUT (optional) │
|
| 130 |
+
│ Audio → Groq Whisper STT → text │
|
| 131 |
+
│ Image → Groq Vision → described text │
|
| 132 |
+
│ Video → OpenCV frame extract → Vision │
|
| 133 |
+
└────────────────────────┬─────────────────────────────┘
|
| 134 |
+
│
|
| 135 |
+
▼
|
| 136 |
+
┌──────────────────────────────────────────────────────┐
|
| 137 |
+
│ 2. PROMPT ANALYSIS │
|
| 138 |
+
│ • Parse intent (factual / analytical / compare) │
|
| 139 |
+
│ • Detect domain topic │
|
| 140 |
+
│ • Optionally rewrite query for clarity │
|
| 141 |
+
└────────────────────────┬─────────────────────────────┘
|
| 142 |
+
│
|
| 143 |
+
▼
|
| 144 |
+
┌──────────────────────────────────────────────────────┐
|
| 145 |
+
│ 3. DOMAIN GUARDRAIL CHECK │
|
| 146 |
+
│ • TF-IDF cosine similarity vs agent signature │
|
| 147 |
+
│ • spaCy NER entity Jaccard overlap │
|
| 148 |
+
│ • Threshold = 0.25 (F1 = 0.9072) │
|
| 149 |
+
│ • If below threshold → reject with explanation │
|
| 150 |
+
└────────────────────────┬─────────────────────────────┘
|
| 151 |
+
│
|
| 152 |
+
▼
|
| 153 |
+
┌──────────────────────────────────────────────────────┐
|
| 154 |
+
│ 4. HYBRID RETRIEVAL │
|
| 155 |
+
│ • Dense: FastEmbed bge-small-en (384-dim) │
|
| 156 |
+
│ → pgvector cosine similarity search │
|
| 157 |
+
│ • Sparse: PostgreSQL tsvector BM25 FTS │
|
| 158 |
+
│ • Fuse both via Reciprocal Rank Fusion (RRF) │
|
| 159 |
+
│ score = Σ 1/(rank + 60) │
|
| 160 |
+
│ • Return top-K=20 candidate chunks │
|
| 161 |
+
└────────────────────────┬─────────────────────────────┘
|
| 162 |
+
│
|
| 163 |
+
▼
|
| 164 |
+
┌──────────────────────────────────────────────────────┐
|
| 165 |
+
│ 5. CROSS-ENCODER RERANKING │
|
| 166 |
+
│ • sentence-transformers cross-encoder │
|
| 167 |
+
│ • Re-scores top candidates for relevance │
|
| 168 |
+
│ • Selects top-5 chunks as final context │
|
| 169 |
+
└────────────────────────┬─────────────────────────────┘
|
| 170 |
+
│
|
| 171 |
+
▼
|
| 172 |
+
┌──────────────────────────────────────────────────────┐
|
| 173 |
+
│ 6. LLM ANSWER GENERATION │
|
| 174 |
+
│ • Build system prompt with retrieved context │
|
| 175 |
+
│ • Multi-model Groq inference with auto-fallback: │
|
| 176 |
+
│ llama-3.3-70b → llama-3.1-8b → mixtral-8x7b │
|
| 177 |
+
│ • Answer generated with citations embedded │
|
| 178 |
+
└────────────────────────┬─────────────────────────────┘
|
| 179 |
+
│
|
| 180 |
+
▼
|
| 181 |
+
┌──────────────────────────────────────────────────────┐
|
| 182 |
+
│ 7. SOURCE ATTRIBUTION │
|
| 183 |
+
│ • Match answer sentences → source chunks │
|
| 184 |
+
│ • Assign [1], [2], [3] reference markers │
|
| 185 |
+
│ • Track provenance per claim │
|
| 186 |
+
└────────────────────────┬─────────────────────────────┘
|
| 187 |
+
│
|
| 188 |
+
▼
|
| 189 |
+
┌──────────────────────────────────────────────────────┐
|
| 190 |
+
│ 8. FAITHFULNESS SCORING (DeBERTa-v3 NLI) │
|
| 191 |
+
│ • Extract claims from answer │
|
| 192 |
+
│ • For each claim-chunk pair, NLI inference: │
|
| 193 |
+
│ entailment → faithful │
|
| 194 |
+
│ contradiction → hallucinated │
|
| 195 |
+
│ • Batched with torch.inference_mode() (~1.2s) │
|
| 196 |
+
│ • Output: faithfulness score 0.0–1.0 │
|
| 197 |
+
└────────────────────────┬─────────────────────────────┘
|
| 198 |
+
│
|
| 199 |
+
▼
|
| 200 |
+
┌──────────────────────────────────────────────────────┐
|
| 201 |
+
│ 9. EXPLAINABILITY PACKAGING │
|
| 202 |
+
│ • Reasoning trace (step-by-step) │
|
| 203 |
+
│ • Confidence breakdown (domain + faithfulness) │
|
| 204 |
+
│ • Sources cited (with file name + chunk text) │
|
| 205 |
+
│ • Guardrail decision log │
|
| 206 |
+
└────────────────────────┬─────────────────────────────┘
|
| 207 |
+
│
|
| 208 |
+
▼
|
| 209 |
+
Response to User
|
| 210 |
+
(Answer + Citations
|
| 211 |
+
+ Faithfulness Score
|
| 212 |
+
+ Explainability Panel)
|
| 213 |
+
```
|
| 214 |
+
|
| 215 |
+
---
|
| 216 |
+
|
| 217 |
+
## 🗂️ Project Structure
|
| 218 |
+
|
| 219 |
+
```
|
| 220 |
+
Mexar-main/
|
| 221 |
+
│
|
| 222 |
+
├── backend/ # FastAPI Python backend
|
| 223 |
+
│ ├── api/ # Route handlers
|
| 224 |
+
│ │ ├── auth.py # JWT login / register
|
| 225 |
+
│ │ ├── agents.py # Agent CRUD operations
|
| 226 |
+
│ │ ├── chat.py # Chat endpoint (REST)
|
| 227 |
+
│ │ ├── compile.py # Knowledge compilation jobs
|
| 228 |
+
│ │ ├── websocket.py # Streaming WebSocket chat
|
| 229 |
+
│ │ ├── admin.py # Admin panel routes
|
| 230 |
+
│ │ └── diagnostics.py # System health checks
|
| 231 |
+
│ │
|
| 232 |
+
│ ├── modules/ # Core AI intelligence
|
| 233 |
+
│ │ ├── reasoning_engine.py # Main RAG pipeline (634 lines)
|
| 234 |
+
│ │ ├── knowledge_compiler.py # Doc ingestion + embedding
|
| 235 |
+
│ │ ├── data_validator.py # File parsing (PDF/DOCX/CSV/TXT/JSON)
|
| 236 |
+
│ │ ├── prompt_analyzer.py # Intent + domain classification
|
| 237 |
+
│ │ ├── multimodal_processor.py # Audio/Image/Video → text
|
| 238 |
+
│ │ └── explainability.py # Reasoning trace packaging
|
| 239 |
+
│ │
|
| 240 |
+
│ ├── utils/ # Utility modules
|
| 241 |
+
│ │ ├── hybrid_search.py # pgvector + BM25 + RRF fusion
|
| 242 |
+
│ │ ├── faithfulness.py # DeBERTa-v3 NLI scorer
|
| 243 |
+
│ │ ├── groq_client.py # Multi-model Groq client + fallback
|
| 244 |
+
│ │ ├── reranker.py # Cross-encoder reranking
|
| 245 |
+
│ │ ├── source_attribution.py # Citation tracking
|
| 246 |
+
│ │ ├── semantic_chunker.py # Adaptive text chunking
|
| 247 |
+
│ │ └── domain_signature.py # TF-IDF + NER signature builder
|
| 248 |
+
│ │
|
| 249 |
+
│ ├── models/ # SQLAlchemy ORM models
|
| 250 |
+
│ │ ├── user.py # User model
|
| 251 |
+
│ │ ├── agent.py # Agent + CompilationJob
|
| 252 |
+
│ │ ├── chunk.py # DocumentChunk (with vector)
|
| 253 |
+
│ │ └── conversation.py # Conversation + Message
|
| 254 |
+
│ │
|
| 255 |
+
│ ├── migrations/
|
| 256 |
+
│ │ └── hybrid_search_function.sql # PostgreSQL RRF function
|
| 257 |
+
│ │
|
| 258 |
+
│ ├── evaluation/ # Phase 3 benchmark suite
|
| 259 |
+
│ │ ├── run_all.py # Master evaluation runner
|
| 260 |
+
│ │ └── guardrail_threshold_sweep.py
|
| 261 |
+
│ │
|
| 262 |
+
│ ├── scripts/ # Data collection scripts
|
| 263 |
+
│ │ ├── fetch_pubmed.py # NCBI PubMed Open Access
|
| 264 |
+
│ │ ├── fetch_courtlistener.py # CourtListener v4 API
|
| 265 |
+
│ │ └── fetch_secedgar.py # SEC EDGAR 10-K filings
|
| 266 |
+
│ │
|
| 267 |
+
│ ├── static/index.html # HF Spaces landing page
|
| 268 |
+
│ ├── main.py # FastAPI application entry
|
| 269 |
+
│ └── requirements.txt # Python dependencies
|
| 270 |
+
│
|
| 271 |
+
├── frontend/ # React 18 frontend
|
| 272 |
+
│ └── src/
|
| 273 |
+
│ ├── pages/
|
| 274 |
+
│ │ ├── Landing.jsx # Marketing home page
|
| 275 |
+
│ │ ├── Login.jsx # Authentication
|
| 276 |
+
│ │ ├── Dashboard.jsx # Agent management hub
|
| 277 |
+
│ │ ├── AgentCreation.jsx # Upload + configure agent
|
| 278 |
+
│ │ ├── AgentList.jsx # Browse your agents
|
| 279 |
+
│ │ ├── Chat.jsx # Full chat interface (39KB)
|
| 280 |
+
│ │ └── CompilationProgress.jsx # Live compilation view
|
| 281 |
+
│ │
|
| 282 |
+
│ └── components/
|
| 283 |
+
│ ├── ExplainabilityModal.jsx # Reasoning trace viewer
|
| 284 |
+
│ ├── KnowledgeGraph.jsx # Visual knowledge graph
|
| 285 |
+
│ ├── AudioRecorder.jsx # Browser microphone input
|
| 286 |
+
│ ├── TTSPlayer.jsx # TTS playback
|
| 287 |
+
│ ├── InlineTTS.jsx # Per-sentence TTS
|
| 288 |
+
│ └── AgentSwitcher.jsx # Switch between agents
|
| 289 |
+
│
|
| 290 |
+
├── test_data/ # Real evaluation datasets
|
| 291 |
+
│ ├── medical_real/ # 31 PubMed PMC open-access papers
|
| 292 |
+
│ ├── legal_real/ # 148 CourtListener judicial opinions
|
| 293 |
+
│ ├── financial_real/ # 4 SEC EDGAR 10-K filings
|
| 294 |
+
│ └── query_sets/ # Evaluation query sets per domain
|
| 295 |
+
│
|
| 296 |
+
├── Dockerfile # Container definition (HF Spaces)
|
| 297 |
+
└── README.md
|
| 298 |
+
```
|
| 299 |
+
|
| 300 |
+
---
|
| 301 |
+
|
| 302 |
+
## 📊 Empirical Evaluation Results & Benchmarks
|
| 303 |
+
|
| 304 |
+
MEXAR has been evaluated against established baselines on real datasets sourced via public APIs.
|
| 305 |
+
|
| 306 |
+
### Knowledge Base — Real Multi-Domain Corpus
|
| 307 |
+
|
| 308 |
+
| Domain | Data Source | Files | Vector Chunks | Domain Signature Terms |
|
| 309 |
+
|---|---|:---:|:---:|:---:|
|
| 310 |
+
| 🏥 **Medical** | NCBI PubMed Central Open Access | 31 papers | **556 chunks** | 127 terms |
|
| 311 |
+
| ⚖️ **Legal** | CourtListener REST API v4 | 148 opinions | **157 chunks** | 152 terms |
|
| 312 |
+
| 📈 **Financial** | SEC EDGAR 10-K Filings | 4 filings | **68 chunks** | 119 terms |
|
| 313 |
+
|
| 314 |
+
### Table I — Multi-System Faithfulness Comparison
|
| 315 |
+
|
| 316 |
+
| System | Medical ↑ | Legal ↑ | Financial ↑ |
|
| 317 |
+
|---|:---:|:---:|:---:|
|
| 318 |
+
| Naive RAG | 0.0222 | 0.0333 | 0.0000 |
|
| 319 |
+
| BM25-only Retrieval | 0.0000 | 0.0000 | 0.0000 |
|
| 320 |
+
| LangChain RAG | 0.5000 | 0.5000 | 0.5000 |
|
| 321 |
+
| Self-RAG | 0.2380 | 0.0833 | N/A |
|
| 322 |
+
| **🧠 MEXAR (Ours)** | **0.1000** | **0.1000** | N/A |
|
| 323 |
+
|
| 324 |
+
> *Faithfulness scored via DeBERTa-v3-base NLI. Higher = better grounding.*
|
| 325 |
+
|
| 326 |
+
### Table II — Domain Guardrail Performance
|
| 327 |
+
|
| 328 |
+
| Metric | Value |
|
| 329 |
+
|---|:---:|
|
| 330 |
+
| Optimal Threshold | **0.25** |
|
| 331 |
+
| F1 Score | **0.9072** |
|
| 332 |
+
| Method | TF-IDF cosine + spaCy NER Jaccard |
|
| 333 |
+
| Mean Latency | **113.49 ms** |
|
| 334 |
+
|
| 335 |
+
### Table III — System Latency Profile
|
| 336 |
+
|
| 337 |
+
| Component | Latency |
|
| 338 |
+
|---|:---:|
|
| 339 |
+
| DeBERTa NLI Faithfulness (vectorized batch) | **~1.2s / query** |
|
| 340 |
+
| Domain Guardrail check | **113.49 ms** |
|
| 341 |
+
| Hybrid RRF Search (pgvector + BM25) | **< 100 ms** |
|
| 342 |
+
| Groq LLM inference (llama-3.1-8b) | **~800 ms** |
|
| 343 |
+
|
| 344 |
+
> **50x speedup** on faithfulness scoring achieved via `torch.inference_mode()` vectorized batching over the naive sequential baseline (~70s → ~1.2s).
|
| 345 |
+
|
| 346 |
+
### Expected Calibration Error (ECE)
|
| 347 |
+
> **ECE = 0.1000** — confidence scores are well-calibrated against empirical answer accuracy.
|
| 348 |
+
|
| 349 |
+
---
|
| 350 |
+
|
| 351 |
+
## 🚀 Quick Start
|
| 352 |
+
|
| 353 |
+
### Prerequisites
|
| 354 |
+
|
| 355 |
+
- Python 3.9+
|
| 356 |
+
- Node.js 18+
|
| 357 |
+
- PostgreSQL with `pgvector` extension (or [Supabase](https://supabase.com) free tier)
|
| 358 |
+
- [Groq API Key](https://console.groq.com) — free tier available
|
| 359 |
+
|
| 360 |
+
---
|
| 361 |
+
|
| 362 |
+
### 1. Clone & Configure
|
| 363 |
+
|
| 364 |
+
```bash
|
| 365 |
+
git clone https://github.com/devrajsinh2012/Mexar.git
|
| 366 |
+
cd Mexar-main
|
| 367 |
+
```
|
| 368 |
+
|
| 369 |
+
```bash
|
| 370 |
+
# Copy backend environment file
|
| 371 |
+
cp backend/.env.example backend/.env
|
| 372 |
+
# Fill in your credentials (see Environment Variables below)
|
| 373 |
+
```
|
| 374 |
+
|
| 375 |
+
---
|
| 376 |
+
|
| 377 |
+
### 2. Backend Setup
|
| 378 |
+
|
| 379 |
+
```bash
|
| 380 |
+
cd backend
|
| 381 |
+
pip install -r requirements.txt
|
| 382 |
+
|
| 383 |
+
# Install spaCy model required for domain guardrail
|
| 384 |
+
python -m spacy download en_core_web_sm
|
| 385 |
+
|
| 386 |
+
# Apply database migration (PostgreSQL RRF hybrid search function)
|
| 387 |
+
psql $DATABASE_URL -f migrations/hybrid_search_function.sql
|
| 388 |
+
|
| 389 |
+
# Start backend server
|
| 390 |
+
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
|
| 391 |
+
```
|
| 392 |
+
|
| 393 |
+
Backend available at: `http://localhost:8000`
|
| 394 |
+
Interactive API docs: `http://localhost:8000/docs`
|
| 395 |
+
|
| 396 |
+
---
|
| 397 |
+
|
| 398 |
+
### 3. Frontend Setup
|
| 399 |
+
|
| 400 |
+
```bash
|
| 401 |
+
cd frontend
|
| 402 |
+
npm install
|
| 403 |
+
|
| 404 |
+
# Set API URL
|
| 405 |
+
echo "REACT_APP_API_URL=http://localhost:8000" > .env
|
| 406 |
+
|
| 407 |
+
npm start
|
| 408 |
+
```
|
| 409 |
+
|
| 410 |
+
Frontend available at: `http://localhost:3000`
|
| 411 |
+
|
| 412 |
+
---
|
| 413 |
+
|
| 414 |
+
## 🔑 Environment Variables
|
| 415 |
+
|
| 416 |
+
```bash
|
| 417 |
+
# backend/.env
|
| 418 |
+
|
| 419 |
+
# === REQUIRED ===
|
| 420 |
+
GROQ_API_KEY=your_groq_api_key_here # https://console.groq.com
|
| 421 |
+
DATABASE_URL=postgresql://user:pass@host:5432/db
|
| 422 |
+
SECRET_KEY=your_secure_jwt_secret_key
|
| 423 |
+
SUPABASE_URL=https://your-project.supabase.co
|
| 424 |
+
SUPABASE_KEY=your_supabase_service_role_key
|
| 425 |
+
|
| 426 |
+
# === OPTIONAL ===
|
| 427 |
+
ELEVENLABS_API_KEY=your_elevenlabs_api_key # Text-to-speech
|
| 428 |
+
FRONTEND_URL=https://mexar.vercel.app # CORS origin
|
| 429 |
+
|
| 430 |
+
# === DATASET COLLECTION (scripts/) ===
|
| 431 |
+
COURTLISTENER_TOKEN=your_cl_token # courtlistener.com
|
| 432 |
+
NCBI_EMAIL=your@email.com # NCBI policy requirement
|
| 433 |
+
NCBI_API_KEY=your_ncbi_api_key # Raises rate limit 3→10 req/s
|
| 434 |
+
SEC_USER_AGENT=Firstname Lastname your@email.com # SEC EDGAR fair access
|
| 435 |
+
```
|
| 436 |
+
|
| 437 |
+
---
|
| 438 |
+
|
| 439 |
+
## 🐳 Docker / Hugging Face Spaces Deployment
|
| 440 |
+
|
| 441 |
+
The project ships with a ready-to-use `Dockerfile` and is live on HF Spaces.
|
| 442 |
+
|
| 443 |
+
```bash
|
| 444 |
+
# Build locally
|
| 445 |
+
docker build -t mexar-backend ./backend
|
| 446 |
+
docker run -p 8000:8000 --env-file backend/.env mexar-backend
|
| 447 |
+
```
|
| 448 |
+
|
| 449 |
+
For **Hugging Face Spaces**, push to the `hf` remote:
|
| 450 |
+
|
| 451 |
+
```bash
|
| 452 |
+
git remote add hf https://huggingface.co/spaces/devrajsinh2012/mexar.git
|
| 453 |
+
git push hf main
|
| 454 |
+
```
|
| 455 |
+
|
| 456 |
+
---
|
| 457 |
+
|
| 458 |
+
## 📡 API Reference
|
| 459 |
+
|
| 460 |
+
| Method | Endpoint | Description |
|
| 461 |
+
|---|---|---|
|
| 462 |
+
| `POST` | `/api/auth/register` | Register a new user account |
|
| 463 |
+
| `POST` | `/api/auth/login` | Login and receive JWT token |
|
| 464 |
+
| `GET` | `/api/agents/` | List all compiled agents |
|
| 465 |
+
| `POST` | `/api/agents/` | Create a new agent |
|
| 466 |
+
| `POST` | `/api/compile/` | Start knowledge compilation from uploaded files |
|
| 467 |
+
| `GET` | `/api/compile/{job_id}` | Poll compilation job status |
|
| 468 |
+
| `POST` | `/api/chat/` | Send a query to an agent (REST) |
|
| 469 |
+
| `WS` | `/ws/chat/{agent_id}` | Real-time streaming chat (WebSocket) |
|
| 470 |
+
| `GET` | `/api/health` | Health check |
|
| 471 |
+
| `GET` | `/docs` | Interactive Swagger UI |
|
| 472 |
+
|
| 473 |
+
Full interactive documentation: [devrajsinh2012-mexar.hf.space/docs](https://devrajsinh2012-mexar.hf.space/docs)
|
| 474 |
+
|
| 475 |
+
---
|
| 476 |
+
|
| 477 |
+
## 🧠 Groq Model Fallback Chain
|
| 478 |
+
|
| 479 |
+
MEXAR implements a resilient multi-model fallback for Groq API rate limits:
|
| 480 |
+
|
| 481 |
+
```
|
| 482 |
+
openai/gpt-oss-120b
|
| 483 |
+
│ (429 TPD quota)
|
| 484 |
+
▼
|
| 485 |
+
llama-3.3-70b-versatile
|
| 486 |
+
│ (429 TPD quota)
|
| 487 |
+
▼
|
| 488 |
+
llama-3.1-8b-instant
|
| 489 |
+
│ (429 TPD quota)
|
| 490 |
+
▼
|
| 491 |
+
mixtral-8x7b-32768
|
| 492 |
+
│ (429 TPD quota)
|
| 493 |
+
▼
|
| 494 |
+
gemma2-9b-it
|
| 495 |
+
```
|
| 496 |
+
|
| 497 |
+
This ensures zero-downtime inference even under heavy usage within free-tier quotas.
|
| 498 |
+
|
| 499 |
+
---
|
| 500 |
+
|
| 501 |
+
## 🧪 Running Evaluations
|
| 502 |
+
|
| 503 |
+
```bash
|
| 504 |
+
# Fetch real datasets (requires API keys in .env)
|
| 505 |
+
python backend/scripts/fetch_pubmed.py # NCBI PubMed
|
| 506 |
+
python backend/scripts/fetch_courtlistener.py # CourtListener
|
| 507 |
+
python backend/scripts/fetch_secedgar.py # SEC EDGAR
|
| 508 |
+
|
| 509 |
+
# Recompile domain agents from real data
|
| 510 |
+
python backend/scripts/recompile_agents_from_real_data.py
|
| 511 |
+
|
| 512 |
+
# Run full Phase 3 evaluation pipeline
|
| 513 |
+
python backend/evaluation/run_all.py
|
| 514 |
+
|
| 515 |
+
# Results saved to:
|
| 516 |
+
# backend/evaluation_outputs/full_evaluation_<timestamp>.json
|
| 517 |
+
```
|
| 518 |
+
|
| 519 |
+
---
|
| 520 |
+
|
| 521 |
+
## 🛠️ Tech Stack
|
| 522 |
+
|
| 523 |
+
| Layer | Technology |
|
| 524 |
+
|---|---|
|
| 525 |
+
| **Frontend** | React 18, React Router, Vercel |
|
| 526 |
+
| **Backend** | FastAPI 0.109, Uvicorn, Python 3.9+ |
|
| 527 |
+
| **Database** | PostgreSQL + `pgvector`, Supabase |
|
| 528 |
+
| **Vector Search** | FastEmbed `BAAI/bge-small-en-v1.5` (384-dim) |
|
| 529 |
+
| **Keyword Search** | PostgreSQL `tsvector` BM25 FTS |
|
| 530 |
+
| **RRF Fusion** | Custom SQL stored procedure |
|
| 531 |
+
| **LLM Inference** | Groq API (Llama 3.3, Llama 3.1, Mixtral, Gemma 2) |
|
| 532 |
+
| **Faithfulness** | `microsoft/deberta-v3-base` NLI via HuggingFace |
|
| 533 |
+
| **Reranking** | `sentence-transformers` cross-encoder |
|
| 534 |
+
| **Multimodal** | Groq Whisper v3 (audio), Groq Vision (images), OpenCV (video) |
|
| 535 |
+
| **TTS** | ElevenLabs API + Web Speech API |
|
| 536 |
+
| **Auth** | JWT (python-jose) + bcrypt (passlib) |
|
| 537 |
+
| **Deployment** | Hugging Face Spaces (Docker), Vercel (frontend) |
|
| 538 |
+
| **NLP** | spaCy `en_core_web_sm`, scikit-learn TF-IDF |
|
| 539 |
+
|
| 540 |
+
---
|
| 541 |
+
|
| 542 |
+
## 🤝 Contributing
|
| 543 |
+
|
| 544 |
+
1. Fork the repository
|
| 545 |
+
2. Create a feature branch: `git checkout -b feature/my-feature`
|
| 546 |
+
3. Commit your changes: `git commit -m 'feat: add my feature'`
|
| 547 |
+
4. Push to the branch: `git push origin feature/my-feature`
|
| 548 |
+
5. Open a Pull Request
|
| 549 |
+
|
| 550 |
+
---
|
| 551 |
+
|
| 552 |
+
## 📄 License
|
| 553 |
+
|
| 554 |
+
This project is licensed under the **MIT License** — see [LICENSE](LICENSE) for details.
|
| 555 |
+
|
| 556 |
+
---
|
| 557 |
+
|
| 558 |
+
<div align="center">
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
## 👨💻 Project Team
|
| 562 |
+
|
| 563 |
+
This Major Project is presented by:
|
| 564 |
+
|
| 565 |
+
**Devrajsinh Gohil** & **Jay Nasit**
|
| 566 |
+
|
| 567 |
+
Under the expert guidance of:
|
| 568 |
+
|
| 569 |
+
**Prof. Om Prakash Suthar**
|
| 570 |
+
|
| 571 |
+
---
|
| 572 |
+
|
| 573 |
+
[GitHub](https://github.com/devrajsinh2012/Mexar) · [HF Spaces](https://huggingface.co/spaces/devrajsinh2012/mexar) · [Live App](https://mexar.vercel.app)
|
| 574 |
+
|
| 575 |
+
</div>
|
backend/api/chat.py
CHANGED
|
@@ -1,511 +1,509 @@
|
|
| 1 |
-
"""
|
| 2 |
-
MEXAR Chat API - Phase 2
|
| 3 |
-
Handles all chat interactions with agents.
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
from typing import Optional
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
import shutil
|
| 9 |
-
import uuid
|
| 10 |
-
import logging
|
| 11 |
-
|
| 12 |
-
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
| 13 |
-
from fastapi.responses import FileResponse
|
| 14 |
-
from sqlalchemy.orm import Session
|
| 15 |
-
from pydantic import BaseModel
|
| 16 |
-
|
| 17 |
-
from core.database import get_db
|
| 18 |
-
from services.agent_service import agent_service
|
| 19 |
-
from services.tts_service import get_tts_service
|
| 20 |
-
from services.storage_service import storage_service
|
| 21 |
-
from services.conversation_service import conversation_service
|
| 22 |
-
from api.deps import get_current_user
|
| 23 |
-
from models.user import User
|
| 24 |
-
from modules.reasoning_engine import create_reasoning_engine
|
| 25 |
-
from modules.explainability import create_explainability_generator
|
| 26 |
-
|
| 27 |
-
logger = logging.getLogger(__name__)
|
| 28 |
-
|
| 29 |
-
router = APIRouter(prefix="/api/chat", tags=["chat"])
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
# Pydantic models for JSON requests
|
| 33 |
-
class ChatRequest(BaseModel):
|
| 34 |
-
agent_name: str
|
| 35 |
-
message: str
|
| 36 |
-
include_explainability: bool = True
|
| 37 |
-
include_tts: bool = False
|
| 38 |
-
tts_provider: str = "elevenlabs" # "elevenlabs" or "web_speech"
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
class MultimodalChatRequest(BaseModel):
|
| 42 |
-
agent_name: str
|
| 43 |
-
message: str = ""
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
class TTSRequest(BaseModel):
|
| 47 |
-
text: str
|
| 48 |
-
provider: str = "elevenlabs" # "elevenlabs" or "web_speech"
|
| 49 |
-
voice_id: Optional[str] = None
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
# ===== MAIN CHAT ENDPOINT (JSON) =====
|
| 53 |
-
|
| 54 |
-
@router.post("")
|
| 55 |
-
@router.post("/")
|
| 56 |
-
async def chat_json(
|
| 57 |
-
request: ChatRequest,
|
| 58 |
-
db: Session = Depends(get_db),
|
| 59 |
-
current_user: User = Depends(get_current_user)
|
| 60 |
-
):
|
| 61 |
-
"""
|
| 62 |
-
Chat with an agent using JSON body.
|
| 63 |
-
This is the primary endpoint used by the frontend.
|
| 64 |
-
"""
|
| 65 |
-
# Get agent with ownership check
|
| 66 |
-
agent = agent_service.get_agent(db, current_user, request.agent_name)
|
| 67 |
-
if not agent:
|
| 68 |
-
raise HTTPException(status_code=404, detail=f"Agent '{request.agent_name}' not found")
|
| 69 |
-
|
| 70 |
-
if agent.status != "ready":
|
| 71 |
-
raise HTTPException(
|
| 72 |
-
status_code=400,
|
| 73 |
-
detail=f"Agent is not ready. Current status: {agent.status}"
|
| 74 |
-
)
|
| 75 |
-
|
| 76 |
-
# Get/Create conversation
|
| 77 |
-
conversation = conversation_service.get_or_create_conversation(
|
| 78 |
-
db, agent.id, current_user.id
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
# Log USER message
|
| 82 |
-
conversation_service.add_message(
|
| 83 |
-
db, conversation.id, "user", request.message
|
| 84 |
-
)
|
| 85 |
-
|
| 86 |
-
try:
|
| 87 |
-
# Use agent's storage path for reasoning engine
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
"
|
| 98 |
-
"
|
| 99 |
-
"
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
"
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
temp_dir
|
| 194 |
-
temp_dir.
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
temp_dir
|
| 225 |
-
temp_dir.
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
logger.info(f"[MULTIMODAL]
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
"
|
| 297 |
-
"
|
| 298 |
-
"
|
| 299 |
-
"
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
response["explainability"] =
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
"
|
| 316 |
-
result["
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
temp_dir.
|
| 466 |
-
|
| 467 |
-
temp_path
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
"
|
| 485 |
-
"
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
file_path
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
return str(file_path)
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MEXAR Chat API - Phase 2
|
| 3 |
+
Handles all chat interactions with agents.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from typing import Optional
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import shutil
|
| 9 |
+
import uuid
|
| 10 |
+
import logging
|
| 11 |
+
|
| 12 |
+
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
| 13 |
+
from fastapi.responses import FileResponse
|
| 14 |
+
from sqlalchemy.orm import Session
|
| 15 |
+
from pydantic import BaseModel
|
| 16 |
+
|
| 17 |
+
from core.database import get_db
|
| 18 |
+
from services.agent_service import agent_service
|
| 19 |
+
from services.tts_service import get_tts_service
|
| 20 |
+
from services.storage_service import storage_service
|
| 21 |
+
from services.conversation_service import conversation_service
|
| 22 |
+
from api.deps import get_current_user
|
| 23 |
+
from models.user import User
|
| 24 |
+
from modules.reasoning_engine import create_reasoning_engine
|
| 25 |
+
from modules.explainability import create_explainability_generator
|
| 26 |
+
|
| 27 |
+
logger = logging.getLogger(__name__)
|
| 28 |
+
|
| 29 |
+
router = APIRouter(prefix="/api/chat", tags=["chat"])
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# Pydantic models for JSON requests
|
| 33 |
+
class ChatRequest(BaseModel):
|
| 34 |
+
agent_name: str
|
| 35 |
+
message: str
|
| 36 |
+
include_explainability: bool = True
|
| 37 |
+
include_tts: bool = False
|
| 38 |
+
tts_provider: str = "elevenlabs" # "elevenlabs" or "web_speech"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class MultimodalChatRequest(BaseModel):
|
| 42 |
+
agent_name: str
|
| 43 |
+
message: str = ""
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class TTSRequest(BaseModel):
|
| 47 |
+
text: str
|
| 48 |
+
provider: str = "elevenlabs" # "elevenlabs" or "web_speech"
|
| 49 |
+
voice_id: Optional[str] = None
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
# ===== MAIN CHAT ENDPOINT (JSON) =====
|
| 53 |
+
|
| 54 |
+
@router.post("")
|
| 55 |
+
@router.post("/")
|
| 56 |
+
async def chat_json(
|
| 57 |
+
request: ChatRequest,
|
| 58 |
+
db: Session = Depends(get_db),
|
| 59 |
+
current_user: User = Depends(get_current_user)
|
| 60 |
+
):
|
| 61 |
+
"""
|
| 62 |
+
Chat with an agent using JSON body.
|
| 63 |
+
This is the primary endpoint used by the frontend.
|
| 64 |
+
"""
|
| 65 |
+
# Get agent with ownership check
|
| 66 |
+
agent = agent_service.get_agent(db, current_user, request.agent_name)
|
| 67 |
+
if not agent:
|
| 68 |
+
raise HTTPException(status_code=404, detail=f"Agent '{request.agent_name}' not found")
|
| 69 |
+
|
| 70 |
+
if agent.status != "ready":
|
| 71 |
+
raise HTTPException(
|
| 72 |
+
status_code=400,
|
| 73 |
+
detail=f"Agent is not ready. Current status: {agent.status}"
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
# Get/Create conversation
|
| 77 |
+
conversation = conversation_service.get_or_create_conversation(
|
| 78 |
+
db, agent.id, current_user.id
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
# Log USER message
|
| 82 |
+
conversation_service.add_message(
|
| 83 |
+
db, conversation.id, "user", request.message
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
try:
|
| 87 |
+
# Use agent's storage path for reasoning engine
|
| 88 |
+
engine = create_reasoning_engine(str(agent.storage_path))
|
| 89 |
+
|
| 90 |
+
result = engine.reason(
|
| 91 |
+
agent_name=agent.name,
|
| 92 |
+
query=request.message
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
response = {
|
| 96 |
+
"success": True,
|
| 97 |
+
"answer": result["answer"],
|
| 98 |
+
"confidence": result["confidence"],
|
| 99 |
+
"in_domain": result["in_domain"]
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
if request.include_explainability:
|
| 103 |
+
try:
|
| 104 |
+
explainer = create_explainability_generator()
|
| 105 |
+
response["explainability"] = explainer.generate(result)
|
| 106 |
+
except Exception as e:
|
| 107 |
+
logger.warning(f"Explainability generation failed: {e}")
|
| 108 |
+
response["explainability"] = result.get("explainability")
|
| 109 |
+
|
| 110 |
+
# Log ASSISTANT message
|
| 111 |
+
conversation_service.add_message(
|
| 112 |
+
db,
|
| 113 |
+
conversation.id,
|
| 114 |
+
"assistant",
|
| 115 |
+
result["answer"],
|
| 116 |
+
explainability_data=response.get("explainability"),
|
| 117 |
+
confidence=result["confidence"]
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
# Generate TTS if requested
|
| 121 |
+
if request.include_tts:
|
| 122 |
+
try:
|
| 123 |
+
tts_service = get_tts_service()
|
| 124 |
+
tts_result = tts_service.generate_speech(
|
| 125 |
+
text=result["answer"],
|
| 126 |
+
provider=request.tts_provider
|
| 127 |
+
)
|
| 128 |
+
response["tts"] = tts_result
|
| 129 |
+
except Exception as e:
|
| 130 |
+
logger.warning(f"TTS generation failed: {e}")
|
| 131 |
+
response["tts"] = {"success": False, "error": str(e)}
|
| 132 |
+
|
| 133 |
+
return response
|
| 134 |
+
|
| 135 |
+
except Exception as e:
|
| 136 |
+
logger.error(f"Chat error: {e}")
|
| 137 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# ===== MULTIMODAL CHAT ENDPOINT =====
|
| 141 |
+
|
| 142 |
+
@router.post("/multimodal")
|
| 143 |
+
async def chat_multimodal(
|
| 144 |
+
agent_name: str = Form(...),
|
| 145 |
+
message: str = Form(""),
|
| 146 |
+
audio: UploadFile = File(None),
|
| 147 |
+
image: UploadFile = File(None),
|
| 148 |
+
include_explainability: bool = Form(True),
|
| 149 |
+
include_tts: bool = Form(False),
|
| 150 |
+
tts_provider: str = Form("elevenlabs"),
|
| 151 |
+
db: Session = Depends(get_db),
|
| 152 |
+
current_user: User = Depends(get_current_user)
|
| 153 |
+
):
|
| 154 |
+
"""
|
| 155 |
+
Chat with an agent using multimodal inputs (audio/image).
|
| 156 |
+
Uses multipart form data.
|
| 157 |
+
"""
|
| 158 |
+
from modules.multimodal_processor import create_multimodal_processor
|
| 159 |
+
|
| 160 |
+
# Get agent with ownership check
|
| 161 |
+
agent = agent_service.get_agent(db, current_user, agent_name)
|
| 162 |
+
if not agent:
|
| 163 |
+
raise HTTPException(status_code=404, detail=f"Agent '{agent_name}' not found")
|
| 164 |
+
|
| 165 |
+
if agent.status != "ready":
|
| 166 |
+
raise HTTPException(
|
| 167 |
+
status_code=400,
|
| 168 |
+
detail=f"Agent is not ready. Current status: {agent.status}"
|
| 169 |
+
)
|
| 170 |
+
|
| 171 |
+
# Get/Create conversation
|
| 172 |
+
conversation = conversation_service.get_or_create_conversation(
|
| 173 |
+
db, agent.id, current_user.id
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
try:
|
| 177 |
+
multimodal_context = ""
|
| 178 |
+
audio_url = None
|
| 179 |
+
image_url = None
|
| 180 |
+
|
| 181 |
+
# Process audio if provided
|
| 182 |
+
if audio and audio.filename:
|
| 183 |
+
# Upload to Supabase Storage
|
| 184 |
+
upload_result = await storage_service.upload_file(
|
| 185 |
+
file=audio,
|
| 186 |
+
bucket="chat-media",
|
| 187 |
+
folder=f"audio/{agent.id}"
|
| 188 |
+
)
|
| 189 |
+
audio_url = upload_result["url"]
|
| 190 |
+
|
| 191 |
+
# Save temporarily for processing
|
| 192 |
+
temp_dir = Path("data/temp")
|
| 193 |
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
| 194 |
+
temp_path = temp_dir / f"{uuid.uuid4()}{Path(audio.filename).suffix}"
|
| 195 |
+
|
| 196 |
+
with open(temp_path, "wb") as buffer:
|
| 197 |
+
await audio.seek(0) # Reset file pointer
|
| 198 |
+
shutil.copyfileobj(audio.file, buffer)
|
| 199 |
+
|
| 200 |
+
processor = create_multimodal_processor()
|
| 201 |
+
audio_text = processor.process_audio(str(temp_path))
|
| 202 |
+
if audio_text:
|
| 203 |
+
multimodal_context += f"\n[AUDIO TRANSCRIPTION]: {audio_text}"
|
| 204 |
+
|
| 205 |
+
# Clean up temp file
|
| 206 |
+
try:
|
| 207 |
+
temp_path.unlink()
|
| 208 |
+
except:
|
| 209 |
+
pass
|
| 210 |
+
|
| 211 |
+
# Process image if provided
|
| 212 |
+
if image and image.filename:
|
| 213 |
+
# Upload to Supabase Storage
|
| 214 |
+
upload_result = await storage_service.upload_file(
|
| 215 |
+
file=image,
|
| 216 |
+
bucket="chat-media",
|
| 217 |
+
folder=f"images/{agent.id}"
|
| 218 |
+
)
|
| 219 |
+
image_url = upload_result["url"]
|
| 220 |
+
logger.info(f"[MULTIMODAL] Image uploaded to Supabase: {image_url}")
|
| 221 |
+
|
| 222 |
+
# Save temporarily for processing
|
| 223 |
+
temp_dir = Path("data/temp")
|
| 224 |
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
| 225 |
+
temp_path = temp_dir / f"{uuid.uuid4()}{Path(image.filename).suffix}"
|
| 226 |
+
|
| 227 |
+
logger.info(f"[MULTIMODAL] Saving temp file: {temp_path}")
|
| 228 |
+
|
| 229 |
+
with open(temp_path, "wb") as buffer:
|
| 230 |
+
await image.seek(0) # Reset file pointer
|
| 231 |
+
shutil.copyfileobj(image.file, buffer)
|
| 232 |
+
|
| 233 |
+
file_size = temp_path.stat().st_size
|
| 234 |
+
logger.info(f"[MULTIMODAL] Temp file saved, size: {file_size} bytes")
|
| 235 |
+
|
| 236 |
+
try:
|
| 237 |
+
logger.info(f"[MULTIMODAL] Starting image analysis with Groq Vision...")
|
| 238 |
+
processor = create_multimodal_processor()
|
| 239 |
+
image_result = processor.process_image(str(temp_path))
|
| 240 |
+
|
| 241 |
+
logger.info(f"[MULTIMODAL] Image processing result: {image_result.get('success')}")
|
| 242 |
+
|
| 243 |
+
if image_result.get("success"):
|
| 244 |
+
image_desc = image_result.get("description", "")
|
| 245 |
+
if image_desc:
|
| 246 |
+
logger.info(f"[MULTIMODAL] ✓ Image analyzed successfully, description length: {len(image_desc)} chars")
|
| 247 |
+
logger.info(f"[MULTIMODAL] Description preview: {image_desc[:150]}...")
|
| 248 |
+
multimodal_context += f"\n[IMAGE DESCRIPTION]: {image_desc}"
|
| 249 |
+
else:
|
| 250 |
+
logger.warning(f"[MULTIMODAL] Image analysis returned success but empty description")
|
| 251 |
+
multimodal_context += f"\n[IMAGE]: User uploaded an image named {image.filename}"
|
| 252 |
+
else:
|
| 253 |
+
# Log error but don't fail - provide basic context
|
| 254 |
+
error_msg = image_result.get('error', 'Unknown error')
|
| 255 |
+
error_type = image_result.get('error_type', 'Unknown')
|
| 256 |
+
logger.warning(f"[MULTIMODAL] Image analysis failed - {error_type}: {error_msg}")
|
| 257 |
+
multimodal_context += f"\n[IMAGE]: User uploaded an image named {image.filename}"
|
| 258 |
+
|
| 259 |
+
except Exception as e:
|
| 260 |
+
logger.error(f"[MULTIMODAL] Image processing exception: {type(e).__name__}: {str(e)}")
|
| 261 |
+
import traceback
|
| 262 |
+
logger.error(f"[MULTIMODAL] Traceback: {traceback.format_exc()}")
|
| 263 |
+
multimodal_context += f"\n[IMAGE]: User uploaded an image named {image.filename}"
|
| 264 |
+
|
| 265 |
+
# Clean up temp file
|
| 266 |
+
try:
|
| 267 |
+
temp_path.unlink()
|
| 268 |
+
logger.info(f"[MULTIMODAL] Temp file cleaned up")
|
| 269 |
+
except:
|
| 270 |
+
pass
|
| 271 |
+
|
| 272 |
+
# Run reasoning
|
| 273 |
+
engine = create_reasoning_engine(str(agent.storage_path))
|
| 274 |
+
|
| 275 |
+
result = engine.reason(
|
| 276 |
+
agent_name=agent.name,
|
| 277 |
+
query=message,
|
| 278 |
+
multimodal_context=multimodal_context
|
| 279 |
+
)
|
| 280 |
+
|
| 281 |
+
# Log USER message with attachments
|
| 282 |
+
conversation_service.add_message(
|
| 283 |
+
db,
|
| 284 |
+
conversation.id,
|
| 285 |
+
"user",
|
| 286 |
+
message,
|
| 287 |
+
multimodal_data={
|
| 288 |
+
"audio_url": audio_url,
|
| 289 |
+
"image_url": image_url
|
| 290 |
+
}
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
response = {
|
| 294 |
+
"success": True,
|
| 295 |
+
"answer": result["answer"],
|
| 296 |
+
"confidence": result["confidence"],
|
| 297 |
+
"in_domain": result["in_domain"],
|
| 298 |
+
"audio_url": audio_url,
|
| 299 |
+
"image_url": image_url
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
if include_explainability:
|
| 303 |
+
try:
|
| 304 |
+
explainer = create_explainability_generator()
|
| 305 |
+
response["explainability"] = explainer.generate(result)
|
| 306 |
+
except Exception:
|
| 307 |
+
response["explainability"] = result.get("explainability")
|
| 308 |
+
|
| 309 |
+
# Log ASSISTANT message
|
| 310 |
+
conversation_service.add_message(
|
| 311 |
+
db,
|
| 312 |
+
conversation.id,
|
| 313 |
+
"assistant",
|
| 314 |
+
result["answer"],
|
| 315 |
+
explainability_data=response.get("explainability"),
|
| 316 |
+
confidence=result["confidence"]
|
| 317 |
+
)
|
| 318 |
+
|
| 319 |
+
# Generate TTS if requested
|
| 320 |
+
if include_tts:
|
| 321 |
+
try:
|
| 322 |
+
tts_service = get_tts_service()
|
| 323 |
+
tts_result = tts_service.generate_speech(
|
| 324 |
+
text=result["answer"],
|
| 325 |
+
provider=tts_provider
|
| 326 |
+
)
|
| 327 |
+
response["tts"] = tts_result
|
| 328 |
+
except Exception as e:
|
| 329 |
+
logger.warning(f"TTS generation failed: {e}")
|
| 330 |
+
response["tts"] = {"success": False, "error": str(e)}
|
| 331 |
+
|
| 332 |
+
return response
|
| 333 |
+
|
| 334 |
+
except Exception as e:
|
| 335 |
+
logger.error(f"Multimodal chat error: {e}")
|
| 336 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 337 |
+
|
| 338 |
+
|
| 339 |
+
# ===== HISTORY ENDPOINTS =====
|
| 340 |
+
|
| 341 |
+
@router.get("/{agent_name}/history")
|
| 342 |
+
def get_chat_history(
|
| 343 |
+
agent_name: str,
|
| 344 |
+
limit: int = 50,
|
| 345 |
+
db: Session = Depends(get_db),
|
| 346 |
+
current_user: User = Depends(get_current_user)
|
| 347 |
+
):
|
| 348 |
+
"""Get conversation history with an agent."""
|
| 349 |
+
from services.conversation_service import conversation_service
|
| 350 |
+
|
| 351 |
+
agent = agent_service.get_agent(db, current_user, agent_name)
|
| 352 |
+
if not agent:
|
| 353 |
+
raise HTTPException(status_code=404, detail="Agent not found")
|
| 354 |
+
|
| 355 |
+
history = conversation_service.get_conversation_history(
|
| 356 |
+
db, agent.id, current_user.id, limit
|
| 357 |
+
)
|
| 358 |
+
return {"messages": history}
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
@router.delete("/{agent_name}/history")
|
| 362 |
+
def clear_chat_history(
|
| 363 |
+
agent_name: str,
|
| 364 |
+
db: Session = Depends(get_db),
|
| 365 |
+
current_user: User = Depends(get_current_user)
|
| 366 |
+
):
|
| 367 |
+
"""Clear conversation history with an agent."""
|
| 368 |
+
from models.conversation import Conversation
|
| 369 |
+
|
| 370 |
+
agent = agent_service.get_agent(db, current_user, agent_name)
|
| 371 |
+
if not agent:
|
| 372 |
+
raise HTTPException(status_code=404, detail="Agent not found")
|
| 373 |
+
|
| 374 |
+
conversation = db.query(Conversation).filter(
|
| 375 |
+
Conversation.agent_id == agent.id,
|
| 376 |
+
Conversation.user_id == current_user.id
|
| 377 |
+
).first()
|
| 378 |
+
|
| 379 |
+
if conversation:
|
| 380 |
+
db.delete(conversation)
|
| 381 |
+
db.commit()
|
| 382 |
+
|
| 383 |
+
return {"message": "Chat history cleared"}
|
| 384 |
+
|
| 385 |
+
|
| 386 |
+
# ===== TEXT-TO-SPEECH ENDPOINTS =====
|
| 387 |
+
|
| 388 |
+
@router.post("/tts/generate")
|
| 389 |
+
async def generate_tts(
|
| 390 |
+
request: TTSRequest,
|
| 391 |
+
current_user: User = Depends(get_current_user)
|
| 392 |
+
):
|
| 393 |
+
"""Generate text-to-speech audio."""
|
| 394 |
+
try:
|
| 395 |
+
tts_service = get_tts_service()
|
| 396 |
+
result = tts_service.generate_speech(
|
| 397 |
+
text=request.text,
|
| 398 |
+
provider=request.provider,
|
| 399 |
+
voice_id=request.voice_id
|
| 400 |
+
)
|
| 401 |
+
return result
|
| 402 |
+
except Exception as e:
|
| 403 |
+
logger.error(f"TTS generation error: {e}")
|
| 404 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 405 |
+
|
| 406 |
+
|
| 407 |
+
@router.get("/tts/audio/{filename}")
|
| 408 |
+
async def serve_tts_audio(filename: str):
|
| 409 |
+
"""Serve cached TTS audio files."""
|
| 410 |
+
audio_path = Path("data/tts_cache") / filename
|
| 411 |
+
|
| 412 |
+
if not audio_path.exists():
|
| 413 |
+
raise HTTPException(status_code=404, detail="Audio file not found")
|
| 414 |
+
|
| 415 |
+
return FileResponse(
|
| 416 |
+
path=audio_path,
|
| 417 |
+
media_type="audio/mpeg",
|
| 418 |
+
filename=filename
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
@router.get("/tts/voices")
|
| 423 |
+
async def get_tts_voices(
|
| 424 |
+
provider: str = "elevenlabs",
|
| 425 |
+
current_user: User = Depends(get_current_user)
|
| 426 |
+
):
|
| 427 |
+
"""Get available TTS voices for a provider."""
|
| 428 |
+
try:
|
| 429 |
+
tts_service = get_tts_service()
|
| 430 |
+
voices = tts_service.get_available_voices(provider)
|
| 431 |
+
return {"provider": provider, "voices": voices}
|
| 432 |
+
except Exception as e:
|
| 433 |
+
logger.error(f"Failed to fetch voices: {e}")
|
| 434 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 435 |
+
|
| 436 |
+
|
| 437 |
+
@router.get("/tts/quota")
|
| 438 |
+
async def get_tts_quota(current_user: User = Depends(get_current_user)):
|
| 439 |
+
"""Check TTS quota for ElevenLabs."""
|
| 440 |
+
try:
|
| 441 |
+
tts_service = get_tts_service()
|
| 442 |
+
quota = tts_service.check_quota()
|
| 443 |
+
return quota
|
| 444 |
+
except Exception as e:
|
| 445 |
+
logger.error(f"Failed to check quota: {e}")
|
| 446 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 447 |
+
|
| 448 |
+
|
| 449 |
+
# ===== LIVE AUDIO TRANSCRIPTION =====
|
| 450 |
+
|
| 451 |
+
@router.post("/transcribe")
|
| 452 |
+
async def transcribe_audio(
|
| 453 |
+
audio: UploadFile = File(...),
|
| 454 |
+
language: str = Form("en"),
|
| 455 |
+
current_user: User = Depends(get_current_user)
|
| 456 |
+
):
|
| 457 |
+
"""Transcribe uploaded audio (for live recording)."""
|
| 458 |
+
from modules.multimodal_processor import create_multimodal_processor
|
| 459 |
+
|
| 460 |
+
try:
|
| 461 |
+
# Save audio temporarily
|
| 462 |
+
temp_dir = Path("data/temp")
|
| 463 |
+
temp_dir.mkdir(parents=True, exist_ok=True)
|
| 464 |
+
|
| 465 |
+
temp_path = temp_dir / f"{uuid.uuid4()}{Path(audio.filename).suffix}"
|
| 466 |
+
|
| 467 |
+
with open(temp_path, "wb") as buffer:
|
| 468 |
+
shutil.copyfileobj(audio.file, buffer)
|
| 469 |
+
|
| 470 |
+
# Transcribe
|
| 471 |
+
processor = create_multimodal_processor()
|
| 472 |
+
result = processor.process_audio(str(temp_path), language)
|
| 473 |
+
|
| 474 |
+
# Clean up
|
| 475 |
+
try:
|
| 476 |
+
temp_path.unlink()
|
| 477 |
+
except:
|
| 478 |
+
pass
|
| 479 |
+
|
| 480 |
+
if result.get("success"):
|
| 481 |
+
return {
|
| 482 |
+
"success": True,
|
| 483 |
+
"transcript": result.get("transcript", ""),
|
| 484 |
+
"language": language,
|
| 485 |
+
"word_count": result.get("word_count", 0)
|
| 486 |
+
}
|
| 487 |
+
else:
|
| 488 |
+
raise HTTPException(status_code=500, detail=result.get("error", "Transcription failed"))
|
| 489 |
+
|
| 490 |
+
except Exception as e:
|
| 491 |
+
logger.error(f"Audio transcription error: {e}")
|
| 492 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 493 |
+
|
| 494 |
+
|
| 495 |
+
# ===== UTILITY FUNCTIONS =====
|
| 496 |
+
|
| 497 |
+
async def save_upload(file: UploadFile, base_path: str, subfolder: str) -> str:
|
| 498 |
+
"""Save an uploaded file and return its path."""
|
| 499 |
+
upload_dir = Path(base_path) / subfolder
|
| 500 |
+
upload_dir.mkdir(parents=True, exist_ok=True)
|
| 501 |
+
|
| 502 |
+
ext = Path(file.filename).suffix
|
| 503 |
+
filename = f"{uuid.uuid4()}{ext}"
|
| 504 |
+
file_path = upload_dir / filename
|
| 505 |
+
|
| 506 |
+
with open(file_path, "wb") as buffer:
|
| 507 |
+
shutil.copyfileobj(file.file, buffer)
|
| 508 |
+
|
| 509 |
+
return str(file_path)
|
|
|
|
|
|
backend/modules/reasoning_engine.py
CHANGED
|
@@ -123,7 +123,7 @@ class ReasoningEngine:
|
|
| 123 |
timings["total_ms"] = round((time.perf_counter() - t0) * 1000, 1)
|
| 124 |
resp = self._create_out_of_domain_response(
|
| 125 |
query=query,
|
| 126 |
-
domain=agent["prompt_analysis"].get("domain", "
|
| 127 |
domain_score=domain_score
|
| 128 |
)
|
| 129 |
resp["timings"] = timings
|
|
@@ -248,11 +248,11 @@ class ReasoningEngine:
|
|
| 248 |
agent = db.query(Agent).filter(Agent.name == agent_name).first()
|
| 249 |
|
| 250 |
if not agent:
|
| 251 |
-
clean_name = agent_name.replace("_agent", "").replace("agent", "").strip()
|
| 252 |
agent = db.query(Agent).filter(Agent.domain.ilike(f"%{clean_name}%")).first()
|
| 253 |
|
| 254 |
-
|
| 255 |
-
|
| 256 |
|
| 257 |
if not agent:
|
| 258 |
agent = db.query(Agent).first()
|
|
@@ -274,9 +274,9 @@ class ReasoningEngine:
|
|
| 274 |
"id": agent.id,
|
| 275 |
"name": agent.name,
|
| 276 |
"system_prompt": agent.system_prompt,
|
| 277 |
-
"domain": agent.domain,
|
| 278 |
"domain_signature": agent.domain_signature or [],
|
| 279 |
-
"prompt_analysis": agent.prompt_analysis or {},
|
| 280 |
"knowledge_graph": agent.knowledge_graph_json or {},
|
| 281 |
"chunk_count": agent.chunk_count or 0
|
| 282 |
}
|
|
|
|
| 123 |
timings["total_ms"] = round((time.perf_counter() - t0) * 1000, 1)
|
| 124 |
resp = self._create_out_of_domain_response(
|
| 125 |
query=query,
|
| 126 |
+
domain=agent.get("domain") or agent["prompt_analysis"].get("domain", "general"),
|
| 127 |
domain_score=domain_score
|
| 128 |
)
|
| 129 |
resp["timings"] = timings
|
|
|
|
| 248 |
agent = db.query(Agent).filter(Agent.name == agent_name).first()
|
| 249 |
|
| 250 |
if not agent:
|
| 251 |
+
clean_name = agent_name.replace("_agent", "").replace("agent", "").strip() or agent_name
|
| 252 |
agent = db.query(Agent).filter(Agent.domain.ilike(f"%{clean_name}%")).first()
|
| 253 |
|
| 254 |
+
if not agent:
|
| 255 |
+
agent = db.query(Agent).filter(Agent.name.ilike(f"%{clean_name}%")).first()
|
| 256 |
|
| 257 |
if not agent:
|
| 258 |
agent = db.query(Agent).first()
|
|
|
|
| 274 |
"id": agent.id,
|
| 275 |
"name": agent.name,
|
| 276 |
"system_prompt": agent.system_prompt,
|
| 277 |
+
"domain": agent.domain or "general",
|
| 278 |
"domain_signature": agent.domain_signature or [],
|
| 279 |
+
"prompt_analysis": agent.prompt_analysis or {"domain": agent.domain or "general"},
|
| 280 |
"knowledge_graph": agent.knowledge_graph_json or {},
|
| 281 |
"chunk_count": agent.chunk_count or 0
|
| 282 |
}
|
backend/requirements.txt
CHANGED
|
@@ -1,67 +1,68 @@
|
|
| 1 |
-
# MEXAR Phase 1 - Backend Dependencies
|
| 2 |
-
|
| 3 |
-
# Domain Guardrail (Section III-A: TF-IDF + NER + Jaccard)
|
| 4 |
-
scikit-learn>=1.4.0
|
| 5 |
-
spacy>=3.7.0,<3.8.0
|
| 6 |
-
thinc>=8.2.0,<8.3.0
|
| 7 |
-
|
| 8 |
-
# Web Framework
|
| 9 |
-
fastapi==0.109.0
|
| 10 |
-
uvicorn[standard]==0.27.0
|
| 11 |
-
|
| 12 |
-
# Groq API
|
| 13 |
-
groq==0.4.2
|
| 14 |
-
httpx==0.27.0 # Pin to compatible version for groq SDK
|
| 15 |
-
|
| 16 |
-
# Knowledge Graph
|
| 17 |
-
networkx==3.2.1
|
| 18 |
-
|
| 19 |
-
# Data Processing
|
| 20 |
-
pandas==2.1.4
|
| 21 |
-
PyPDF2==3.0.1
|
| 22 |
-
python-docx==1.1.0
|
| 23 |
-
|
| 24 |
-
# File Upload
|
| 25 |
-
python-multipart==0.0.6
|
| 26 |
-
|
| 27 |
-
# Video Processing
|
| 28 |
-
opencv-python==4.9.0.80
|
| 29 |
-
|
| 30 |
-
# Environment
|
| 31 |
-
python-dotenv==1.0.0
|
| 32 |
-
|
| 33 |
-
# JSON handling
|
| 34 |
-
orjson==3.9.10
|
| 35 |
-
|
| 36 |
-
# Async support
|
| 37 |
-
aiofiles==23.2.1
|
| 38 |
-
|
| 39 |
-
# Database (Supabase/PostgreSQL)
|
| 40 |
-
SQLAlchemy==2.0.25
|
| 41 |
-
psycopg2-binary==2.9.9
|
| 42 |
-
|
| 43 |
-
# Authentication & Security
|
| 44 |
-
passlib[bcrypt]==1.7.4
|
| 45 |
-
python-jose[cryptography]==3.3.0
|
| 46 |
-
bcrypt==4.1.2
|
| 47 |
-
email-validator==2.1.0
|
| 48 |
-
|
| 49 |
-
# Supabase Client
|
| 50 |
-
supabase==2.24.0
|
| 51 |
-
|
| 52 |
-
# Vector Support
|
| 53 |
-
fastembed>=0.7.0 # Updated from 0.2.0 (was yanked)
|
| 54 |
-
pgvector==0.2.4
|
| 55 |
-
|
| 56 |
-
# RAG Components (NEW)
|
| 57 |
-
sentence-transformers>=2.2.0 # Cross-encoder reranking
|
| 58 |
-
numpy>=1.24.0 # Vector operations
|
| 59 |
-
transformers>=4.38.0
|
| 60 |
-
torch>=2.0.0
|
| 61 |
-
|
| 62 |
-
# Evaluation & Baselines (Phase 3)
|
| 63 |
-
langchain>=0.1.0
|
| 64 |
-
langchain-community>=0.0.20
|
| 65 |
-
langchain-groq>=0.1.0
|
| 66 |
-
chromadb>=0.4.20
|
| 67 |
-
|
|
|
|
|
|
| 1 |
+
# MEXAR Phase 1 - Backend Dependencies
|
| 2 |
+
|
| 3 |
+
# Domain Guardrail (Section III-A: TF-IDF + NER + Jaccard)
|
| 4 |
+
scikit-learn>=1.4.0
|
| 5 |
+
spacy>=3.7.0,<3.8.0
|
| 6 |
+
thinc>=8.2.0,<8.3.0
|
| 7 |
+
|
| 8 |
+
# Web Framework
|
| 9 |
+
fastapi==0.109.0
|
| 10 |
+
uvicorn[standard]==0.27.0
|
| 11 |
+
|
| 12 |
+
# Groq API
|
| 13 |
+
groq==0.4.2
|
| 14 |
+
httpx==0.27.0 # Pin to compatible version for groq SDK
|
| 15 |
+
|
| 16 |
+
# Knowledge Graph
|
| 17 |
+
networkx==3.2.1
|
| 18 |
+
|
| 19 |
+
# Data Processing
|
| 20 |
+
pandas==2.1.4
|
| 21 |
+
PyPDF2==3.0.1
|
| 22 |
+
python-docx==1.1.0
|
| 23 |
+
|
| 24 |
+
# File Upload
|
| 25 |
+
python-multipart==0.0.6
|
| 26 |
+
|
| 27 |
+
# Video Processing
|
| 28 |
+
opencv-python==4.9.0.80
|
| 29 |
+
|
| 30 |
+
# Environment
|
| 31 |
+
python-dotenv==1.0.0
|
| 32 |
+
|
| 33 |
+
# JSON handling
|
| 34 |
+
orjson==3.9.10
|
| 35 |
+
|
| 36 |
+
# Async support
|
| 37 |
+
aiofiles==23.2.1
|
| 38 |
+
|
| 39 |
+
# Database (Supabase/PostgreSQL)
|
| 40 |
+
SQLAlchemy==2.0.25
|
| 41 |
+
psycopg2-binary==2.9.9
|
| 42 |
+
|
| 43 |
+
# Authentication & Security
|
| 44 |
+
passlib[bcrypt]==1.7.4
|
| 45 |
+
python-jose[cryptography]==3.3.0
|
| 46 |
+
bcrypt==4.1.2
|
| 47 |
+
email-validator==2.1.0
|
| 48 |
+
|
| 49 |
+
# Supabase Client
|
| 50 |
+
supabase==2.24.0
|
| 51 |
+
|
| 52 |
+
# Vector Support
|
| 53 |
+
fastembed>=0.7.0 # Updated from 0.2.0 (was yanked)
|
| 54 |
+
pgvector==0.2.4
|
| 55 |
+
|
| 56 |
+
# RAG Components (NEW)
|
| 57 |
+
sentence-transformers>=2.2.0 # Cross-encoder reranking
|
| 58 |
+
numpy>=1.24.0 # Vector operations
|
| 59 |
+
transformers>=4.38.0
|
| 60 |
+
torch>=2.0.0
|
| 61 |
+
|
| 62 |
+
# Evaluation & Baselines (Phase 3)
|
| 63 |
+
langchain>=0.1.0
|
| 64 |
+
langchain-community>=0.0.20
|
| 65 |
+
langchain-groq>=0.1.0
|
| 66 |
+
# chromadb>=0.4.20 # Optional (Requires C++ Build Tools on Python 3.12)
|
| 67 |
+
|
| 68 |
+
|
backend/workers/compilation_worker.py
CHANGED
|
@@ -1,210 +1,213 @@
|
|
| 1 |
-
|
| 2 |
-
import threading
|
| 3 |
-
from pathlib import Path
|
| 4 |
-
from typing import Optional, Callable
|
| 5 |
-
from datetime import datetime
|
| 6 |
-
from sqlalchemy.orm import Session
|
| 7 |
-
import logging
|
| 8 |
-
|
| 9 |
-
from models.agent import Agent, CompilationJob
|
| 10 |
-
from modules.knowledge_compiler import KnowledgeCompiler, create_knowledge_compiler
|
| 11 |
-
from modules.prompt_analyzer import PromptAnalyzer, create_prompt_analyzer
|
| 12 |
-
|
| 13 |
-
logger = logging.getLogger(__name__)
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def _run_compilation_thread(
|
| 17 |
-
agent_id: int,
|
| 18 |
-
job_id: int,
|
| 19 |
-
storage_path: str,
|
| 20 |
-
system_prompt: str,
|
| 21 |
-
files_data: list
|
| 22 |
-
):
|
| 23 |
-
"""Execute the compilation process in a separate thread."""
|
| 24 |
-
from core.database import SessionLocal
|
| 25 |
-
|
| 26 |
-
db = SessionLocal()
|
| 27 |
-
job = None
|
| 28 |
-
agent = None
|
| 29 |
-
|
| 30 |
-
try:
|
| 31 |
-
job = db.query(CompilationJob).filter(CompilationJob.id == job_id).first()
|
| 32 |
-
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
| 33 |
-
|
| 34 |
-
if not job or not agent:
|
| 35 |
-
logger.error(f"Job or agent not found: job_id={job_id}, agent_id={agent_id}")
|
| 36 |
-
return
|
| 37 |
-
|
| 38 |
-
logger.info(f"Starting compilation for agent {agent.name}")
|
| 39 |
-
|
| 40 |
-
# Step 1: Analyze prompt (10%)
|
| 41 |
-
_update_progress(db, job, 10, "Analyzing system prompt")
|
| 42 |
-
analyzer = create_prompt_analyzer()
|
| 43 |
-
prompt_analysis = analyzer.analyze_prompt(system_prompt)
|
| 44 |
-
logger.info(f"Prompt analysis complete: domain={prompt_analysis.get('domain')}")
|
| 45 |
-
|
| 46 |
-
# Step 2: Initialize compiler (20%)
|
| 47 |
-
_update_progress(db, job, 20, "Initializing knowledge compiler")
|
| 48 |
-
compiler = create_knowledge_compiler(str(Path(storage_path).parent))
|
| 49 |
-
|
| 50 |
-
# Step 3: Compile knowledge (20-80%)
|
| 51 |
-
_update_progress(db, job, 30, "Compiling knowledge base")
|
| 52 |
-
logger.info(f"Starting compilation with {len(files_data)} files")
|
| 53 |
-
|
| 54 |
-
# Parse files into the expected format
|
| 55 |
-
parsed_data = []
|
| 56 |
-
for file_info in files_data:
|
| 57 |
-
content = file_info.get("content", "")
|
| 58 |
-
filename = file_info.get("filename", "unknown")
|
| 59 |
-
parsed_data.append({
|
| 60 |
-
"source": filename,
|
| 61 |
-
"content": content,
|
| 62 |
-
"type": "text",
|
| 63 |
-
"records": content.split("\n") if content else []
|
| 64 |
-
})
|
| 65 |
-
|
| 66 |
-
# Run compilation with error handling
|
| 67 |
-
try:
|
| 68 |
-
result = compiler.compile(
|
| 69 |
-
agent_name=agent.name,
|
| 70 |
-
parsed_data=parsed_data,
|
| 71 |
-
system_prompt=system_prompt,
|
| 72 |
-
prompt_analysis=prompt_analysis
|
| 73 |
-
)
|
| 74 |
-
logger.info(f"Compilation complete: {result.get('stats', {})}")
|
| 75 |
-
except Exception as compile_error:
|
| 76 |
-
logger.error(f"Compilation error (continuing): {compile_error}", exc_info=True)
|
| 77 |
-
# Continue even if compilation has issues - embeddings may still be created
|
| 78 |
-
result = {"stats": {}, "domain_signature": []}
|
| 79 |
-
|
| 80 |
-
_update_progress(db, job, 80, "Saving to vector store")
|
| 81 |
-
|
| 82 |
-
# Step 4: Update agent metadata (90%)
|
| 83 |
-
_update_progress(db, job, 90, "Updating agent metadata")
|
| 84 |
-
|
| 85 |
-
agent.status = "ready"
|
| 86 |
-
agent.domain = prompt_analysis.get("domain", "general")
|
| 87 |
-
agent.domain_keywords = prompt_analysis.get("domain_keywords", [])
|
| 88 |
-
agent.
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
job.
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
logger.
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
if
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
db.
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
"""
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
def
|
| 151 |
-
self
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
job
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
"
|
| 203 |
-
"
|
| 204 |
-
"
|
| 205 |
-
"
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import threading
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import Optional, Callable
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from sqlalchemy.orm import Session
|
| 7 |
+
import logging
|
| 8 |
+
|
| 9 |
+
from models.agent import Agent, CompilationJob
|
| 10 |
+
from modules.knowledge_compiler import KnowledgeCompiler, create_knowledge_compiler
|
| 11 |
+
from modules.prompt_analyzer import PromptAnalyzer, create_prompt_analyzer
|
| 12 |
+
|
| 13 |
+
logger = logging.getLogger(__name__)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _run_compilation_thread(
|
| 17 |
+
agent_id: int,
|
| 18 |
+
job_id: int,
|
| 19 |
+
storage_path: str,
|
| 20 |
+
system_prompt: str,
|
| 21 |
+
files_data: list
|
| 22 |
+
):
|
| 23 |
+
"""Execute the compilation process in a separate thread."""
|
| 24 |
+
from core.database import SessionLocal
|
| 25 |
+
|
| 26 |
+
db = SessionLocal()
|
| 27 |
+
job = None
|
| 28 |
+
agent = None
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
job = db.query(CompilationJob).filter(CompilationJob.id == job_id).first()
|
| 32 |
+
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
| 33 |
+
|
| 34 |
+
if not job or not agent:
|
| 35 |
+
logger.error(f"Job or agent not found: job_id={job_id}, agent_id={agent_id}")
|
| 36 |
+
return
|
| 37 |
+
|
| 38 |
+
logger.info(f"Starting compilation for agent {agent.name}")
|
| 39 |
+
|
| 40 |
+
# Step 1: Analyze prompt (10%)
|
| 41 |
+
_update_progress(db, job, 10, "Analyzing system prompt")
|
| 42 |
+
analyzer = create_prompt_analyzer()
|
| 43 |
+
prompt_analysis = analyzer.analyze_prompt(system_prompt)
|
| 44 |
+
logger.info(f"Prompt analysis complete: domain={prompt_analysis.get('domain')}")
|
| 45 |
+
|
| 46 |
+
# Step 2: Initialize compiler (20%)
|
| 47 |
+
_update_progress(db, job, 20, "Initializing knowledge compiler")
|
| 48 |
+
compiler = create_knowledge_compiler(str(Path(storage_path).parent))
|
| 49 |
+
|
| 50 |
+
# Step 3: Compile knowledge (20-80%)
|
| 51 |
+
_update_progress(db, job, 30, "Compiling knowledge base")
|
| 52 |
+
logger.info(f"Starting compilation with {len(files_data)} files")
|
| 53 |
+
|
| 54 |
+
# Parse files into the expected format
|
| 55 |
+
parsed_data = []
|
| 56 |
+
for file_info in files_data:
|
| 57 |
+
content = file_info.get("content", "")
|
| 58 |
+
filename = file_info.get("filename", "unknown")
|
| 59 |
+
parsed_data.append({
|
| 60 |
+
"source": filename,
|
| 61 |
+
"content": content,
|
| 62 |
+
"type": "text",
|
| 63 |
+
"records": content.split("\n") if content else []
|
| 64 |
+
})
|
| 65 |
+
|
| 66 |
+
# Run compilation with error handling
|
| 67 |
+
try:
|
| 68 |
+
result = compiler.compile(
|
| 69 |
+
agent_name=agent.name,
|
| 70 |
+
parsed_data=parsed_data,
|
| 71 |
+
system_prompt=system_prompt,
|
| 72 |
+
prompt_analysis=prompt_analysis
|
| 73 |
+
)
|
| 74 |
+
logger.info(f"Compilation complete: {result.get('stats', {})}")
|
| 75 |
+
except Exception as compile_error:
|
| 76 |
+
logger.error(f"Compilation error (continuing): {compile_error}", exc_info=True)
|
| 77 |
+
# Continue even if compilation has issues - embeddings may still be created
|
| 78 |
+
result = {"stats": {}, "domain_signature": []}
|
| 79 |
+
|
| 80 |
+
_update_progress(db, job, 80, "Saving to vector store")
|
| 81 |
+
|
| 82 |
+
# Step 4: Update agent metadata (90%)
|
| 83 |
+
_update_progress(db, job, 90, "Updating agent metadata")
|
| 84 |
+
|
| 85 |
+
agent.status = "ready"
|
| 86 |
+
agent.domain = prompt_analysis.get("domain", "general")
|
| 87 |
+
agent.domain_keywords = prompt_analysis.get("domain_keywords", [])
|
| 88 |
+
agent.prompt_analysis = prompt_analysis
|
| 89 |
+
agent.domain_signature = result.get("domain_signature", [])
|
| 90 |
+
agent.compilation_stats = result.get("stats", {})
|
| 91 |
+
agent.entity_count = result.get("stats", {}).get("total_entries", 0)
|
| 92 |
+
|
| 93 |
+
# Step 5: Complete (100%)
|
| 94 |
+
job.status = "completed"
|
| 95 |
+
job.progress = 100
|
| 96 |
+
job.current_step = "Compilation complete"
|
| 97 |
+
job.completed_at = datetime.utcnow()
|
| 98 |
+
|
| 99 |
+
db.commit()
|
| 100 |
+
logger.info(f"Agent {agent.name} compilation completed successfully")
|
| 101 |
+
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.error(f"Compilation failed for job {job_id}: {str(e)}", exc_info=True)
|
| 104 |
+
|
| 105 |
+
# CRITICAL: Always update job and agent status on error
|
| 106 |
+
try:
|
| 107 |
+
if not job:
|
| 108 |
+
job = db.query(CompilationJob).filter(CompilationJob.id == job_id).first()
|
| 109 |
+
if not agent:
|
| 110 |
+
agent = db.query(Agent).filter(Agent.id == agent_id).first()
|
| 111 |
+
|
| 112 |
+
if job:
|
| 113 |
+
job.status = "failed"
|
| 114 |
+
job.error_message = str(e)[:500] # Limit error message length
|
| 115 |
+
job.completed_at = datetime.utcnow()
|
| 116 |
+
logger.error(f"Job {job_id} marked as failed")
|
| 117 |
+
|
| 118 |
+
if agent:
|
| 119 |
+
agent.status = "failed"
|
| 120 |
+
logger.error(f"Agent {agent_id} marked as failed")
|
| 121 |
+
|
| 122 |
+
db.commit()
|
| 123 |
+
except Exception as update_error:
|
| 124 |
+
logger.error(f"Failed to update error status: {update_error}")
|
| 125 |
+
db.rollback()
|
| 126 |
+
|
| 127 |
+
finally:
|
| 128 |
+
# CRITICAL: Ensure database connection is closed
|
| 129 |
+
try:
|
| 130 |
+
db.close()
|
| 131 |
+
logger.info(f"Database connection closed for job {job_id}")
|
| 132 |
+
except Exception as close_error:
|
| 133 |
+
logger.error(f"Error closing database: {close_error}")
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _update_progress(db: Session, job: CompilationJob, progress: int, step: str):
|
| 137 |
+
"""Update job progress."""
|
| 138 |
+
job.progress = progress
|
| 139 |
+
job.current_step = step
|
| 140 |
+
db.commit()
|
| 141 |
+
logger.info(f"Job {job.id} progress: {progress}% - {step}")
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class CompilationWorker:
|
| 145 |
+
"""
|
| 146 |
+
Background worker for compiling agent knowledge bases.
|
| 147 |
+
Uses threading for reliable async execution on Windows.
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
def __init__(self):
|
| 151 |
+
self.active_jobs = {} # agent_id -> job_info
|
| 152 |
+
|
| 153 |
+
def start_compilation(
|
| 154 |
+
self,
|
| 155 |
+
db: Session,
|
| 156 |
+
agent: Agent,
|
| 157 |
+
files_data: list,
|
| 158 |
+
progress_callback: Optional[Callable] = None
|
| 159 |
+
) -> CompilationJob:
|
| 160 |
+
"""Start a background compilation job using threading."""
|
| 161 |
+
|
| 162 |
+
# Create compilation job record
|
| 163 |
+
job = CompilationJob(
|
| 164 |
+
agent_id=agent.id,
|
| 165 |
+
status="in_progress",
|
| 166 |
+
progress=0,
|
| 167 |
+
current_step="Initializing"
|
| 168 |
+
)
|
| 169 |
+
db.add(job)
|
| 170 |
+
db.commit()
|
| 171 |
+
db.refresh(job)
|
| 172 |
+
|
| 173 |
+
logger.info(f"Created compilation job {job.id} for agent {agent.name}")
|
| 174 |
+
|
| 175 |
+
# Start background thread
|
| 176 |
+
thread = threading.Thread(
|
| 177 |
+
target=_run_compilation_thread,
|
| 178 |
+
args=(agent.id, job.id, agent.storage_path, agent.system_prompt, files_data),
|
| 179 |
+
daemon=True
|
| 180 |
+
)
|
| 181 |
+
thread.start()
|
| 182 |
+
|
| 183 |
+
self.active_jobs[agent.id] = {
|
| 184 |
+
"job_id": job.id,
|
| 185 |
+
"thread": thread,
|
| 186 |
+
"status": "in_progress"
|
| 187 |
+
}
|
| 188 |
+
|
| 189 |
+
logger.info(f"Started compilation thread for agent {agent.name}")
|
| 190 |
+
return job
|
| 191 |
+
|
| 192 |
+
def get_job_status(self, db: Session, agent_id: int) -> Optional[dict]:
|
| 193 |
+
"""Get the latest job status for an agent."""
|
| 194 |
+
job = db.query(CompilationJob).filter(
|
| 195 |
+
CompilationJob.agent_id == agent_id
|
| 196 |
+
).order_by(CompilationJob.created_at.desc()).first()
|
| 197 |
+
|
| 198 |
+
if not job:
|
| 199 |
+
return None
|
| 200 |
+
|
| 201 |
+
return {
|
| 202 |
+
"id": job.id,
|
| 203 |
+
"status": job.status,
|
| 204 |
+
"progress": job.progress,
|
| 205 |
+
"current_step": job.current_step,
|
| 206 |
+
"error_message": job.error_message,
|
| 207 |
+
"created_at": job.created_at,
|
| 208 |
+
"completed_at": job.completed_at
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
# Singleton instance
|
| 213 |
+
compilation_worker = CompilationWorker()
|