devrajsinh2012 commited on
Commit
b576b71
·
verified ·
1 Parent(s): 39d6f5a

Upload folder using huggingface_hub

Browse files
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
- [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg?style=for-the-badge&logo=python)](https://www.python.org/)
20
- [![FastAPI](https://img.shields.io/badge/FastAPI-0.109-009688.svg?style=for-the-badge&logo=fastapi)](https://fastapi.tiangolo.com/)
21
- [![React 18](https://img.shields.io/badge/React-18-61dafb.svg?style=for-the-badge&logo=react)](https://reactjs.org/)
22
- [![Groq](https://img.shields.io/badge/Groq-LLM-f54e42.svg?style=for-the-badge)](https://groq.com/)
23
- [![Supabase](https://img.shields.io/badge/Supabase-pgvector-3ECF8E?style=for-the-badge&logo=supabase)](https://supabase.com/)
24
- [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](LICENSE)
25
-
26
- <br/>
27
-
28
- **🚀 Live App** → [mexar.vercel.app](https://mexar.vercel.app) &nbsp;&nbsp;|&nbsp;&nbsp; **📡 Backend API** → [devrajsinh2012-mexar.hf.space](https://devrajsinh2012-mexar.hf.space) &nbsp;&nbsp;|&nbsp;&nbsp; **📖 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
- Built with ❤️ by **Devrajsinh Gohil**
561
-
562
- [GitHub](https://github.com/devrajsinh2012/Mexar) · [HF Spaces](https://huggingface.co/spaces/devrajsinh2012/mexar) · [Live App](https://mexar.vercel.app)
563
-
564
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
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
+ [![Python 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg?style=for-the-badge&logo=python)](https://www.python.org/)
20
+ [![FastAPI](https://img.shields.io/badge/FastAPI-0.109-009688.svg?style=for-the-badge&logo=fastapi)](https://fastapi.tiangolo.com/)
21
+ [![React 18](https://img.shields.io/badge/React-18-61dafb.svg?style=for-the-badge&logo=react)](https://reactjs.org/)
22
+ [![Groq](https://img.shields.io/badge/Groq-LLM-f54e42.svg?style=for-the-badge)](https://groq.com/)
23
+ [![Supabase](https://img.shields.io/badge/Supabase-pgvector-3ECF8E?style=for-the-badge&logo=supabase)](https://supabase.com/)
24
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge)](LICENSE)
25
+
26
+ <br/>
27
+
28
+ **🚀 Live App** → [mexar.vercel.app](https://mexar.vercel.app) &nbsp;&nbsp;|&nbsp;&nbsp; **📡 Backend API** → [devrajsinh2012-mexar.hf.space](https://devrajsinh2012-mexar.hf.space) &nbsp;&nbsp;|&nbsp;&nbsp; **📖 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
- storage_path = Path(agent.storage_path).parent
89
- engine = create_reasoning_engine(str(storage_path))
90
-
91
- result = engine.reason(
92
- agent_name=agent.name,
93
- query=request.message
94
- )
95
-
96
- response = {
97
- "success": True,
98
- "answer": result["answer"],
99
- "confidence": result["confidence"],
100
- "in_domain": result["in_domain"]
101
- }
102
-
103
- if request.include_explainability:
104
- try:
105
- explainer = create_explainability_generator()
106
- response["explainability"] = explainer.generate(result)
107
- except Exception as e:
108
- logger.warning(f"Explainability generation failed: {e}")
109
- response["explainability"] = result.get("explainability")
110
-
111
- # Log ASSISTANT message
112
- conversation_service.add_message(
113
- db,
114
- conversation.id,
115
- "assistant",
116
- result["answer"],
117
- explainability_data=response.get("explainability"),
118
- confidence=result["confidence"]
119
- )
120
-
121
- # Generate TTS if requested
122
- if request.include_tts:
123
- try:
124
- tts_service = get_tts_service()
125
- tts_result = tts_service.generate_speech(
126
- text=result["answer"],
127
- provider=request.tts_provider
128
- )
129
- response["tts"] = tts_result
130
- except Exception as e:
131
- logger.warning(f"TTS generation failed: {e}")
132
- response["tts"] = {"success": False, "error": str(e)}
133
-
134
- return response
135
-
136
- except Exception as e:
137
- logger.error(f"Chat error: {e}")
138
- raise HTTPException(status_code=500, detail=str(e))
139
-
140
-
141
- # ===== MULTIMODAL CHAT ENDPOINT =====
142
-
143
- @router.post("/multimodal")
144
- async def chat_multimodal(
145
- agent_name: str = Form(...),
146
- message: str = Form(""),
147
- audio: UploadFile = File(None),
148
- image: UploadFile = File(None),
149
- include_explainability: bool = Form(True),
150
- include_tts: bool = Form(False),
151
- tts_provider: str = Form("elevenlabs"),
152
- db: Session = Depends(get_db),
153
- current_user: User = Depends(get_current_user)
154
- ):
155
- """
156
- Chat with an agent using multimodal inputs (audio/image).
157
- Uses multipart form data.
158
- """
159
- from modules.multimodal_processor import create_multimodal_processor
160
-
161
- # Get agent with ownership check
162
- agent = agent_service.get_agent(db, current_user, agent_name)
163
- if not agent:
164
- raise HTTPException(status_code=404, detail=f"Agent '{agent_name}' not found")
165
-
166
- if agent.status != "ready":
167
- raise HTTPException(
168
- status_code=400,
169
- detail=f"Agent is not ready. Current status: {agent.status}"
170
- )
171
-
172
- # Get/Create conversation
173
- conversation = conversation_service.get_or_create_conversation(
174
- db, agent.id, current_user.id
175
- )
176
-
177
- try:
178
- multimodal_context = ""
179
- audio_url = None
180
- image_url = None
181
-
182
- # Process audio if provided
183
- if audio and audio.filename:
184
- # Upload to Supabase Storage
185
- upload_result = await storage_service.upload_file(
186
- file=audio,
187
- bucket="chat-media",
188
- folder=f"audio/{agent.id}"
189
- )
190
- audio_url = upload_result["url"]
191
-
192
- # Save temporarily for processing
193
- temp_dir = Path("data/temp")
194
- temp_dir.mkdir(parents=True, exist_ok=True)
195
- temp_path = temp_dir / f"{uuid.uuid4()}{Path(audio.filename).suffix}"
196
-
197
- with open(temp_path, "wb") as buffer:
198
- await audio.seek(0) # Reset file pointer
199
- shutil.copyfileobj(audio.file, buffer)
200
-
201
- processor = create_multimodal_processor()
202
- audio_text = processor.process_audio(str(temp_path))
203
- if audio_text:
204
- multimodal_context += f"\n[AUDIO TRANSCRIPTION]: {audio_text}"
205
-
206
- # Clean up temp file
207
- try:
208
- temp_path.unlink()
209
- except:
210
- pass
211
-
212
- # Process image if provided
213
- if image and image.filename:
214
- # Upload to Supabase Storage
215
- upload_result = await storage_service.upload_file(
216
- file=image,
217
- bucket="chat-media",
218
- folder=f"images/{agent.id}"
219
- )
220
- image_url = upload_result["url"]
221
- logger.info(f"[MULTIMODAL] Image uploaded to Supabase: {image_url}")
222
-
223
- # Save temporarily for processing
224
- temp_dir = Path("data/temp")
225
- temp_dir.mkdir(parents=True, exist_ok=True)
226
- temp_path = temp_dir / f"{uuid.uuid4()}{Path(image.filename).suffix}"
227
-
228
- logger.info(f"[MULTIMODAL] Saving temp file: {temp_path}")
229
-
230
- with open(temp_path, "wb") as buffer:
231
- await image.seek(0) # Reset file pointer
232
- shutil.copyfileobj(image.file, buffer)
233
-
234
- file_size = temp_path.stat().st_size
235
- logger.info(f"[MULTIMODAL] Temp file saved, size: {file_size} bytes")
236
-
237
- try:
238
- logger.info(f"[MULTIMODAL] Starting image analysis with Groq Vision...")
239
- processor = create_multimodal_processor()
240
- image_result = processor.process_image(str(temp_path))
241
-
242
- logger.info(f"[MULTIMODAL] Image processing result: {image_result.get('success')}")
243
-
244
- if image_result.get("success"):
245
- image_desc = image_result.get("description", "")
246
- if image_desc:
247
- logger.info(f"[MULTIMODAL] Image analyzed successfully, description length: {len(image_desc)} chars")
248
- logger.info(f"[MULTIMODAL] Description preview: {image_desc[:150]}...")
249
- multimodal_context += f"\n[IMAGE DESCRIPTION]: {image_desc}"
250
- else:
251
- logger.warning(f"[MULTIMODAL] Image analysis returned success but empty description")
252
- multimodal_context += f"\n[IMAGE]: User uploaded an image named {image.filename}"
253
- else:
254
- # Log error but don't fail - provide basic context
255
- error_msg = image_result.get('error', 'Unknown error')
256
- error_type = image_result.get('error_type', 'Unknown')
257
- logger.warning(f"[MULTIMODAL] Image analysis failed - {error_type}: {error_msg}")
258
- multimodal_context += f"\n[IMAGE]: User uploaded an image named {image.filename}"
259
-
260
- except Exception as e:
261
- logger.error(f"[MULTIMODAL] Image processing exception: {type(e).__name__}: {str(e)}")
262
- import traceback
263
- logger.error(f"[MULTIMODAL] Traceback: {traceback.format_exc()}")
264
- multimodal_context += f"\n[IMAGE]: User uploaded an image named {image.filename}"
265
-
266
- # Clean up temp file
267
- try:
268
- temp_path.unlink()
269
- logger.info(f"[MULTIMODAL] Temp file cleaned up")
270
- except:
271
- pass
272
-
273
- # Run reasoning
274
- storage_path = Path(agent.storage_path).parent
275
- engine = create_reasoning_engine(str(storage_path))
276
-
277
- result = engine.reason(
278
- agent_name=agent.name,
279
- query=message,
280
- multimodal_context=multimodal_context
281
- )
282
-
283
- # Log USER message with attachments
284
- conversation_service.add_message(
285
- db,
286
- conversation.id,
287
- "user",
288
- message,
289
- multimodal_data={
290
- "audio_url": audio_url,
291
- "image_url": image_url
292
- }
293
- )
294
-
295
- response = {
296
- "success": True,
297
- "answer": result["answer"],
298
- "confidence": result["confidence"],
299
- "in_domain": result["in_domain"],
300
- "audio_url": audio_url,
301
- "image_url": image_url
302
- }
303
-
304
- if include_explainability:
305
- try:
306
- explainer = create_explainability_generator()
307
- response["explainability"] = explainer.generate(result)
308
- except Exception:
309
- response["explainability"] = result.get("explainability")
310
-
311
- # Log ASSISTANT message
312
- conversation_service.add_message(
313
- db,
314
- conversation.id,
315
- "assistant",
316
- result["answer"],
317
- explainability_data=response.get("explainability"),
318
- confidence=result["confidence"]
319
- )
320
-
321
- # Generate TTS if requested
322
- if include_tts:
323
- try:
324
- tts_service = get_tts_service()
325
- tts_result = tts_service.generate_speech(
326
- text=result["answer"],
327
- provider=tts_provider
328
- )
329
- response["tts"] = tts_result
330
- except Exception as e:
331
- logger.warning(f"TTS generation failed: {e}")
332
- response["tts"] = {"success": False, "error": str(e)}
333
-
334
- return response
335
-
336
- except Exception as e:
337
- logger.error(f"Multimodal chat error: {e}")
338
- raise HTTPException(status_code=500, detail=str(e))
339
-
340
-
341
- # ===== HISTORY ENDPOINTS =====
342
-
343
- @router.get("/{agent_name}/history")
344
- def get_chat_history(
345
- agent_name: str,
346
- limit: int = 50,
347
- db: Session = Depends(get_db),
348
- current_user: User = Depends(get_current_user)
349
- ):
350
- """Get conversation history with an agent."""
351
- from services.conversation_service import conversation_service
352
-
353
- agent = agent_service.get_agent(db, current_user, agent_name)
354
- if not agent:
355
- raise HTTPException(status_code=404, detail="Agent not found")
356
-
357
- history = conversation_service.get_conversation_history(
358
- db, agent.id, current_user.id, limit
359
- )
360
- return {"messages": history}
361
-
362
-
363
- @router.delete("/{agent_name}/history")
364
- def clear_chat_history(
365
- agent_name: str,
366
- db: Session = Depends(get_db),
367
- current_user: User = Depends(get_current_user)
368
- ):
369
- """Clear conversation history with an agent."""
370
- from models.conversation import Conversation
371
-
372
- agent = agent_service.get_agent(db, current_user, agent_name)
373
- if not agent:
374
- raise HTTPException(status_code=404, detail="Agent not found")
375
-
376
- conversation = db.query(Conversation).filter(
377
- Conversation.agent_id == agent.id,
378
- Conversation.user_id == current_user.id
379
- ).first()
380
-
381
- if conversation:
382
- db.delete(conversation)
383
- db.commit()
384
-
385
- return {"message": "Chat history cleared"}
386
-
387
-
388
- # ===== TEXT-TO-SPEECH ENDPOINTS =====
389
-
390
- @router.post("/tts/generate")
391
- async def generate_tts(
392
- request: TTSRequest,
393
- current_user: User = Depends(get_current_user)
394
- ):
395
- """Generate text-to-speech audio."""
396
- try:
397
- tts_service = get_tts_service()
398
- result = tts_service.generate_speech(
399
- text=request.text,
400
- provider=request.provider,
401
- voice_id=request.voice_id
402
- )
403
- return result
404
- except Exception as e:
405
- logger.error(f"TTS generation error: {e}")
406
- raise HTTPException(status_code=500, detail=str(e))
407
-
408
-
409
- @router.get("/tts/audio/{filename}")
410
- async def serve_tts_audio(filename: str):
411
- """Serve cached TTS audio files."""
412
- audio_path = Path("data/tts_cache") / filename
413
-
414
- if not audio_path.exists():
415
- raise HTTPException(status_code=404, detail="Audio file not found")
416
-
417
- return FileResponse(
418
- path=audio_path,
419
- media_type="audio/mpeg",
420
- filename=filename
421
- )
422
-
423
-
424
- @router.get("/tts/voices")
425
- async def get_tts_voices(
426
- provider: str = "elevenlabs",
427
- current_user: User = Depends(get_current_user)
428
- ):
429
- """Get available TTS voices for a provider."""
430
- try:
431
- tts_service = get_tts_service()
432
- voices = tts_service.get_available_voices(provider)
433
- return {"provider": provider, "voices": voices}
434
- except Exception as e:
435
- logger.error(f"Failed to fetch voices: {e}")
436
- raise HTTPException(status_code=500, detail=str(e))
437
-
438
-
439
- @router.get("/tts/quota")
440
- async def get_tts_quota(current_user: User = Depends(get_current_user)):
441
- """Check TTS quota for ElevenLabs."""
442
- try:
443
- tts_service = get_tts_service()
444
- quota = tts_service.check_quota()
445
- return quota
446
- except Exception as e:
447
- logger.error(f"Failed to check quota: {e}")
448
- raise HTTPException(status_code=500, detail=str(e))
449
-
450
-
451
- # ===== LIVE AUDIO TRANSCRIPTION =====
452
-
453
- @router.post("/transcribe")
454
- async def transcribe_audio(
455
- audio: UploadFile = File(...),
456
- language: str = Form("en"),
457
- current_user: User = Depends(get_current_user)
458
- ):
459
- """Transcribe uploaded audio (for live recording)."""
460
- from modules.multimodal_processor import create_multimodal_processor
461
-
462
- try:
463
- # Save audio temporarily
464
- temp_dir = Path("data/temp")
465
- temp_dir.mkdir(parents=True, exist_ok=True)
466
-
467
- temp_path = temp_dir / f"{uuid.uuid4()}{Path(audio.filename).suffix}"
468
-
469
- with open(temp_path, "wb") as buffer:
470
- shutil.copyfileobj(audio.file, buffer)
471
-
472
- # Transcribe
473
- processor = create_multimodal_processor()
474
- result = processor.process_audio(str(temp_path), language)
475
-
476
- # Clean up
477
- try:
478
- temp_path.unlink()
479
- except:
480
- pass
481
-
482
- if result.get("success"):
483
- return {
484
- "success": True,
485
- "transcript": result.get("transcript", ""),
486
- "language": language,
487
- "word_count": result.get("word_count", 0)
488
- }
489
- else:
490
- raise HTTPException(status_code=500, detail=result.get("error", "Transcription failed"))
491
-
492
- except Exception as e:
493
- logger.error(f"Audio transcription error: {e}")
494
- raise HTTPException(status_code=500, detail=str(e))
495
-
496
-
497
- # ===== UTILITY FUNCTIONS =====
498
-
499
- async def save_upload(file: UploadFile, base_path: str, subfolder: str) -> str:
500
- """Save an uploaded file and return its path."""
501
- upload_dir = Path(base_path) / subfolder
502
- upload_dir.mkdir(parents=True, exist_ok=True)
503
-
504
- ext = Path(file.filename).suffix
505
- filename = f"{uuid.uuid4()}{ext}"
506
- file_path = upload_dir / filename
507
-
508
- with open(file_path, "wb") as buffer:
509
- shutil.copyfileobj(file.file, buffer)
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", "unknown"),
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
- 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,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.entity_count = result.get("stats", {}).get("total_entries", 0)
89
-
90
- # Step 5: Complete (100%)
91
- job.status = "completed"
92
- job.progress = 100
93
- job.current_step = "Compilation complete"
94
- job.completed_at = datetime.utcnow()
95
-
96
- db.commit()
97
- logger.info(f"Agent {agent.name} compilation completed successfully")
98
-
99
- except Exception as e:
100
- logger.error(f"Compilation failed for job {job_id}: {str(e)}", exc_info=True)
101
-
102
- # CRITICAL: Always update job and agent status on error
103
- try:
104
- if not job:
105
- job = db.query(CompilationJob).filter(CompilationJob.id == job_id).first()
106
- if not agent:
107
- agent = db.query(Agent).filter(Agent.id == agent_id).first()
108
-
109
- if job:
110
- job.status = "failed"
111
- job.error_message = str(e)[:500] # Limit error message length
112
- job.completed_at = datetime.utcnow()
113
- logger.error(f"Job {job_id} marked as failed")
114
-
115
- if agent:
116
- agent.status = "failed"
117
- logger.error(f"Agent {agent_id} marked as failed")
118
-
119
- db.commit()
120
- except Exception as update_error:
121
- logger.error(f"Failed to update error status: {update_error}")
122
- db.rollback()
123
-
124
- finally:
125
- # CRITICAL: Ensure database connection is closed
126
- try:
127
- db.close()
128
- logger.info(f"Database connection closed for job {job_id}")
129
- except Exception as close_error:
130
- logger.error(f"Error closing database: {close_error}")
131
-
132
-
133
- def _update_progress(db: Session, job: CompilationJob, progress: int, step: str):
134
- """Update job progress."""
135
- job.progress = progress
136
- job.current_step = step
137
- db.commit()
138
- logger.info(f"Job {job.id} progress: {progress}% - {step}")
139
-
140
-
141
- class CompilationWorker:
142
- """
143
- Background worker for compiling agent knowledge bases.
144
- Uses threading for reliable async execution on Windows.
145
- """
146
-
147
- def __init__(self):
148
- self.active_jobs = {} # agent_id -> job_info
149
-
150
- def start_compilation(
151
- self,
152
- db: Session,
153
- agent: Agent,
154
- files_data: list,
155
- progress_callback: Optional[Callable] = None
156
- ) -> CompilationJob:
157
- """Start a background compilation job using threading."""
158
-
159
- # Create compilation job record
160
- job = CompilationJob(
161
- agent_id=agent.id,
162
- status="in_progress",
163
- progress=0,
164
- current_step="Initializing"
165
- )
166
- db.add(job)
167
- db.commit()
168
- db.refresh(job)
169
-
170
- logger.info(f"Created compilation job {job.id} for agent {agent.name}")
171
-
172
- # Start background thread
173
- thread = threading.Thread(
174
- target=_run_compilation_thread,
175
- args=(agent.id, job.id, agent.storage_path, agent.system_prompt, files_data),
176
- daemon=True
177
- )
178
- thread.start()
179
-
180
- self.active_jobs[agent.id] = {
181
- "job_id": job.id,
182
- "thread": thread,
183
- "status": "in_progress"
184
- }
185
-
186
- logger.info(f"Started compilation thread for agent {agent.name}")
187
- return job
188
-
189
- def get_job_status(self, db: Session, agent_id: int) -> Optional[dict]:
190
- """Get the latest job status for an agent."""
191
- job = db.query(CompilationJob).filter(
192
- CompilationJob.agent_id == agent_id
193
- ).order_by(CompilationJob.created_at.desc()).first()
194
-
195
- if not job:
196
- return None
197
-
198
- return {
199
- "id": job.id,
200
- "status": job.status,
201
- "progress": job.progress,
202
- "current_step": job.current_step,
203
- "error_message": job.error_message,
204
- "created_at": job.created_at,
205
- "completed_at": job.completed_at
206
- }
207
-
208
-
209
- # Singleton instance
210
- compilation_worker = CompilationWorker()
 
 
 
 
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()