Mirror of github.com/Abhisingh18/Trust-first-AI-Copilot
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- README.md +225 -0
- __pycache__/main.cpython-311.pyc +0 -0
- app/__pycache__/main.cpython-311.pyc +0 -0
- app/api/__pycache__/endpoints.cpython-311.pyc +0 -0
- app/api/endpoints.py +182 -0
- app/core/__pycache__/config.cpython-311.pyc +0 -0
- app/core/__pycache__/prompts.cpython-311.pyc +0 -0
- app/core/config.py +28 -0
- app/core/prompts.py +164 -0
- app/services/__pycache__/files.cpython-311.pyc +0 -0
- app/services/__pycache__/intent.cpython-311.pyc +0 -0
- app/services/__pycache__/llm.cpython-311.pyc +0 -0
- app/services/__pycache__/search.cpython-311.pyc +0 -0
- app/services/__pycache__/vector.cpython-311.pyc +0 -0
- app/services/files.py +63 -0
- app/services/intent.py +63 -0
- app/services/llm.py +240 -0
- app/services/search.py +55 -0
- app/services/vector.py +76 -0
- backend/.env +17 -0
- backend/__init__.py +0 -0
- backend/__pycache__/__init__.cpython-311.pyc +0 -0
- backend/debug.log +823 -0
- backend/debug_env.py +47 -0
- backend/list_available_models.py +21 -0
- backend/list_models.py +12 -0
- backend/my_models.txt +0 -0
- backend/rag_summarizer.py +81 -0
- debug.log +18 -0
- frontend/.gitignore +41 -0
- frontend/README.md +36 -0
- frontend/app/favicon.ico +0 -0
- frontend/app/globals.css +100 -0
- frontend/app/layout.tsx +35 -0
- frontend/app/page.tsx +5 -0
- frontend/components/Chat/ChatInput.tsx +87 -0
- frontend/components/Chat/ChatLayout.tsx +237 -0
- frontend/components/Chat/MessageBubble.tsx +121 -0
- frontend/components/Chat/Sidebar.tsx +90 -0
- frontend/components/ui/SearchBar.tsx +58 -0
- frontend/components/ui/SourceList.tsx +42 -0
- frontend/env_example.txt +1 -0
- frontend/eslint.config.mjs +18 -0
- frontend/lib/utils.ts +37 -0
- frontend/next.config.ts +7 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +32 -0
- frontend/postcss.config.mjs +7 -0
- frontend/public/file.svg +1 -0
- frontend/public/globe.svg +1 -0
README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Trust-First AI Copilot
|
| 2 |
+
**Perplexity-Style • System-Driven • No Custom LLM**
|
| 3 |
+
Deployed Link: https://trust-first-ai.vercel.app/
|
| 4 |
+
|
| 5 |
+
A trust-first AI Copilot that delivers **verified, source-grounded, and confidence-scored answers** using strict system rules — inspired by Perplexity and designed to fix the core limitations of modern AI copilots.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## Problem
|
| 10 |
+
Most AI copilots today:
|
| 11 |
+
- Produce confident but incorrect (hallucinated) answers
|
| 12 |
+
- Lose context in long or multi-file documents
|
| 13 |
+
- Hide sources and assumptions
|
| 14 |
+
- Provide limited admin visibility and control
|
| 15 |
+
- Encourage blind dependency on AI outputs
|
| 16 |
+
|
| 17 |
+
These issues lead to **wrong decisions, rework, and low trust**.
|
| 18 |
+
|
| 19 |
+
---
|
| 20 |
+
|
| 21 |
+
## Solution
|
| 22 |
+
This project implements a **system-first AI Copilot** where:
|
| 23 |
+
- Retrieval is mandatory (no context → no answer)
|
| 24 |
+
- Every answer is backed by sources
|
| 25 |
+
- Confidence is explicitly shown
|
| 26 |
+
- Low-confidence answers are refused
|
| 27 |
+
- Automation is human-approved
|
| 28 |
+
- Security, transparency, and auditability are built-in
|
| 29 |
+
|
| 30 |
+
The focus is **system design over model size**.
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## Core Principles
|
| 35 |
+
- No guessing
|
| 36 |
+
- No hidden sources
|
| 37 |
+
- No blind automation
|
| 38 |
+
- Refusal is a feature, not a failure
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## Key Features
|
| 43 |
+
- Mandatory Retrieval-Augmented Generation (RAG)
|
| 44 |
+
- Source-linked answers with citations
|
| 45 |
+
- Confidence scoring (High / Medium / Low)
|
| 46 |
+
- Automatic refusal on insufficient data
|
| 47 |
+
- Workspace / project-level context memory
|
| 48 |
+
- Intent detection and auto-clarification
|
| 49 |
+
- Human-in-the-loop automation (n8n)
|
| 50 |
+
- Zero-trust data access
|
| 51 |
+
- Full audit logs (OpenTelemetry)
|
| 52 |
+
- Model-agnostic LLM layer (Groq)
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## System Architecture
|
| 57 |
+
User
|
| 58 |
+
→ Intent Detection
|
| 59 |
+
→ Search & Retrieval (Tavily + Vector DB)
|
| 60 |
+
→ Context Ranking & Filtering
|
| 61 |
+
→ LLM (Groq – language & reasoning only)
|
| 62 |
+
→ Verification & Confidence Engine
|
| 63 |
+
→ Answer + Sources + Assumptions
|
| 64 |
+
→ (Optional) Human-Approved Automation
|
| 65 |
+
→ Audit Logs & Admin Dashboard
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
## What This Project Is Not
|
| 69 |
+
- Not a chatbot
|
| 70 |
+
- Not prompt-dependent
|
| 71 |
+
- Not blind AI
|
| 72 |
+
- Not a Copilot replacement
|
| 73 |
+
|
| 74 |
+
This is a **controlled, transparent, enterprise-ready AI system**.
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## Tech Stack
|
| 79 |
+
**Frontend**
|
| 80 |
+
- Next.js
|
| 81 |
+
- React
|
| 82 |
+
- Tailwind CSS
|
| 83 |
+
|
| 84 |
+
**Backend**
|
| 85 |
+
- FastAPI (Python)
|
| 86 |
+
|
| 87 |
+
**AI & Data**
|
| 88 |
+
- LLM: Groq (LLaMA / Mixtral)
|
| 89 |
+
- Search: Tavily API
|
| 90 |
+
- Embeddings: Hugging Face / Local models
|
| 91 |
+
- Vector DB: FAISS / Qdrant
|
| 92 |
+
- Automation: n8n
|
| 93 |
+
- Logging & Audit: OpenTelemetry
|
| 94 |
+
|
| 95 |
+
**Deployment**
|
| 96 |
+
- Frontend: Vercel
|
| 97 |
+
- Backend: Render
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
## Project Structure
|
| 102 |
+
project-root/
|
| 103 |
+
├── frontend/
|
| 104 |
+
│ ├── pages/
|
| 105 |
+
│ ├── components/
|
| 106 |
+
│ └── services/
|
| 107 |
+
├── backend/
|
| 108 |
+
│ ├── main.py
|
| 109 |
+
│ ├── rag/
|
| 110 |
+
│ ├── verification/
|
| 111 |
+
│ ├── automation/
|
| 112 |
+
│ └── requirements.txt
|
| 113 |
+
├── docs/
|
| 114 |
+
└── README.md
|
| 115 |
+
|
| 116 |
+
yaml
|
| 117 |
+
Copy code
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
## Required API Keys
|
| 122 |
+
| Service | Purpose |
|
| 123 |
+
|-------|---------|
|
| 124 |
+
| Groq | LLM inference |
|
| 125 |
+
| Tavily | Web search |
|
| 126 |
+
| Hugging Face | Embeddings |
|
| 127 |
+
| n8n | Automation |
|
| 128 |
+
|
| 129 |
+
> All API keys are stored **only in backend environment variables**.
|
| 130 |
+
|
| 131 |
+
---
|
| 132 |
+
|
| 133 |
+
## Local Setup (Backend)
|
| 134 |
+
```bash
|
| 135 |
+
git clone https://github.com/your-username/your-repo.git
|
| 136 |
+
cd backend
|
| 137 |
+
python -m venv venv
|
| 138 |
+
source venv/bin/activate
|
| 139 |
+
pip install -r requirements.txt
|
| 140 |
+
Create .env:
|
| 141 |
+
|
| 142 |
+
env
|
| 143 |
+
Copy code
|
| 144 |
+
GROQ_API_KEY=xxxx
|
| 145 |
+
TAVILY_API_KEY=tvly_xxxx
|
| 146 |
+
HF_API_KEY=hf_xxxx
|
| 147 |
+
N8N_API_KEY=xxxx
|
| 148 |
+
N8N_BASE_URL=http://localhost:5678
|
| 149 |
+
Run backend:
|
| 150 |
+
|
| 151 |
+
bash
|
| 152 |
+
Copy code
|
| 153 |
+
uvicorn main:app --reload
|
| 154 |
+
Local Setup (Frontend)
|
| 155 |
+
bash
|
| 156 |
+
Copy code
|
| 157 |
+
cd frontend
|
| 158 |
+
npm install
|
| 159 |
+
npm run dev
|
| 160 |
+
Deployment
|
| 161 |
+
Backend
|
| 162 |
+
|
| 163 |
+
Push code to GitHub
|
| 164 |
+
|
| 165 |
+
Connect repository to Render
|
| 166 |
+
|
| 167 |
+
Build command:
|
| 168 |
+
|
| 169 |
+
bash
|
| 170 |
+
Copy code
|
| 171 |
+
pip install -r requirements.txt
|
| 172 |
+
Start command:
|
| 173 |
+
|
| 174 |
+
bash
|
| 175 |
+
Copy code
|
| 176 |
+
uvicorn main:app --host 0.0.0.0 --port 10000
|
| 177 |
+
Frontend
|
| 178 |
+
|
| 179 |
+
Deploy via Vercel
|
| 180 |
+
|
| 181 |
+
Set backend API URL in environment variables
|
| 182 |
+
|
| 183 |
+
Security & Trust Model
|
| 184 |
+
API keys never exposed to frontend
|
| 185 |
+
|
| 186 |
+
Per-user data isolation
|
| 187 |
+
|
| 188 |
+
Role-based access control
|
| 189 |
+
|
| 190 |
+
Full audit trail for AI actions
|
| 191 |
+
|
| 192 |
+
How Hallucinations Are Prevented
|
| 193 |
+
Retrieval is mandatory
|
| 194 |
+
|
| 195 |
+
Claims must map to sources
|
| 196 |
+
|
| 197 |
+
Confidence is evaluated
|
| 198 |
+
|
| 199 |
+
Low confidence triggers refusal
|
| 200 |
+
|
| 201 |
+
No source → No answer
|
| 202 |
+
|
| 203 |
+
Use Cases
|
| 204 |
+
Research and academic assistance
|
| 205 |
+
|
| 206 |
+
Enterprise internal knowledge copilots
|
| 207 |
+
|
| 208 |
+
Policy and compliance analysis
|
| 209 |
+
|
| 210 |
+
Long-document summarization
|
| 211 |
+
|
| 212 |
+
Decision-support systems
|
| 213 |
+
|
| 214 |
+
Future Improvements
|
| 215 |
+
Offline read-only mode
|
| 216 |
+
|
| 217 |
+
Multimodal reasoning (charts + text)
|
| 218 |
+
|
| 219 |
+
Advanced admin dashboards
|
| 220 |
+
|
| 221 |
+
Domain-specific copilots
|
| 222 |
+
|
| 223 |
+
Final Note
|
| 224 |
+
LLMs don’t fail — systems fail.
|
| 225 |
+
This project demonstrates how strong system design beats larger models.
|
__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (1.21 kB). View file
|
|
|
app/__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (1.21 kB). View file
|
|
|
app/api/__pycache__/endpoints.cpython-311.pyc
ADDED
|
Binary file (9.31 kB). View file
|
|
|
app/api/endpoints.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, HTTPException
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
from app.services.llm import llm_service
|
| 5 |
+
from app.services.vector import vector_service
|
| 6 |
+
from app.services.search import search_service
|
| 7 |
+
from app.services.intent import IntentService
|
| 8 |
+
from app.services.files import file_service
|
| 9 |
+
from fastapi import UploadFile, File
|
| 10 |
+
|
| 11 |
+
# Initialize Intent Service
|
| 12 |
+
intent_service = IntentService(llm_service)
|
| 13 |
+
|
| 14 |
+
router = APIRouter()
|
| 15 |
+
|
| 16 |
+
# --- Pydantic Models ---
|
| 17 |
+
class QueryRequest(BaseModel):
|
| 18 |
+
query: str
|
| 19 |
+
|
| 20 |
+
class Source(BaseModel):
|
| 21 |
+
title: str
|
| 22 |
+
url: str
|
| 23 |
+
snippet: str
|
| 24 |
+
|
| 25 |
+
class ChallengeRequest(BaseModel):
|
| 26 |
+
original_query: str
|
| 27 |
+
original_answer: str
|
| 28 |
+
sources_text: str
|
| 29 |
+
|
| 30 |
+
class QueryResponse(BaseModel):
|
| 31 |
+
answer: str
|
| 32 |
+
sources: List[Source]
|
| 33 |
+
confidence: str
|
| 34 |
+
search_queries: List[str]
|
| 35 |
+
intent: Optional[str] = None
|
| 36 |
+
thought_process: Optional[str] = None
|
| 37 |
+
|
| 38 |
+
# --- Endpoints ---
|
| 39 |
+
|
| 40 |
+
@router.post("/query", response_model=QueryResponse)
|
| 41 |
+
async def process_query(request: QueryRequest):
|
| 42 |
+
"""
|
| 43 |
+
Main orchestration endpoint for the Trust-First Copilot.
|
| 44 |
+
"""
|
| 45 |
+
user_query = request.query
|
| 46 |
+
print(f"Refining query: {user_query}")
|
| 47 |
+
|
| 48 |
+
try:
|
| 49 |
+
# --- PHASE 1: INTENT & RISK ANALYSIS ---
|
| 50 |
+
print("🧠 Analyzing Intent...")
|
| 51 |
+
try:
|
| 52 |
+
intent = await intent_service.analyze(user_query)
|
| 53 |
+
print(f" Category: {intent.category}")
|
| 54 |
+
print(f" Reasoning: {intent.reasoning}")
|
| 55 |
+
print(f" Risk: {intent.risk_level}")
|
| 56 |
+
except Exception as e:
|
| 57 |
+
print(f"Intent Error: {e}")
|
| 58 |
+
from app.services.intent import IntentResponse
|
| 59 |
+
intent = IntentResponse(category="SEARCH_REQUIRED", reasoning="Error", risk_level="LOW")
|
| 60 |
+
|
| 61 |
+
# Risk Guard
|
| 62 |
+
if intent.risk_level == "HIGH":
|
| 63 |
+
return QueryResponse(
|
| 64 |
+
answer="I cannot fulfill this request as it has been flagged as high risk/safety violation.",
|
| 65 |
+
sources=[],
|
| 66 |
+
confidence="Blocked",
|
| 67 |
+
search_queries=[],
|
| 68 |
+
intent="High Risk",
|
| 69 |
+
thought_process=f"Blocked by Risk Analyzer. Reasoning: {intent.reasoning}"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
# --- PHASE 2: EXECUTION ---
|
| 73 |
+
search_results = []
|
| 74 |
+
|
| 75 |
+
# Branch 1: Needs Search
|
| 76 |
+
if intent.category == "SEARCH_REQUIRED" or intent.category == "DATA_ANALYSIS":
|
| 77 |
+
print("🔍 Initiating Web Search...")
|
| 78 |
+
search_results = await search_service.search(user_query)
|
| 79 |
+
if not search_results:
|
| 80 |
+
# Fallback if search finds nothing but intent was search
|
| 81 |
+
pass
|
| 82 |
+
|
| 83 |
+
# Branch 2: Coding (Skip Search usually, unless specific docs needed)
|
| 84 |
+
elif intent.category == "CODING_TASK":
|
| 85 |
+
print("💻 Coding Task - Focused Generation")
|
| 86 |
+
# Potential future improvement: Search for docs if needed
|
| 87 |
+
|
| 88 |
+
# Branch 3: Chat / General
|
| 89 |
+
else:
|
| 90 |
+
print("💬 Chat Mode - Direct Generation")
|
| 91 |
+
|
| 92 |
+
# --- PHASE 3: CONTEXT & RAG ---
|
| 93 |
+
context_text = ""
|
| 94 |
+
final_sources = []
|
| 95 |
+
|
| 96 |
+
if search_results:
|
| 97 |
+
# RAG Logic
|
| 98 |
+
print("Indexing search results in Vector DB...")
|
| 99 |
+
vector_service.create_index_from_results(search_results)
|
| 100 |
+
|
| 101 |
+
print("Searching Vector DB for relevant context...")
|
| 102 |
+
relevant_chunks = vector_service.search_similar(user_query, k=5)
|
| 103 |
+
final_sources = relevant_chunks if relevant_chunks else search_results
|
| 104 |
+
|
| 105 |
+
context_text = "\n\n".join([
|
| 106 |
+
f"Source {i+1}:\nTitle: {r.get('title')}\nURL: {r.get('url')}\nContent: {r.get('content')}"
|
| 107 |
+
for i, r in enumerate(final_sources)
|
| 108 |
+
])
|
| 109 |
+
else:
|
| 110 |
+
context_text = "No external sources used. Answering from internal knowledge."
|
| 111 |
+
|
| 112 |
+
# --- PHASE 4: SYNTHESIS ---
|
| 113 |
+
# Modify prompt based on intent? For now, standard synthesis but context aware.
|
| 114 |
+
answer = await llm_service.synthesize_answer(user_query, context_text)
|
| 115 |
+
|
| 116 |
+
# --- PHASE 5: VERIFICATION ---
|
| 117 |
+
confidence_level = "Medium"
|
| 118 |
+
if intent.category == "SEARCH_REQUIRED":
|
| 119 |
+
confidence_assessment = await llm_service.verify_confidence(answer, context_text)
|
| 120 |
+
if "High confidence" in confidence_assessment: confidence_level = "High"
|
| 121 |
+
elif "Low confidence" in confidence_assessment: confidence_level = "Low"
|
| 122 |
+
else:
|
| 123 |
+
confidence_level = "N/A (Chat)"
|
| 124 |
+
|
| 125 |
+
# Construct Response
|
| 126 |
+
formatted_sources = [
|
| 127 |
+
Source(title=r.get('title', 'Unknown'), url=r.get('url', '#'), snippet=r.get('content', '')[:200])
|
| 128 |
+
for r in (search_results if search_results else [])
|
| 129 |
+
]
|
| 130 |
+
|
| 131 |
+
return QueryResponse(
|
| 132 |
+
answer=answer,
|
| 133 |
+
sources=formatted_sources,
|
| 134 |
+
confidence=confidence_level,
|
| 135 |
+
search_queries=[user_query],
|
| 136 |
+
intent=intent.category,
|
| 137 |
+
thought_process=f"Intent: {intent.category}. Reasoning: {intent.reasoning}"
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
except Exception as e:
|
| 141 |
+
print(f"Error processing query: {e}")
|
| 142 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 143 |
+
|
| 144 |
+
@router.post("/challenge", response_model=QueryResponse)
|
| 145 |
+
async def challenge_answer(request: ChallengeRequest):
|
| 146 |
+
"""
|
| 147 |
+
'Disagree-with-Me' Mode: Critiques the previous answer.
|
| 148 |
+
"""
|
| 149 |
+
try:
|
| 150 |
+
from app.core import prompts
|
| 151 |
+
# Construct the critique prompt
|
| 152 |
+
messages = [
|
| 153 |
+
{"role": "system", "content": prompts.MASTER_PROMPT_CHALLENGE},
|
| 154 |
+
{"role": "user", "content": f"Query: {request.original_query}\n\nAnswer to critique: {request.original_answer}\n\nSources used:\n{request.sources_text}"}
|
| 155 |
+
]
|
| 156 |
+
|
| 157 |
+
critique = await llm_service._generate(messages, temperature=0.7)
|
| 158 |
+
|
| 159 |
+
# Return as a new message, but marked as a critique
|
| 160 |
+
return QueryResponse(
|
| 161 |
+
answer=critique,
|
| 162 |
+
sources=[],
|
| 163 |
+
confidence="High (Critique)",
|
| 164 |
+
search_queries=[],
|
| 165 |
+
intent="CRITIQUE",
|
| 166 |
+
thought_process="Devil's Advocate Mode Activated."
|
| 167 |
+
)
|
| 168 |
+
except Exception as e:
|
| 169 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 170 |
+
|
| 171 |
+
@router.post("/upload")
|
| 172 |
+
async def upload_file(file: UploadFile = File(...)):
|
| 173 |
+
"""
|
| 174 |
+
Parses an uploaded file and returns its text content for RAG.
|
| 175 |
+
"""
|
| 176 |
+
try:
|
| 177 |
+
filename = file.filename
|
| 178 |
+
print(f"📂 Processing file: {filename}")
|
| 179 |
+
content = await file_service.process_file(file)
|
| 180 |
+
return {"filename": filename, "content": content}
|
| 181 |
+
except Exception as e:
|
| 182 |
+
raise HTTPException(status_code=500, detail=f"Upload failed: {str(e)}")
|
app/core/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (1.26 kB). View file
|
|
|
app/core/__pycache__/prompts.cpython-311.pyc
ADDED
|
Binary file (4.07 kB). View file
|
|
|
app/core/config.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings
|
| 2 |
+
|
| 3 |
+
class Settings(BaseSettings):
|
| 4 |
+
PROJECT_NAME: str = "Trust-First Copilot"
|
| 5 |
+
API_V1_STR: str = "/api/v1"
|
| 6 |
+
|
| 7 |
+
# LLM Provider (OpenAI/Anthropic/Azure)
|
| 8 |
+
OPENAI_API_KEY: str = ""
|
| 9 |
+
|
| 10 |
+
# Search Provider (Tavily/Serper)
|
| 11 |
+
TAVILY_API_KEY: str = ""
|
| 12 |
+
|
| 13 |
+
# Hugging Face Provider
|
| 14 |
+
HUGGINGFACE_API_KEY: str = ""
|
| 15 |
+
|
| 16 |
+
# Groq Provider
|
| 17 |
+
GROQ_API_KEY: str = ""
|
| 18 |
+
|
| 19 |
+
# Gemini Provider
|
| 20 |
+
GEMINI_API_KEY: str = ""
|
| 21 |
+
|
| 22 |
+
# CORS
|
| 23 |
+
BACKEND_CORS_ORIGINS: list[str] = ["http://localhost:3000"]
|
| 24 |
+
|
| 25 |
+
class Config:
|
| 26 |
+
env_file = ".env"
|
| 27 |
+
|
| 28 |
+
settings = Settings()
|
app/core/prompts.py
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Master Prompts for Trust-First Copilot
|
| 2 |
+
# These prompts MUST be used verbatim to ensure the "Trust Engine" behavior.
|
| 3 |
+
|
| 4 |
+
SYSTEM_PROMPT = """
|
| 5 |
+
You are NOT a chatbot.
|
| 6 |
+
|
| 7 |
+
You are a verification-first, search-grounded AI system similar to Perplexity.
|
| 8 |
+
|
| 9 |
+
Your job is NOT to guess.
|
| 10 |
+
Your job is to:
|
| 11 |
+
- search
|
| 12 |
+
- retrieve
|
| 13 |
+
- verify
|
| 14 |
+
- summarize
|
| 15 |
+
- cite
|
| 16 |
+
|
| 17 |
+
STRICT RULES:
|
| 18 |
+
1. If you do not find reliable sources, you MUST say "I don't have enough data to answer this."
|
| 19 |
+
2. Every factual claim MUST be backed by a source.
|
| 20 |
+
3. No source = no answer.
|
| 21 |
+
4. Estimates must clearly state assumptions.
|
| 22 |
+
5. Confidence > verbosity.
|
| 23 |
+
6. Refusal is better than hallucination.
|
| 24 |
+
|
| 25 |
+
You are building user trust, not impressing users.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
MASTER_PROMPT_SEARCH = """
|
| 29 |
+
You are a search and retrieval engine.
|
| 30 |
+
|
| 31 |
+
TASK:
|
| 32 |
+
Given a user question, do the following:
|
| 33 |
+
|
| 34 |
+
1. Break the question into searchable sub-queries
|
| 35 |
+
2. Identify what kind of sources are needed (reports, papers, govt data, official websites)
|
| 36 |
+
3. Retrieve ONLY high-quality, reliable sources
|
| 37 |
+
4. Reject blogs, opinions, or unclear sources
|
| 38 |
+
5. List the sources with:
|
| 39 |
+
- Source name
|
| 40 |
+
- Year
|
| 41 |
+
- Authority (who published it)
|
| 42 |
+
|
| 43 |
+
DO NOT answer the question yet.
|
| 44 |
+
ONLY return the list of sources and why they are relevant.
|
| 45 |
+
"""
|
| 46 |
+
|
| 47 |
+
MASTER_PROMPT_ANSWER = """
|
| 48 |
+
You are an answer synthesis engine in 'Truth-First' mode.
|
| 49 |
+
|
| 50 |
+
INPUT:
|
| 51 |
+
- User question
|
| 52 |
+
- Retrieved sources (from previous step)
|
| 53 |
+
|
| 54 |
+
RULES:
|
| 55 |
+
1. Answer ONLY using the provided sources.
|
| 56 |
+
2. Separate FACTS from ASSUMPTIONS clearly.
|
| 57 |
+
3. If information is missing, list it under 'Unknowns'.
|
| 58 |
+
|
| 59 |
+
FORMAT:
|
| 60 |
+
|
| 61 |
+
### ✅ Verified Facts
|
| 62 |
+
(List only what is explicitly supported by sources)
|
| 63 |
+
|
| 64 |
+
### ⚠️ Assumptions & Risks
|
| 65 |
+
(List any logical leaps, estimates, or potential inaccuracies)
|
| 66 |
+
|
| 67 |
+
### ❌ Unknowns / Limitations
|
| 68 |
+
(List what the sources did NOT cover)
|
| 69 |
+
|
| 70 |
+
### 💡 Synthesis
|
| 71 |
+
(Your cohesive answer. Use the following structure to make it engaging and clear:)
|
| 72 |
+
|
| 73 |
+
[Direct Answer: 1-2 sentences summarizing the core response]
|
| 74 |
+
|
| 75 |
+
### **📊 Key Details**
|
| 76 |
+
(Use bullet points. **Bold** the key value or term in each bullet. e.g., "- **Population:** 1.4B")
|
| 77 |
+
|
| 78 |
+
### **🧑🤝🧑 Demographic / Structural Breakdown**
|
| 79 |
+
(If applicable, break down by category. **Bold** category names.)
|
| 80 |
+
|
| 81 |
+
### **🌍 Why This Matters**
|
| 82 |
+
(Explain the significance. **Bold** the main implication.)
|
| 83 |
+
|
| 84 |
+
### **⚠️ Considerations**
|
| 85 |
+
(Briefly mention major challenges. **Bold** the core challenge.)
|
| 86 |
+
|
| 87 |
+
Tone:
|
| 88 |
+
Neutral, factual, professional. **Use bolding liberally for emphasis and scannability.**
|
| 89 |
+
"""
|
| 90 |
+
|
| 91 |
+
MASTER_PROMPT_CHALLENGE = """
|
| 92 |
+
You are a 'Devil's Advocate' Engine.
|
| 93 |
+
|
| 94 |
+
Your task is to CRITIQUE the following answer.
|
| 95 |
+
Don't be polite. Be rigorous.
|
| 96 |
+
|
| 97 |
+
Identify:
|
| 98 |
+
1. Logical fallacies.
|
| 99 |
+
2. Unverified assumptions.
|
| 100 |
+
3. Alternative explanations that were ignored.
|
| 101 |
+
|
| 102 |
+
Output format:
|
| 103 |
+
"Here is why this answer might be wrong..."
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
MASTER_PROMPT_CITATION = """
|
| 107 |
+
You are a citation and transparency engine.
|
| 108 |
+
|
| 109 |
+
TASK:
|
| 110 |
+
For the generated answer:
|
| 111 |
+
1. Map each factual statement to its exact source
|
| 112 |
+
2. Display citations inline or as numbered references
|
| 113 |
+
3. Clearly separate:
|
| 114 |
+
- Facts
|
| 115 |
+
- Estimates
|
| 116 |
+
- Assumptions
|
| 117 |
+
|
| 118 |
+
OUTPUT MUST INCLUDE:
|
| 119 |
+
- Sources section
|
| 120 |
+
- What is known with high confidence
|
| 121 |
+
- What is uncertain or estimated
|
| 122 |
+
"""
|
| 123 |
+
|
| 124 |
+
MASTER_PROMPT_CONFIDENCE = """
|
| 125 |
+
You are a confidence evaluation system.
|
| 126 |
+
|
| 127 |
+
Evaluate the answer and assign:
|
| 128 |
+
- High confidence
|
| 129 |
+
- Medium confidence
|
| 130 |
+
- Low confidence
|
| 131 |
+
|
| 132 |
+
RULES:
|
| 133 |
+
- High: Fully supported by multiple reliable sources
|
| 134 |
+
- Medium: Partially supported or indirect evidence
|
| 135 |
+
- Low: Weak or missing evidence → REFUSE answer
|
| 136 |
+
|
| 137 |
+
If refusing:
|
| 138 |
+
- Clearly explain why
|
| 139 |
+
- Suggest what data would be needed to answer
|
| 140 |
+
|
| 141 |
+
Never bluff.
|
| 142 |
+
Never overstate.
|
| 143 |
+
"""
|
| 144 |
+
|
| 145 |
+
MASTER_PROMPT_FORMAT = """
|
| 146 |
+
Present the final answer in a website-friendly format.
|
| 147 |
+
|
| 148 |
+
SECTIONS:
|
| 149 |
+
1. Direct Answer (1–2 lines)
|
| 150 |
+
2. Explanation
|
| 151 |
+
3. Sources (clickable)
|
| 152 |
+
4. Confidence Level
|
| 153 |
+
5. Assumptions / Limitations
|
| 154 |
+
6. Shareable Summary (optional)
|
| 155 |
+
|
| 156 |
+
DESIGN PRINCIPLES:
|
| 157 |
+
- Clean
|
| 158 |
+
- Minimal
|
| 159 |
+
- Search-first
|
| 160 |
+
- Trust-focused
|
| 161 |
+
|
| 162 |
+
Avoid chatty language.
|
| 163 |
+
Avoid emojis.
|
| 164 |
+
"""
|
app/services/__pycache__/files.cpython-311.pyc
ADDED
|
Binary file (4.04 kB). View file
|
|
|
app/services/__pycache__/intent.cpython-311.pyc
ADDED
|
Binary file (3.54 kB). View file
|
|
|
app/services/__pycache__/llm.cpython-311.pyc
ADDED
|
Binary file (13 kB). View file
|
|
|
app/services/__pycache__/search.cpython-311.pyc
ADDED
|
Binary file (3.04 kB). View file
|
|
|
app/services/__pycache__/vector.cpython-311.pyc
ADDED
|
Binary file (4.43 kB). View file
|
|
|
app/services/files.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fitz # PyMuPDF
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from fastapi import UploadFile
|
| 4 |
+
import io
|
| 5 |
+
|
| 6 |
+
class FileService:
|
| 7 |
+
async def process_file(self, file: UploadFile) -> str:
|
| 8 |
+
content_type = file.content_type
|
| 9 |
+
filename = file.filename.lower()
|
| 10 |
+
|
| 11 |
+
content = await file.read()
|
| 12 |
+
extracted_text = ""
|
| 13 |
+
|
| 14 |
+
try:
|
| 15 |
+
# PDF Processing
|
| 16 |
+
if "pdf" in content_type or filename.endswith(".pdf"):
|
| 17 |
+
extracted_text = self._process_pdf(content)
|
| 18 |
+
|
| 19 |
+
# Excel/CSV Processing
|
| 20 |
+
elif "excel" in content_type or "spreadsheet" in content_type or filename.endswith((".xlsx", ".xls", ".csv")):
|
| 21 |
+
extracted_text = self._process_spreadsheet(content, filename)
|
| 22 |
+
|
| 23 |
+
# Text Processing
|
| 24 |
+
elif "text" in content_type or filename.endswith(".txt") or filename.endswith(".md"):
|
| 25 |
+
extracted_text = content.decode("utf-8")
|
| 26 |
+
|
| 27 |
+
else:
|
| 28 |
+
return f"Unsupported file type: {filename}"
|
| 29 |
+
|
| 30 |
+
return extracted_text
|
| 31 |
+
|
| 32 |
+
except Exception as e:
|
| 33 |
+
return f"Error processing file {filename}: {str(e)}"
|
| 34 |
+
|
| 35 |
+
def _process_pdf(self, content: bytes) -> str:
|
| 36 |
+
doc = fitz.open(stream=content, filetype="pdf")
|
| 37 |
+
text_blocks = []
|
| 38 |
+
|
| 39 |
+
for page_num, page in enumerate(doc):
|
| 40 |
+
text = page.get_text()
|
| 41 |
+
if text.strip():
|
| 42 |
+
# Add page marker for citation
|
| 43 |
+
text_blocks.append(f"--- Page {page_num + 1} ---\n{text}")
|
| 44 |
+
|
| 45 |
+
return "\n\n".join(text_blocks)
|
| 46 |
+
|
| 47 |
+
def _process_spreadsheet(self, content: bytes, filename: str) -> str:
|
| 48 |
+
text_blocks = []
|
| 49 |
+
try:
|
| 50 |
+
if filename.endswith(".csv"):
|
| 51 |
+
df = pd.read_csv(io.BytesIO(content))
|
| 52 |
+
text_blocks.append(f"--- CSV Data ---\n{df.to_markdown(index=False)}")
|
| 53 |
+
else:
|
| 54 |
+
excel_file = pd.ExcelFile(io.BytesIO(content))
|
| 55 |
+
for sheet_name in excel_file.sheet_names:
|
| 56 |
+
df = pd.read_excel(excel_file, sheet_name=sheet_name)
|
| 57 |
+
text_blocks.append(f"--- Sheet: {sheet_name} ---\n{df.to_markdown(index=False)}")
|
| 58 |
+
except Exception as e:
|
| 59 |
+
return f"Error parsing spreadsheet: {e}"
|
| 60 |
+
|
| 61 |
+
return "\n\n".join(text_blocks)
|
| 62 |
+
|
| 63 |
+
file_service = FileService()
|
app/services/intent.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Literal
|
| 2 |
+
from pydantic import BaseModel
|
| 3 |
+
from app.services.llm import LLMService
|
| 4 |
+
|
| 5 |
+
class IntentResponse(BaseModel):
|
| 6 |
+
category: Literal["SEARCH_REQUIRED", "CHAT_ONLY", "CODING_TASK", "DATA_ANALYSIS"]
|
| 7 |
+
reasoning: str
|
| 8 |
+
risk_level: Literal["LOW", "MEDIUM", "HIGH"]
|
| 9 |
+
|
| 10 |
+
class IntentService:
|
| 11 |
+
def __init__(self, llm_service: LLMService):
|
| 12 |
+
self.llm = llm_service
|
| 13 |
+
self.system_prompt = """
|
| 14 |
+
You are the 'Intent Analyzer' for an AI Operating System.
|
| 15 |
+
Your job is to route the user's request to the correct module.
|
| 16 |
+
|
| 17 |
+
Analyze the USER QUERY and return a JSON object.
|
| 18 |
+
|
| 19 |
+
CATEGORIES:
|
| 20 |
+
- SEARCH_REQUIRED: Query asks for current events, news, specific facts not in general knowledge, or research. (e.g., "Stock price of Apple", "Latest AI papers")
|
| 21 |
+
- CHAT_ONLY: General greetings, philosophical questions, summaries of previous context, or logic puzzles. (e.g., "Hi", "Explain Stoicism")
|
| 22 |
+
- CODING_TASK: Requests to write, debug, or explain code.
|
| 23 |
+
- DATA_ANALYSIS: Requests involving CSVs, charts, or math aggregations.
|
| 24 |
+
|
| 25 |
+
RISK LEVELS:
|
| 26 |
+
- HIGH: Asking for dangerous/illegal content, PII, or financial advice.
|
| 27 |
+
- MEDIUM: Ambiguous queries or potential controversies.
|
| 28 |
+
- LOW: Safe, standard queries.
|
| 29 |
+
|
| 30 |
+
Output format: {"category": "...", "reasoning": "...", "risk_level": "..."}
|
| 31 |
+
"""
|
| 32 |
+
|
| 33 |
+
async def analyze(self, query: str) -> IntentResponse:
|
| 34 |
+
# For efficiency, we can use a faster/smaller model or just the main one with strictly low temp
|
| 35 |
+
prompt = f"{self.system_prompt}\n\nUSER QUERY: {query}"
|
| 36 |
+
|
| 37 |
+
# We'll rely on the main LLM to parse this for now.
|
| 38 |
+
# ideally this uses a 'router' model (cheap/fast)
|
| 39 |
+
response_text = await self.llm._generate(
|
| 40 |
+
messages=[{"role": "user", "content": prompt}],
|
| 41 |
+
temperature=0.0
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
# Simple parsing logic (robustness would require structured output mode or regex)
|
| 45 |
+
# Assuming LLM behaves well with JSON instructions.
|
| 46 |
+
import json
|
| 47 |
+
import re
|
| 48 |
+
|
| 49 |
+
try:
|
| 50 |
+
# Clean markdown code blocks if present
|
| 51 |
+
clean_text = re.sub(r"```json|```", "", response_text).strip()
|
| 52 |
+
data = json.loads(clean_text)
|
| 53 |
+
return IntentResponse(**data)
|
| 54 |
+
except Exception as e:
|
| 55 |
+
# Fallback to Safe Default
|
| 56 |
+
print(f"Intent Parsing Failed: {e}. Defaulting to SEARCH.")
|
| 57 |
+
return IntentResponse(
|
| 58 |
+
category="SEARCH_REQUIRED",
|
| 59 |
+
reasoning="Parsing error, defaulting to search.",
|
| 60 |
+
risk_level="low"
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
intent_service = None # initialized in main/deps
|
app/services/llm.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from openai import AsyncOpenAI
|
| 2 |
+
from app.core.config import settings
|
| 3 |
+
from app.core import prompts
|
| 4 |
+
import asyncio
|
| 5 |
+
|
| 6 |
+
# Optional Hugging Face Client
|
| 7 |
+
try:
|
| 8 |
+
from huggingface_hub import AsyncInferenceClient
|
| 9 |
+
except ImportError:
|
| 10 |
+
AsyncInferenceClient = None
|
| 11 |
+
|
| 12 |
+
# Optional Groq Client
|
| 13 |
+
try:
|
| 14 |
+
from groq import AsyncGroq
|
| 15 |
+
except ImportError:
|
| 16 |
+
AsyncGroq = None
|
| 17 |
+
|
| 18 |
+
# Optional Gemini Client
|
| 19 |
+
try:
|
| 20 |
+
import google.generativeai as genai
|
| 21 |
+
except ImportError:
|
| 22 |
+
genai = None
|
| 23 |
+
|
| 24 |
+
class LLMService:
|
| 25 |
+
def __init__(self):
|
| 26 |
+
self.openai_key = settings.OPENAI_API_KEY
|
| 27 |
+
self.hf_key = settings.HUGGINGFACE_API_KEY
|
| 28 |
+
self.groq_key = settings.GROQ_API_KEY
|
| 29 |
+
self.gemini_key = settings.GEMINI_API_KEY
|
| 30 |
+
|
| 31 |
+
with open("debug.log", "a") as f:
|
| 32 |
+
f.write(f"\n--- INIT ---\n")
|
| 33 |
+
f.write(f"OpenAI Key: {bool(self.openai_key)}\n")
|
| 34 |
+
f.write(f"HF Key: {self.hf_key[:5] if self.hf_key else 'None'}\n")
|
| 35 |
+
f.write(f"Groq Key: {self.groq_key[:5] if self.groq_key else 'None'}\n")
|
| 36 |
+
f.write(f"Gemini Key: {self.gemini_key[:5] if self.gemini_key else 'None'}\n")
|
| 37 |
+
|
| 38 |
+
self.provider = "mock"
|
| 39 |
+
self.client = None
|
| 40 |
+
self.model = "gpt-4-turbo-preview"
|
| 41 |
+
|
| 42 |
+
# 1. Prefer Groq (User Requested - Fastest)
|
| 43 |
+
if self.groq_key and "gsk_" in self.groq_key and AsyncGroq:
|
| 44 |
+
self.provider = "groq"
|
| 45 |
+
self.client = AsyncGroq(api_key=self.groq_key)
|
| 46 |
+
self.model = "llama-3.3-70b-versatile"
|
| 47 |
+
print(f"LLM Provider: Groq ({self.model})")
|
| 48 |
+
with open("debug.log", "a") as f: f.write(f"Provider Selected: Groq ({self.model})\n")
|
| 49 |
+
|
| 50 |
+
# 2. Fallback to Gemini
|
| 51 |
+
elif self.gemini_key and genai:
|
| 52 |
+
self.provider = "gemini"
|
| 53 |
+
genai.configure(api_key=self.gemini_key)
|
| 54 |
+
|
| 55 |
+
# Dynamically find a valid model
|
| 56 |
+
valid_model = "gemini-1.5-flash"
|
| 57 |
+
try:
|
| 58 |
+
available_models = [m.name for m in genai.list_models() if 'generateContent' in m.supported_generation_methods]
|
| 59 |
+
print(f"Available Gemini Models: {available_models}")
|
| 60 |
+
|
| 61 |
+
# Rigid preference list
|
| 62 |
+
preferences = [
|
| 63 |
+
"models/gemini-1.5-flash-latest",
|
| 64 |
+
"models/gemini-1.5-flash-001",
|
| 65 |
+
"models/gemini-2.0-flash-exp",
|
| 66 |
+
"models/gemini-1.5-pro-latest",
|
| 67 |
+
]
|
| 68 |
+
|
| 69 |
+
for pref in preferences:
|
| 70 |
+
if pref in available_models:
|
| 71 |
+
valid_model = pref
|
| 72 |
+
break
|
| 73 |
+
# specific check for partial match if exact not found?
|
| 74 |
+
# sticking to exact or known good partials to avoid experimental/paid models
|
| 75 |
+
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"Error listing Gemini models: {e}")
|
| 78 |
+
|
| 79 |
+
# self.client = genai.GenerativeModel(valid_model) # Do not bind static client
|
| 80 |
+
self.model = "auto-retry-gemini"
|
| 81 |
+
print(f"LLM Provider: Gemini (Auto-Retry Mode)")
|
| 82 |
+
with open("debug.log", "a") as f: f.write(f"Provider Selected: Gemini (Auto-Retry)\n")
|
| 83 |
+
|
| 84 |
+
# 3. Fallback logic removed/moved up
|
| 85 |
+
elif False:
|
| 86 |
+
pass
|
| 87 |
+
# print(f"LLM Provider: Groq ({self.model})")
|
| 88 |
+
|
| 89 |
+
# 2. Fallback to Hugging Face
|
| 90 |
+
elif self.hf_key and "hf_" in self.hf_key and AsyncInferenceClient:
|
| 91 |
+
self.provider = "huggingface"
|
| 92 |
+
self.client = AsyncInferenceClient(token=self.hf_key)
|
| 93 |
+
self.model = "HuggingFaceH4/zephyr-7b-beta"
|
| 94 |
+
print(f"LLM Provider: Hugging Face ({self.model})")
|
| 95 |
+
|
| 96 |
+
# 3. Fallback to OpenAI
|
| 97 |
+
elif self.openai_key and "sk-" in self.openai_key:
|
| 98 |
+
self.provider = "openai"
|
| 99 |
+
self.client = AsyncOpenAI(api_key=self.openai_key)
|
| 100 |
+
print("LLM Provider: OpenAI")
|
| 101 |
+
|
| 102 |
+
async def _generate(self, messages, temperature=0.7):
|
| 103 |
+
"""Helper to unify generation calls across providers"""
|
| 104 |
+
if self.provider == "openai":
|
| 105 |
+
response = await self.client.chat.completions.create(
|
| 106 |
+
model=self.model,
|
| 107 |
+
messages=messages,
|
| 108 |
+
temperature=temperature
|
| 109 |
+
)
|
| 110 |
+
return response.choices[0].message.content
|
| 111 |
+
|
| 112 |
+
elif self.provider == "gemini":
|
| 113 |
+
# Dynamic Retry for Gemini
|
| 114 |
+
fallback_models = [
|
| 115 |
+
"models/gemini-1.5-flash",
|
| 116 |
+
"models/gemini-1.5-flash-latest",
|
| 117 |
+
"models/gemini-1.5-flash-001",
|
| 118 |
+
"models/gemini-2.0-flash-exp",
|
| 119 |
+
"models/gemini-2.0-flash",
|
| 120 |
+
"models/gemini-1.5-pro",
|
| 121 |
+
"models/gemini-1.5-pro-latest",
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
full_prompt = ""
|
| 125 |
+
for m in messages:
|
| 126 |
+
role_prefix = "System" if m['role'] == "system" else "User"
|
| 127 |
+
full_prompt += f"{role_prefix}: {m['content']}\n\n"
|
| 128 |
+
|
| 129 |
+
last_error = None
|
| 130 |
+
for model_name in fallback_models:
|
| 131 |
+
try:
|
| 132 |
+
# Create client on the fly for each model
|
| 133 |
+
client = genai.GenerativeModel(model_name)
|
| 134 |
+
response = await client.generate_content_async(
|
| 135 |
+
full_prompt,
|
| 136 |
+
generation_config=genai.types.GenerationConfig(
|
| 137 |
+
temperature=temperature
|
| 138 |
+
)
|
| 139 |
+
)
|
| 140 |
+
return response.text
|
| 141 |
+
except Exception as e:
|
| 142 |
+
print(f"Gemini Model {model_name} failed: {e}")
|
| 143 |
+
last_error = e
|
| 144 |
+
|
| 145 |
+
# If all fail, raise the last error to be caught by the caller
|
| 146 |
+
raise last_error
|
| 147 |
+
|
| 148 |
+
elif self.provider == "groq":
|
| 149 |
+
try:
|
| 150 |
+
response = await self.client.chat.completions.create(
|
| 151 |
+
model=self.model,
|
| 152 |
+
messages=messages,
|
| 153 |
+
temperature=temperature
|
| 154 |
+
)
|
| 155 |
+
return response.choices[0].message.content
|
| 156 |
+
except Exception as e:
|
| 157 |
+
print(f"Groq Error: {e}")
|
| 158 |
+
with open("debug.log", "a") as f: f.write(f"Groq Error: {e}\n")
|
| 159 |
+
raise e
|
| 160 |
+
|
| 161 |
+
elif self.provider == "huggingface":
|
| 162 |
+
# HF AsyncInferenceClient also supports chat_completion style
|
| 163 |
+
response = await self.client.chat_completion(
|
| 164 |
+
model=self.model,
|
| 165 |
+
messages=messages,
|
| 166 |
+
temperature=temperature,
|
| 167 |
+
max_tokens=1000
|
| 168 |
+
)
|
| 169 |
+
return response.choices[0].message.content
|
| 170 |
+
|
| 171 |
+
return None
|
| 172 |
+
|
| 173 |
+
async def get_search_queries(self, user_query: str) -> str:
|
| 174 |
+
if self.provider == "mock":
|
| 175 |
+
return f"Search Query: {user_query}"
|
| 176 |
+
|
| 177 |
+
try:
|
| 178 |
+
messages = [
|
| 179 |
+
{"role": "system", "content": prompts.SYSTEM_PROMPT},
|
| 180 |
+
{"role": "system", "content": prompts.MASTER_PROMPT_SEARCH},
|
| 181 |
+
{"role": "user", "content": user_query}
|
| 182 |
+
]
|
| 183 |
+
content = await self._generate(messages, temperature=0.2)
|
| 184 |
+
return content if content else user_query
|
| 185 |
+
except Exception as e:
|
| 186 |
+
with open("debug.log", "a") as f: f.write(f"LLM Error (Search): {e}\n")
|
| 187 |
+
return user_query
|
| 188 |
+
|
| 189 |
+
async def synthesize_answer(self, user_query: str, sources_text: str) -> str:
|
| 190 |
+
if self.provider == "mock":
|
| 191 |
+
await asyncio.sleep(1)
|
| 192 |
+
return self._mock_synthesize(user_query, sources_text)
|
| 193 |
+
|
| 194 |
+
try:
|
| 195 |
+
messages = [
|
| 196 |
+
{"role": "system", "content": prompts.SYSTEM_PROMPT},
|
| 197 |
+
{"role": "system", "content": prompts.MASTER_PROMPT_ANSWER},
|
| 198 |
+
{"role": "user", "content": f"User Question: {user_query}\n\nExisting Sources:\n{sources_text}"}
|
| 199 |
+
]
|
| 200 |
+
content = await self._generate(messages, temperature=0.1)
|
| 201 |
+
return content if content else self._mock_synthesize(user_query, sources_text)
|
| 202 |
+
except Exception as e:
|
| 203 |
+
with open("debug.log", "a") as f: f.write(f"LLM Error (Synthesis): {e}\n")
|
| 204 |
+
return self._mock_synthesize(user_query, sources_text)
|
| 205 |
+
|
| 206 |
+
async def verify_confidence(self, answer: str, sources_text: str) -> str:
|
| 207 |
+
if self.provider == "mock":
|
| 208 |
+
return "High confidence. (Mock Verification)"
|
| 209 |
+
|
| 210 |
+
try:
|
| 211 |
+
messages = [
|
| 212 |
+
{"role": "system", "content": prompts.MASTER_PROMPT_CONFIDENCE},
|
| 213 |
+
{"role": "user", "content": f"Answer to evaluate:\n{answer}\n\nBased on sources:\n{sources_text}"}
|
| 214 |
+
]
|
| 215 |
+
content = await self._generate(messages, temperature=0.1)
|
| 216 |
+
return content if content else "Medium confidence"
|
| 217 |
+
except Exception as e:
|
| 218 |
+
print(f"LLM Error (Confidence): {e}")
|
| 219 |
+
return "Medium confidence"
|
| 220 |
+
|
| 221 |
+
def _mock_synthesize(self, query: str, sources_text: str = "") -> str:
|
| 222 |
+
source_preview = "No sources found."
|
| 223 |
+
if sources_text:
|
| 224 |
+
lines = sources_text.split('\n')
|
| 225 |
+
titles = [line.replace("Title: ", "") for line in lines if line.startswith("Title: ")]
|
| 226 |
+
source_preview = f"Found {len(titles)} Sources including: " + ", ".join(titles[:3])
|
| 227 |
+
|
| 228 |
+
return f"""
|
| 229 |
+
**Note: Running in Partial Mode (Tavily Connected, No LLM Key).**
|
| 230 |
+
|
| 231 |
+
The system successfully searched using your **Tavily Key** and found real data:
|
| 232 |
+
_{source_preview}_
|
| 233 |
+
|
| 234 |
+
However, I cannot generate the answer.
|
| 235 |
+
Current Provider: **{self.provider}**
|
| 236 |
+
Check debug.log for specific error (e.g., 404 Model Not Found or Auth Error).
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
llm_service = LLMService()
|
| 240 |
+
|
app/services/search.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from typing import List, Dict, Any
|
| 3 |
+
from app.core.config import settings
|
| 4 |
+
|
| 5 |
+
# Placeholder for Tavily or similar search client
|
| 6 |
+
try:
|
| 7 |
+
from tavily import TavilyClient
|
| 8 |
+
except ImportError:
|
| 9 |
+
TavilyClient = None
|
| 10 |
+
|
| 11 |
+
class SearchService:
|
| 12 |
+
def __init__(self):
|
| 13 |
+
self.api_key = settings.TAVILY_API_KEY
|
| 14 |
+
self.client = None
|
| 15 |
+
if self.api_key and TavilyClient:
|
| 16 |
+
self.client = TavilyClient(api_key=self.api_key)
|
| 17 |
+
|
| 18 |
+
async def search(self, query: str, search_depth: str = "advanced") -> List[Dict[str, Any]]:
|
| 19 |
+
"""
|
| 20 |
+
Executes a search using the Master Prompt strategies (conceptually).
|
| 21 |
+
In reality, we send the query to a search API.
|
| 22 |
+
"""
|
| 23 |
+
print(f"Executing search for: {query}")
|
| 24 |
+
|
| 25 |
+
if not self.client:
|
| 26 |
+
# Mock response for testing without API Key
|
| 27 |
+
return self._mock_search_results(query)
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
# Tavily context search is optimized for RAG
|
| 31 |
+
response = self.client.search(query=query, search_depth=search_depth, include_domains=[])
|
| 32 |
+
return response.get("results", [])
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(f"Search failed: {e}")
|
| 35 |
+
return []
|
| 36 |
+
|
| 37 |
+
def _mock_search_results(self, query: str) -> List[Dict[str, Any]]:
|
| 38 |
+
return [
|
| 39 |
+
{
|
| 40 |
+
"title": f"Mock Source about {query}",
|
| 41 |
+
"url": "https://example.com/source1",
|
| 42 |
+
"content": f"This is a verified source containing specific data about {query}. It confirms the primary query intent.",
|
| 43 |
+
"score": 0.95,
|
| 44 |
+
"published_date": "2024-01-01"
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"title": f"Another Relevant Source for {query}",
|
| 48 |
+
"url": "https://gov.data/report",
|
| 49 |
+
"content": f"Official government data suggests that {query} has seen a 20% increase in importance.",
|
| 50 |
+
"score": 0.88,
|
| 51 |
+
"published_date": "2023-12-15"
|
| 52 |
+
}
|
| 53 |
+
]
|
| 54 |
+
|
| 55 |
+
search_service = SearchService()
|
app/services/vector.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
try:
|
| 3 |
+
import faiss
|
| 4 |
+
from sentence_transformers import SentenceTransformer
|
| 5 |
+
except Exception as e:
|
| 6 |
+
print(f"VectorService: Failed to import dependencies: {e}")
|
| 7 |
+
faiss = None
|
| 8 |
+
SentenceTransformer = None
|
| 9 |
+
|
| 10 |
+
class VectorService:
|
| 11 |
+
def __init__(self):
|
| 12 |
+
self.model = None
|
| 13 |
+
self.index = None
|
| 14 |
+
self.chunks = []
|
| 15 |
+
|
| 16 |
+
if SentenceTransformer:
|
| 17 |
+
# Load model once. This might be slow on startup.
|
| 18 |
+
print("Loading generic embedding model (all-MiniLM-L6-v2)...")
|
| 19 |
+
try:
|
| 20 |
+
self.model = SentenceTransformer("all-MiniLM-L6-v2")
|
| 21 |
+
print("Embedding model loaded successfully.")
|
| 22 |
+
except Exception as e:
|
| 23 |
+
print(f"Failed to load embedding model: {e}")
|
| 24 |
+
|
| 25 |
+
def create_index_from_results(self, results: list):
|
| 26 |
+
"""
|
| 27 |
+
Takes a list of search result dicts, creates embeddings, and builds a FAISS index.
|
| 28 |
+
"""
|
| 29 |
+
if not self.model or not faiss:
|
| 30 |
+
print("VectorService: Dependencies missing or model not loaded.")
|
| 31 |
+
return
|
| 32 |
+
|
| 33 |
+
self.chunks = []
|
| 34 |
+
texts_to_embed = []
|
| 35 |
+
|
| 36 |
+
for res in results:
|
| 37 |
+
# Combine Title and Content for a rich embedding context
|
| 38 |
+
text = f"Title: {res.get('title', '')}\nContent: {res.get('content', '')}"
|
| 39 |
+
self.chunks.append(res) # Keep reference to original object
|
| 40 |
+
texts_to_embed.append(text)
|
| 41 |
+
|
| 42 |
+
if not texts_to_embed:
|
| 43 |
+
return
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
embeddings = self.model.encode(texts_to_embed)
|
| 47 |
+
dimension = embeddings.shape[1]
|
| 48 |
+
|
| 49 |
+
self.index = faiss.IndexFlatL2(dimension)
|
| 50 |
+
self.index.add(np.array(embeddings))
|
| 51 |
+
print(f"VectorService: Created FAISS index with {self.index.ntotal} vectors")
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"VectorService Error during indexing: {e}")
|
| 54 |
+
|
| 55 |
+
def search_similar(self, query: str, k: int = 3):
|
| 56 |
+
"""
|
| 57 |
+
Searches the FAISS index for the most relevant chunks to the query.
|
| 58 |
+
"""
|
| 59 |
+
if not self.index or not self.model:
|
| 60 |
+
return []
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
query_emb = self.model.encode([query])
|
| 64 |
+
distances, indices = self.index.search(query_emb, k)
|
| 65 |
+
|
| 66 |
+
top_results = []
|
| 67 |
+
for idx in indices[0]:
|
| 68 |
+
if idx < len(self.chunks) and idx >= 0:
|
| 69 |
+
top_results.append(self.chunks[idx])
|
| 70 |
+
|
| 71 |
+
return top_results
|
| 72 |
+
except Exception as e:
|
| 73 |
+
print(f"VectorService Error during search: {e}")
|
| 74 |
+
return []
|
| 75 |
+
|
| 76 |
+
vector_service = VectorService()
|
backend/.env
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Trust-First Copilot Configuration
|
| 2 |
+
|
| 3 |
+
# OpenAI API Key (Optional if using Hugging Face)
|
| 4 |
+
OPENAI_API_KEY=
|
| 5 |
+
|
| 6 |
+
# Tavily API Key (Required for Search)
|
| 7 |
+
TAVILY_API_KEY=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
|
| 8 |
+
|
| 9 |
+
# Hugging Face API Key (Free Brain Power!)
|
| 10 |
+
HUGGINGFACE_API_KEY=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
|
| 11 |
+
|
| 12 |
+
# Groq API Key (Fastest Brain)
|
| 13 |
+
GROQ_API_KEY=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
|
| 14 |
+
PROJECT_NAME="Advance Copilot"
|
| 15 |
+
|
| 16 |
+
# Gemini API Key (Google)
|
| 17 |
+
GEMINI_API_KEY=AIzaSyBb27_DT8Py1m2KrctHNZuIcuyu75xWOfo
|
backend/__init__.py
ADDED
|
File without changes
|
backend/__pycache__/__init__.cpython-311.pyc
ADDED
|
Binary file (147 Bytes). View file
|
|
|
backend/debug.log
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
--- INIT ---
|
| 3 |
+
OpenAI Key: True
|
| 4 |
+
HF Key: hf_Js
|
| 5 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 6 |
+
|
| 7 |
+
--- INIT ---
|
| 8 |
+
OpenAI Key: True
|
| 9 |
+
HF Key: hf_Js
|
| 10 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 11 |
+
|
| 12 |
+
--- INIT ---
|
| 13 |
+
OpenAI Key: True
|
| 14 |
+
HF Key: hf_Js
|
| 15 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 16 |
+
|
| 17 |
+
--- INIT ---
|
| 18 |
+
OpenAI Key: True
|
| 19 |
+
HF Key: hf_Js
|
| 20 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 21 |
+
|
| 22 |
+
--- INIT ---
|
| 23 |
+
OpenAI Key: True
|
| 24 |
+
HF Key: hf_Js
|
| 25 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 26 |
+
|
| 27 |
+
--- INIT ---
|
| 28 |
+
OpenAI Key: True
|
| 29 |
+
HF Key: hf_Js
|
| 30 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 31 |
+
LLM Error (Synthesis): Error code: 404 - {'error': {'message': 'The model `gpt-4-turbo-preview` does not exist or you do not have access to it.', 'type': 'invalid_request_error', 'param': None, 'code': 'model_not_found'}}
|
| 32 |
+
LLM Error (Synthesis): Error code: 404 - {'error': {'message': 'The model `gpt-4-turbo-preview` does not exist or you do not have access to it.', 'type': 'invalid_request_error', 'param': None, 'code': 'model_not_found'}}
|
| 33 |
+
LLM Error (Synthesis): Error code: 404 - {'error': {'message': 'The model `gpt-4-turbo-preview` does not exist or you do not have access to it.', 'type': 'invalid_request_error', 'param': None, 'code': 'model_not_found'}}
|
| 34 |
+
LLM Error (Synthesis): Error code: 404 - {'error': {'message': 'The model `gpt-4-turbo-preview` does not exist or you do not have access to it.', 'type': 'invalid_request_error', 'param': None, 'code': 'model_not_found'}}
|
| 35 |
+
LLM Error (Synthesis): Error code: 404 - {'error': {'message': 'The model `gpt-4-turbo-preview` does not exist or you do not have access to it.', 'type': 'invalid_request_error', 'param': None, 'code': 'model_not_found'}}
|
| 36 |
+
|
| 37 |
+
--- INIT ---
|
| 38 |
+
OpenAI Key: True
|
| 39 |
+
HF Key: hf_Js
|
| 40 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 41 |
+
|
| 42 |
+
--- INIT ---
|
| 43 |
+
OpenAI Key: True
|
| 44 |
+
HF Key: hf_Js
|
| 45 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 46 |
+
|
| 47 |
+
--- INIT ---
|
| 48 |
+
OpenAI Key: True
|
| 49 |
+
HF Key: hf_Js
|
| 50 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 51 |
+
|
| 52 |
+
--- INIT ---
|
| 53 |
+
OpenAI Key: True
|
| 54 |
+
HF Key: hf_Js
|
| 55 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 56 |
+
|
| 57 |
+
--- INIT ---
|
| 58 |
+
OpenAI Key: True
|
| 59 |
+
HF Key: hf_Js
|
| 60 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 61 |
+
LLM Error (Synthesis): (Request ID: Root=1-696658c4-788fc3276bd8d2ba39479c5d;254be642-c222-4663-84d5-f8ba42f2ebbe)
|
| 62 |
+
|
| 63 |
+
403 Forbidden: This authentication method does not have sufficient permissions to call Inference Providers on behalf of user Abhi18singh.
|
| 64 |
+
Cannot access content at: https://router.huggingface.co/v1/chat/completions.
|
| 65 |
+
Make sure your token has the correct permissions.
|
| 66 |
+
LLM Error (Synthesis): (Request ID: Root=1-696658ef-5a3811376f5b88c367c230fc;3c7bda51-0042-48c4-9c9d-9cd4f4e4d6d2)
|
| 67 |
+
|
| 68 |
+
403 Forbidden: This authentication method does not have sufficient permissions to call Inference Providers on behalf of user Abhi18singh.
|
| 69 |
+
Cannot access content at: https://router.huggingface.co/v1/chat/completions.
|
| 70 |
+
Make sure your token has the correct permissions.
|
| 71 |
+
LLM Error (Synthesis): (Request ID: Root=1-69665998-7a2fa3962140a6b8222dd600;b74da982-0738-479c-9651-854274a658d2)
|
| 72 |
+
|
| 73 |
+
403 Forbidden: This authentication method does not have sufficient permissions to call Inference Providers on behalf of user Abhi18singh.
|
| 74 |
+
Cannot access content at: https://router.huggingface.co/v1/chat/completions.
|
| 75 |
+
Make sure your token has the correct permissions.
|
| 76 |
+
LLM Error (Synthesis): (Request ID: Root=1-69665a20-1573ab002b04c9556e6c0c59;fa51d8f9-7c0a-4211-aacd-105bec200581)
|
| 77 |
+
|
| 78 |
+
403 Forbidden: This authentication method does not have sufficient permissions to call Inference Providers on behalf of user Abhi18singh.
|
| 79 |
+
Cannot access content at: https://router.huggingface.co/v1/chat/completions.
|
| 80 |
+
Make sure your token has the correct permissions.
|
| 81 |
+
LLM Error (Synthesis): (Request ID: Root=1-69665a5e-0646812e1eec86ba4943610e;25d67883-df55-4879-869e-639ffc2a2eb8)
|
| 82 |
+
|
| 83 |
+
403 Forbidden: This authentication method does not have sufficient permissions to call Inference Providers on behalf of user Abhi18singh.
|
| 84 |
+
Cannot access content at: https://router.huggingface.co/v1/chat/completions.
|
| 85 |
+
Make sure your token has the correct permissions.
|
| 86 |
+
|
| 87 |
+
--- INIT ---
|
| 88 |
+
OpenAI Key: True
|
| 89 |
+
HF Key: hf_Js
|
| 90 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 91 |
+
|
| 92 |
+
--- INIT ---
|
| 93 |
+
OpenAI Key: True
|
| 94 |
+
HF Key: hf_Js
|
| 95 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 96 |
+
|
| 97 |
+
--- INIT ---
|
| 98 |
+
OpenAI Key: True
|
| 99 |
+
HF Key: hf_Js
|
| 100 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 101 |
+
|
| 102 |
+
--- INIT ---
|
| 103 |
+
OpenAI Key: True
|
| 104 |
+
HF Key: hf_Js
|
| 105 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 106 |
+
|
| 107 |
+
--- INIT ---
|
| 108 |
+
OpenAI Key: True
|
| 109 |
+
HF Key: hf_Js
|
| 110 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 111 |
+
|
| 112 |
+
--- INIT ---
|
| 113 |
+
OpenAI Key: True
|
| 114 |
+
HF Key: hf_Js
|
| 115 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 116 |
+
|
| 117 |
+
--- INIT ---
|
| 118 |
+
OpenAI Key: True
|
| 119 |
+
HF Key: hf_Js
|
| 120 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 121 |
+
|
| 122 |
+
--- INIT ---
|
| 123 |
+
OpenAI Key: True
|
| 124 |
+
HF Key: hf_Js
|
| 125 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 126 |
+
|
| 127 |
+
--- INIT ---
|
| 128 |
+
OpenAI Key: True
|
| 129 |
+
HF Key: hf_Js
|
| 130 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 131 |
+
|
| 132 |
+
--- INIT ---
|
| 133 |
+
OpenAI Key: True
|
| 134 |
+
HF Key: hf_Js
|
| 135 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 136 |
+
|
| 137 |
+
--- INIT ---
|
| 138 |
+
OpenAI Key: True
|
| 139 |
+
HF Key: hf_Js
|
| 140 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 141 |
+
|
| 142 |
+
--- INIT ---
|
| 143 |
+
OpenAI Key: True
|
| 144 |
+
HF Key: hf_Js
|
| 145 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 146 |
+
|
| 147 |
+
--- INIT ---
|
| 148 |
+
OpenAI Key: True
|
| 149 |
+
HF Key: hf_Js
|
| 150 |
+
AsyncInferenceClient: <class 'huggingface_hub.inference._generated._async_client.AsyncInferenceClient'>
|
| 151 |
+
|
| 152 |
+
--- INIT ---
|
| 153 |
+
OpenAI Key: True
|
| 154 |
+
HF Key: hf_Js
|
| 155 |
+
Groq Key: gsk_f
|
| 156 |
+
|
| 157 |
+
--- INIT ---
|
| 158 |
+
OpenAI Key: True
|
| 159 |
+
HF Key: hf_Js
|
| 160 |
+
Groq Key: gsk_f
|
| 161 |
+
|
| 162 |
+
--- INIT ---
|
| 163 |
+
OpenAI Key: True
|
| 164 |
+
HF Key: hf_Js
|
| 165 |
+
Groq Key: gsk_f
|
| 166 |
+
|
| 167 |
+
--- INIT ---
|
| 168 |
+
OpenAI Key: True
|
| 169 |
+
HF Key: hf_Js
|
| 170 |
+
Groq Key: gsk_f
|
| 171 |
+
|
| 172 |
+
--- INIT ---
|
| 173 |
+
OpenAI Key: True
|
| 174 |
+
HF Key: hf_Js
|
| 175 |
+
Groq Key: gsk_f
|
| 176 |
+
|
| 177 |
+
--- INIT ---
|
| 178 |
+
OpenAI Key: True
|
| 179 |
+
HF Key: hf_Js
|
| 180 |
+
Groq Key: gsk_f
|
| 181 |
+
|
| 182 |
+
--- INIT ---
|
| 183 |
+
OpenAI Key: True
|
| 184 |
+
HF Key: hf_Js
|
| 185 |
+
Groq Key: gsk_f
|
| 186 |
+
|
| 187 |
+
--- INIT ---
|
| 188 |
+
OpenAI Key: True
|
| 189 |
+
HF Key: hf_Js
|
| 190 |
+
Groq Key: gsk_f
|
| 191 |
+
|
| 192 |
+
--- INIT ---
|
| 193 |
+
OpenAI Key: True
|
| 194 |
+
HF Key: hf_Js
|
| 195 |
+
Groq Key: gsk_f
|
| 196 |
+
|
| 197 |
+
--- INIT ---
|
| 198 |
+
OpenAI Key: True
|
| 199 |
+
HF Key: hf_Js
|
| 200 |
+
Groq Key: gsk_f
|
| 201 |
+
LLM Error (Synthesis): Error code: 400 - {'error': {'message': 'The model `llama3-70b-8192` has been decommissioned and is no longer supported. Please refer to https://console.groq.com/docs/deprecations for a recommendation on which model to use instead.', 'type': 'invalid_request_error', 'code': 'model_decommissioned'}}
|
| 202 |
+
LLM Error (Synthesis): Error code: 400 - {'error': {'message': 'The model `llama3-70b-8192` has been decommissioned and is no longer supported. Please refer to https://console.groq.com/docs/deprecations for a recommendation on which model to use instead.', 'type': 'invalid_request_error', 'code': 'model_decommissioned'}}
|
| 203 |
+
LLM Error (Synthesis): Error code: 400 - {'error': {'message': 'The model `llama3-70b-8192` has been decommissioned and is no longer supported. Please refer to https://console.groq.com/docs/deprecations for a recommendation on which model to use instead.', 'type': 'invalid_request_error', 'code': 'model_decommissioned'}}
|
| 204 |
+
|
| 205 |
+
--- INIT ---
|
| 206 |
+
OpenAI Key: True
|
| 207 |
+
HF Key: hf_Js
|
| 208 |
+
Groq Key: gsk_f
|
| 209 |
+
Gemini Key: AIzaS
|
| 210 |
+
|
| 211 |
+
--- INIT ---
|
| 212 |
+
OpenAI Key: True
|
| 213 |
+
HF Key: hf_Js
|
| 214 |
+
Groq Key: gsk_f
|
| 215 |
+
Gemini Key: AIzaS
|
| 216 |
+
|
| 217 |
+
--- INIT ---
|
| 218 |
+
OpenAI Key: True
|
| 219 |
+
HF Key: hf_Js
|
| 220 |
+
Groq Key: gsk_f
|
| 221 |
+
Gemini Key: AIzaS
|
| 222 |
+
LLM Error (Synthesis): Error code: 400 - {'error': {'message': 'The model `llama3-70b-8192` has been decommissioned and is no longer supported. Please refer to https://console.groq.com/docs/deprecations for a recommendation on which model to use instead.', 'type': 'invalid_request_error', 'code': 'model_decommissioned'}}
|
| 223 |
+
|
| 224 |
+
--- INIT ---
|
| 225 |
+
OpenAI Key: True
|
| 226 |
+
HF Key: hf_Js
|
| 227 |
+
Groq Key: gsk_f
|
| 228 |
+
Gemini Key: AIzaS
|
| 229 |
+
LLM Error (Synthesis): 404 models/gemini-1.5-flash is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.
|
| 230 |
+
|
| 231 |
+
--- INIT ---
|
| 232 |
+
OpenAI Key: True
|
| 233 |
+
HF Key: hf_Js
|
| 234 |
+
Groq Key: gsk_f
|
| 235 |
+
Gemini Key: AIzaS
|
| 236 |
+
|
| 237 |
+
--- INIT ---
|
| 238 |
+
OpenAI Key: True
|
| 239 |
+
HF Key: hf_Js
|
| 240 |
+
Groq Key: gsk_f
|
| 241 |
+
Gemini Key: AIzaS
|
| 242 |
+
|
| 243 |
+
--- INIT ---
|
| 244 |
+
OpenAI Key: True
|
| 245 |
+
HF Key: hf_Js
|
| 246 |
+
Groq Key: gsk_f
|
| 247 |
+
Gemini Key: AIzaS
|
| 248 |
+
LLM Error (Synthesis): 429 You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit.
|
| 249 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-2.5-pro
|
| 250 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.5-pro
|
| 251 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.5-pro
|
| 252 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-2.5-pro
|
| 253 |
+
Please retry in 24.311506491s. [links {
|
| 254 |
+
description: "Learn more about Gemini API quotas"
|
| 255 |
+
url: "https://ai.google.dev/gemini-api/docs/rate-limits"
|
| 256 |
+
}
|
| 257 |
+
, violations {
|
| 258 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_input_token_count"
|
| 259 |
+
quota_id: "GenerateContentInputTokensPerModelPerMinute-FreeTier"
|
| 260 |
+
quota_dimensions {
|
| 261 |
+
key: "model"
|
| 262 |
+
value: "gemini-2.5-pro"
|
| 263 |
+
}
|
| 264 |
+
quota_dimensions {
|
| 265 |
+
key: "location"
|
| 266 |
+
value: "global"
|
| 267 |
+
}
|
| 268 |
+
}
|
| 269 |
+
violations {
|
| 270 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 271 |
+
quota_id: "GenerateRequestsPerMinutePerProjectPerModel-FreeTier"
|
| 272 |
+
quota_dimensions {
|
| 273 |
+
key: "model"
|
| 274 |
+
value: "gemini-2.5-pro"
|
| 275 |
+
}
|
| 276 |
+
quota_dimensions {
|
| 277 |
+
key: "location"
|
| 278 |
+
value: "global"
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
violations {
|
| 282 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 283 |
+
quota_id: "GenerateRequestsPerDayPerProjectPerModel-FreeTier"
|
| 284 |
+
quota_dimensions {
|
| 285 |
+
key: "model"
|
| 286 |
+
value: "gemini-2.5-pro"
|
| 287 |
+
}
|
| 288 |
+
quota_dimensions {
|
| 289 |
+
key: "location"
|
| 290 |
+
value: "global"
|
| 291 |
+
}
|
| 292 |
+
}
|
| 293 |
+
violations {
|
| 294 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_input_token_count"
|
| 295 |
+
quota_id: "GenerateContentInputTokensPerModelPerDay-FreeTier"
|
| 296 |
+
quota_dimensions {
|
| 297 |
+
key: "model"
|
| 298 |
+
value: "gemini-2.5-pro"
|
| 299 |
+
}
|
| 300 |
+
quota_dimensions {
|
| 301 |
+
key: "location"
|
| 302 |
+
value: "global"
|
| 303 |
+
}
|
| 304 |
+
}
|
| 305 |
+
, retry_delay {
|
| 306 |
+
seconds: 24
|
| 307 |
+
}
|
| 308 |
+
]
|
| 309 |
+
LLM Error (Synthesis): 429 You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit.
|
| 310 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-2.5-pro
|
| 311 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.5-pro
|
| 312 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.5-pro
|
| 313 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-2.5-pro
|
| 314 |
+
Please retry in 42.395475103s. [links {
|
| 315 |
+
description: "Learn more about Gemini API quotas"
|
| 316 |
+
url: "https://ai.google.dev/gemini-api/docs/rate-limits"
|
| 317 |
+
}
|
| 318 |
+
, violations {
|
| 319 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_input_token_count"
|
| 320 |
+
quota_id: "GenerateContentInputTokensPerModelPerDay-FreeTier"
|
| 321 |
+
quota_dimensions {
|
| 322 |
+
key: "model"
|
| 323 |
+
value: "gemini-2.5-pro"
|
| 324 |
+
}
|
| 325 |
+
quota_dimensions {
|
| 326 |
+
key: "location"
|
| 327 |
+
value: "global"
|
| 328 |
+
}
|
| 329 |
+
}
|
| 330 |
+
violations {
|
| 331 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 332 |
+
quota_id: "GenerateRequestsPerDayPerProjectPerModel-FreeTier"
|
| 333 |
+
quota_dimensions {
|
| 334 |
+
key: "model"
|
| 335 |
+
value: "gemini-2.5-pro"
|
| 336 |
+
}
|
| 337 |
+
quota_dimensions {
|
| 338 |
+
key: "location"
|
| 339 |
+
value: "global"
|
| 340 |
+
}
|
| 341 |
+
}
|
| 342 |
+
violations {
|
| 343 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 344 |
+
quota_id: "GenerateRequestsPerMinutePerProjectPerModel-FreeTier"
|
| 345 |
+
quota_dimensions {
|
| 346 |
+
key: "model"
|
| 347 |
+
value: "gemini-2.5-pro"
|
| 348 |
+
}
|
| 349 |
+
quota_dimensions {
|
| 350 |
+
key: "location"
|
| 351 |
+
value: "global"
|
| 352 |
+
}
|
| 353 |
+
}
|
| 354 |
+
violations {
|
| 355 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_input_token_count"
|
| 356 |
+
quota_id: "GenerateContentInputTokensPerModelPerMinute-FreeTier"
|
| 357 |
+
quota_dimensions {
|
| 358 |
+
key: "model"
|
| 359 |
+
value: "gemini-2.5-pro"
|
| 360 |
+
}
|
| 361 |
+
quota_dimensions {
|
| 362 |
+
key: "location"
|
| 363 |
+
value: "global"
|
| 364 |
+
}
|
| 365 |
+
}
|
| 366 |
+
, retry_delay {
|
| 367 |
+
seconds: 42
|
| 368 |
+
}
|
| 369 |
+
]
|
| 370 |
+
|
| 371 |
+
--- INIT ---
|
| 372 |
+
OpenAI Key: True
|
| 373 |
+
HF Key: hf_Js
|
| 374 |
+
Groq Key: gsk_f
|
| 375 |
+
Gemini Key: AIzaS
|
| 376 |
+
|
| 377 |
+
--- INIT ---
|
| 378 |
+
OpenAI Key: True
|
| 379 |
+
HF Key: hf_Js
|
| 380 |
+
Groq Key: gsk_f
|
| 381 |
+
Gemini Key: AIzaS
|
| 382 |
+
LLM Error (Synthesis): 404 models/gemini-1.5-flash is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.
|
| 383 |
+
LLM Error (Synthesis): 404 models/gemini-1.5-flash is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.
|
| 384 |
+
|
| 385 |
+
--- INIT ---
|
| 386 |
+
OpenAI Key: True
|
| 387 |
+
HF Key: hf_Js
|
| 388 |
+
Groq Key: gsk_f
|
| 389 |
+
Gemini Key: AIzaS
|
| 390 |
+
|
| 391 |
+
--- INIT ---
|
| 392 |
+
OpenAI Key: True
|
| 393 |
+
HF Key: hf_Js
|
| 394 |
+
Groq Key: gsk_f
|
| 395 |
+
Gemini Key: AIzaS
|
| 396 |
+
|
| 397 |
+
--- INIT ---
|
| 398 |
+
OpenAI Key: True
|
| 399 |
+
HF Key: hf_Js
|
| 400 |
+
Groq Key: gsk_f
|
| 401 |
+
Gemini Key: AIzaS
|
| 402 |
+
|
| 403 |
+
--- INIT ---
|
| 404 |
+
OpenAI Key: True
|
| 405 |
+
HF Key: hf_Js
|
| 406 |
+
Groq Key: gsk_f
|
| 407 |
+
Gemini Key: AIzaS
|
| 408 |
+
LLM Error (Synthesis): 429 You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit.
|
| 409 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-2.0-flash
|
| 410 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.0-flash
|
| 411 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.0-flash
|
| 412 |
+
Please retry in 50.635719043s. [links {
|
| 413 |
+
description: "Learn more about Gemini API quotas"
|
| 414 |
+
url: "https://ai.google.dev/gemini-api/docs/rate-limits"
|
| 415 |
+
}
|
| 416 |
+
, violations {
|
| 417 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_input_token_count"
|
| 418 |
+
quota_id: "GenerateContentInputTokensPerModelPerMinute-FreeTier"
|
| 419 |
+
quota_dimensions {
|
| 420 |
+
key: "model"
|
| 421 |
+
value: "gemini-2.0-flash"
|
| 422 |
+
}
|
| 423 |
+
quota_dimensions {
|
| 424 |
+
key: "location"
|
| 425 |
+
value: "global"
|
| 426 |
+
}
|
| 427 |
+
}
|
| 428 |
+
violations {
|
| 429 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 430 |
+
quota_id: "GenerateRequestsPerMinutePerProjectPerModel-FreeTier"
|
| 431 |
+
quota_dimensions {
|
| 432 |
+
key: "model"
|
| 433 |
+
value: "gemini-2.0-flash"
|
| 434 |
+
}
|
| 435 |
+
quota_dimensions {
|
| 436 |
+
key: "location"
|
| 437 |
+
value: "global"
|
| 438 |
+
}
|
| 439 |
+
}
|
| 440 |
+
violations {
|
| 441 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 442 |
+
quota_id: "GenerateRequestsPerDayPerProjectPerModel-FreeTier"
|
| 443 |
+
quota_dimensions {
|
| 444 |
+
key: "model"
|
| 445 |
+
value: "gemini-2.0-flash"
|
| 446 |
+
}
|
| 447 |
+
quota_dimensions {
|
| 448 |
+
key: "location"
|
| 449 |
+
value: "global"
|
| 450 |
+
}
|
| 451 |
+
}
|
| 452 |
+
, retry_delay {
|
| 453 |
+
seconds: 50
|
| 454 |
+
}
|
| 455 |
+
]
|
| 456 |
+
LLM Error (Synthesis): 429 You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit.
|
| 457 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.0-flash
|
| 458 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.0-flash
|
| 459 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-2.0-flash
|
| 460 |
+
Please retry in 6.198648352s. [links {
|
| 461 |
+
description: "Learn more about Gemini API quotas"
|
| 462 |
+
url: "https://ai.google.dev/gemini-api/docs/rate-limits"
|
| 463 |
+
}
|
| 464 |
+
, violations {
|
| 465 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 466 |
+
quota_id: "GenerateRequestsPerDayPerProjectPerModel-FreeTier"
|
| 467 |
+
quota_dimensions {
|
| 468 |
+
key: "model"
|
| 469 |
+
value: "gemini-2.0-flash"
|
| 470 |
+
}
|
| 471 |
+
quota_dimensions {
|
| 472 |
+
key: "location"
|
| 473 |
+
value: "global"
|
| 474 |
+
}
|
| 475 |
+
}
|
| 476 |
+
violations {
|
| 477 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 478 |
+
quota_id: "GenerateRequestsPerMinutePerProjectPerModel-FreeTier"
|
| 479 |
+
quota_dimensions {
|
| 480 |
+
key: "model"
|
| 481 |
+
value: "gemini-2.0-flash"
|
| 482 |
+
}
|
| 483 |
+
quota_dimensions {
|
| 484 |
+
key: "location"
|
| 485 |
+
value: "global"
|
| 486 |
+
}
|
| 487 |
+
}
|
| 488 |
+
violations {
|
| 489 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_input_token_count"
|
| 490 |
+
quota_id: "GenerateContentInputTokensPerModelPerMinute-FreeTier"
|
| 491 |
+
quota_dimensions {
|
| 492 |
+
key: "model"
|
| 493 |
+
value: "gemini-2.0-flash"
|
| 494 |
+
}
|
| 495 |
+
quota_dimensions {
|
| 496 |
+
key: "location"
|
| 497 |
+
value: "global"
|
| 498 |
+
}
|
| 499 |
+
}
|
| 500 |
+
, retry_delay {
|
| 501 |
+
seconds: 6
|
| 502 |
+
}
|
| 503 |
+
]
|
| 504 |
+
|
| 505 |
+
--- INIT ---
|
| 506 |
+
OpenAI Key: True
|
| 507 |
+
HF Key: hf_Js
|
| 508 |
+
Groq Key: gsk_f
|
| 509 |
+
Gemini Key: AIzaS
|
| 510 |
+
|
| 511 |
+
--- INIT ---
|
| 512 |
+
OpenAI Key: True
|
| 513 |
+
HF Key: hf_Js
|
| 514 |
+
Groq Key: gsk_f
|
| 515 |
+
Gemini Key: AIzaS
|
| 516 |
+
|
| 517 |
+
--- INIT ---
|
| 518 |
+
OpenAI Key: True
|
| 519 |
+
HF Key: hf_Js
|
| 520 |
+
Groq Key: gsk_f
|
| 521 |
+
Gemini Key: AIzaS
|
| 522 |
+
LLM Error (Synthesis): 429 You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit.
|
| 523 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 20, model: gemini-2.5-flash
|
| 524 |
+
Please retry in 9.804671677s. [links {
|
| 525 |
+
description: "Learn more about Gemini API quotas"
|
| 526 |
+
url: "https://ai.google.dev/gemini-api/docs/rate-limits"
|
| 527 |
+
}
|
| 528 |
+
, violations {
|
| 529 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 530 |
+
quota_id: "GenerateRequestsPerDayPerProjectPerModel-FreeTier"
|
| 531 |
+
quota_dimensions {
|
| 532 |
+
key: "model"
|
| 533 |
+
value: "gemini-2.5-flash"
|
| 534 |
+
}
|
| 535 |
+
quota_dimensions {
|
| 536 |
+
key: "location"
|
| 537 |
+
value: "global"
|
| 538 |
+
}
|
| 539 |
+
quota_value: 20
|
| 540 |
+
}
|
| 541 |
+
, retry_delay {
|
| 542 |
+
seconds: 9
|
| 543 |
+
}
|
| 544 |
+
]
|
| 545 |
+
LLM Error (Synthesis): 429 You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit.
|
| 546 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 20, model: gemini-2.5-flash
|
| 547 |
+
Please retry in 29.659097469s. [links {
|
| 548 |
+
description: "Learn more about Gemini API quotas"
|
| 549 |
+
url: "https://ai.google.dev/gemini-api/docs/rate-limits"
|
| 550 |
+
}
|
| 551 |
+
, violations {
|
| 552 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 553 |
+
quota_id: "GenerateRequestsPerDayPerProjectPerModel-FreeTier"
|
| 554 |
+
quota_dimensions {
|
| 555 |
+
key: "model"
|
| 556 |
+
value: "gemini-2.5-flash"
|
| 557 |
+
}
|
| 558 |
+
quota_dimensions {
|
| 559 |
+
key: "location"
|
| 560 |
+
value: "global"
|
| 561 |
+
}
|
| 562 |
+
quota_value: 20
|
| 563 |
+
}
|
| 564 |
+
, retry_delay {
|
| 565 |
+
seconds: 29
|
| 566 |
+
}
|
| 567 |
+
]
|
| 568 |
+
|
| 569 |
+
--- INIT ---
|
| 570 |
+
OpenAI Key: True
|
| 571 |
+
HF Key: hf_Js
|
| 572 |
+
Groq Key: gsk_f
|
| 573 |
+
Gemini Key: AIzaS
|
| 574 |
+
LLM Error (Synthesis): 429 You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit.
|
| 575 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 20, model: gemini-2.5-flash
|
| 576 |
+
Please retry in 3.690783496s. [links {
|
| 577 |
+
description: "Learn more about Gemini API quotas"
|
| 578 |
+
url: "https://ai.google.dev/gemini-api/docs/rate-limits"
|
| 579 |
+
}
|
| 580 |
+
, violations {
|
| 581 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 582 |
+
quota_id: "GenerateRequestsPerDayPerProjectPerModel-FreeTier"
|
| 583 |
+
quota_dimensions {
|
| 584 |
+
key: "model"
|
| 585 |
+
value: "gemini-2.5-flash"
|
| 586 |
+
}
|
| 587 |
+
quota_dimensions {
|
| 588 |
+
key: "location"
|
| 589 |
+
value: "global"
|
| 590 |
+
}
|
| 591 |
+
quota_value: 20
|
| 592 |
+
}
|
| 593 |
+
, retry_delay {
|
| 594 |
+
seconds: 3
|
| 595 |
+
}
|
| 596 |
+
]
|
| 597 |
+
|
| 598 |
+
--- INIT ---
|
| 599 |
+
OpenAI Key: True
|
| 600 |
+
HF Key: hf_Js
|
| 601 |
+
Groq Key: gsk_f
|
| 602 |
+
Gemini Key: AIzaS
|
| 603 |
+
LLM Error (Synthesis): 404 models/gemini-1.5-flash is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.
|
| 604 |
+
|
| 605 |
+
--- INIT ---
|
| 606 |
+
OpenAI Key: True
|
| 607 |
+
HF Key: hf_Js
|
| 608 |
+
Groq Key: gsk_f
|
| 609 |
+
Gemini Key: AIzaS
|
| 610 |
+
|
| 611 |
+
--- INIT ---
|
| 612 |
+
OpenAI Key: True
|
| 613 |
+
HF Key: hf_Js
|
| 614 |
+
Groq Key: gsk_f
|
| 615 |
+
Gemini Key: AIzaS
|
| 616 |
+
LLM Error (Synthesis): 404 models/gemini-1.5-flash is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.
|
| 617 |
+
|
| 618 |
+
--- INIT ---
|
| 619 |
+
OpenAI Key: True
|
| 620 |
+
HF Key: hf_Js
|
| 621 |
+
Groq Key: gsk_f
|
| 622 |
+
Gemini Key: AIzaS
|
| 623 |
+
|
| 624 |
+
--- INIT ---
|
| 625 |
+
OpenAI Key: True
|
| 626 |
+
HF Key: hf_Js
|
| 627 |
+
Groq Key: gsk_f
|
| 628 |
+
Gemini Key: AIzaS
|
| 629 |
+
|
| 630 |
+
--- INIT ---
|
| 631 |
+
OpenAI Key: True
|
| 632 |
+
HF Key: hf_Js
|
| 633 |
+
Groq Key: gsk_f
|
| 634 |
+
Gemini Key: AIzaS
|
| 635 |
+
|
| 636 |
+
--- INIT ---
|
| 637 |
+
OpenAI Key: True
|
| 638 |
+
HF Key: hf_Js
|
| 639 |
+
Groq Key: gsk_f
|
| 640 |
+
Gemini Key: AIzaS
|
| 641 |
+
LLM Error (Synthesis): 404 models/gemini-1.5-flash is not found for API version v1beta, or is not supported for generateContent. Call ListModels to see the list of available models and their supported methods.
|
| 642 |
+
|
| 643 |
+
--- INIT ---
|
| 644 |
+
OpenAI Key: True
|
| 645 |
+
HF Key: hf_Js
|
| 646 |
+
Groq Key: gsk_f
|
| 647 |
+
Gemini Key: AIzaS
|
| 648 |
+
|
| 649 |
+
--- INIT ---
|
| 650 |
+
OpenAI Key: True
|
| 651 |
+
HF Key: hf_Js
|
| 652 |
+
Groq Key: gsk_f
|
| 653 |
+
Gemini Key: AIzaS
|
| 654 |
+
LLM Error (Synthesis): 429 You exceeded your current quota, please check your plan and billing details. For more information on this error, head to: https://ai.google.dev/gemini-api/docs/rate-limits. To monitor your current usage, head to: https://ai.dev/rate-limit.
|
| 655 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.0-flash-exp
|
| 656 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_requests, limit: 0, model: gemini-2.0-flash-exp
|
| 657 |
+
* Quota exceeded for metric: generativelanguage.googleapis.com/generate_content_free_tier_input_token_count, limit: 0, model: gemini-2.0-flash-exp
|
| 658 |
+
Please retry in 36.195734558s. [links {
|
| 659 |
+
description: "Learn more about Gemini API quotas"
|
| 660 |
+
url: "https://ai.google.dev/gemini-api/docs/rate-limits"
|
| 661 |
+
}
|
| 662 |
+
, violations {
|
| 663 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 664 |
+
quota_id: "GenerateRequestsPerDayPerProjectPerModel-FreeTier"
|
| 665 |
+
quota_dimensions {
|
| 666 |
+
key: "model"
|
| 667 |
+
value: "gemini-2.0-flash-exp"
|
| 668 |
+
}
|
| 669 |
+
quota_dimensions {
|
| 670 |
+
key: "location"
|
| 671 |
+
value: "global"
|
| 672 |
+
}
|
| 673 |
+
}
|
| 674 |
+
violations {
|
| 675 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_requests"
|
| 676 |
+
quota_id: "GenerateRequestsPerMinutePerProjectPerModel-FreeTier"
|
| 677 |
+
quota_dimensions {
|
| 678 |
+
key: "model"
|
| 679 |
+
value: "gemini-2.0-flash-exp"
|
| 680 |
+
}
|
| 681 |
+
quota_dimensions {
|
| 682 |
+
key: "location"
|
| 683 |
+
value: "global"
|
| 684 |
+
}
|
| 685 |
+
}
|
| 686 |
+
violations {
|
| 687 |
+
quota_metric: "generativelanguage.googleapis.com/generate_content_free_tier_input_token_count"
|
| 688 |
+
quota_id: "GenerateContentInputTokensPerModelPerMinute-FreeTier"
|
| 689 |
+
quota_dimensions {
|
| 690 |
+
key: "model"
|
| 691 |
+
value: "gemini-2.0-flash-exp"
|
| 692 |
+
}
|
| 693 |
+
quota_dimensions {
|
| 694 |
+
key: "location"
|
| 695 |
+
value: "global"
|
| 696 |
+
}
|
| 697 |
+
}
|
| 698 |
+
, retry_delay {
|
| 699 |
+
seconds: 36
|
| 700 |
+
}
|
| 701 |
+
]
|
| 702 |
+
|
| 703 |
+
--- INIT ---
|
| 704 |
+
OpenAI Key: True
|
| 705 |
+
HF Key: hf_Js
|
| 706 |
+
Groq Key: gsk_f
|
| 707 |
+
Gemini Key: AIzaS
|
| 708 |
+
|
| 709 |
+
--- INIT ---
|
| 710 |
+
OpenAI Key: True
|
| 711 |
+
HF Key: hf_Js
|
| 712 |
+
Groq Key: gsk_f
|
| 713 |
+
Gemini Key: AIzaS
|
| 714 |
+
|
| 715 |
+
--- INIT ---
|
| 716 |
+
OpenAI Key: True
|
| 717 |
+
HF Key: hf_Js
|
| 718 |
+
Groq Key: gsk_f
|
| 719 |
+
Gemini Key: AIzaS
|
| 720 |
+
|
| 721 |
+
--- INIT ---
|
| 722 |
+
OpenAI Key: True
|
| 723 |
+
HF Key: hf_Js
|
| 724 |
+
Groq Key: gsk_f
|
| 725 |
+
Gemini Key: AIzaS
|
| 726 |
+
|
| 727 |
+
--- INIT ---
|
| 728 |
+
OpenAI Key: True
|
| 729 |
+
HF Key: hf_Js
|
| 730 |
+
Groq Key: gsk_f
|
| 731 |
+
Gemini Key: AIzaS
|
| 732 |
+
|
| 733 |
+
--- INIT ---
|
| 734 |
+
OpenAI Key: True
|
| 735 |
+
HF Key: hf_Js
|
| 736 |
+
Groq Key: gsk_f
|
| 737 |
+
Gemini Key: AIzaS
|
| 738 |
+
|
| 739 |
+
--- INIT ---
|
| 740 |
+
OpenAI Key: True
|
| 741 |
+
HF Key: hf_Js
|
| 742 |
+
Groq Key: gsk_f
|
| 743 |
+
Gemini Key: AIzaS
|
| 744 |
+
|
| 745 |
+
--- INIT ---
|
| 746 |
+
OpenAI Key: True
|
| 747 |
+
HF Key: hf_Js
|
| 748 |
+
Groq Key: gsk_f
|
| 749 |
+
Gemini Key: AIzaS
|
| 750 |
+
|
| 751 |
+
--- INIT ---
|
| 752 |
+
OpenAI Key: True
|
| 753 |
+
HF Key: hf_Js
|
| 754 |
+
Groq Key: gsk_f
|
| 755 |
+
Gemini Key: AIzaS
|
| 756 |
+
|
| 757 |
+
--- INIT ---
|
| 758 |
+
OpenAI Key: True
|
| 759 |
+
HF Key: hf_Js
|
| 760 |
+
Groq Key: gsk_f
|
| 761 |
+
Gemini Key: AIzaS
|
| 762 |
+
|
| 763 |
+
--- INIT ---
|
| 764 |
+
OpenAI Key: True
|
| 765 |
+
HF Key: hf_Js
|
| 766 |
+
Groq Key: gsk_f
|
| 767 |
+
Gemini Key: AIzaS
|
| 768 |
+
|
| 769 |
+
--- INIT ---
|
| 770 |
+
OpenAI Key: True
|
| 771 |
+
HF Key: hf_Js
|
| 772 |
+
Groq Key: gsk_f
|
| 773 |
+
Gemini Key: AIzaS
|
| 774 |
+
|
| 775 |
+
--- INIT ---
|
| 776 |
+
OpenAI Key: True
|
| 777 |
+
HF Key: hf_Js
|
| 778 |
+
Groq Key: gsk_f
|
| 779 |
+
Gemini Key: AIzaS
|
| 780 |
+
|
| 781 |
+
--- INIT ---
|
| 782 |
+
OpenAI Key: True
|
| 783 |
+
HF Key: hf_Js
|
| 784 |
+
Groq Key: gsk_f
|
| 785 |
+
Gemini Key: AIzaS
|
| 786 |
+
Provider Selected: Groq (llama-3.3-70b-versatile)
|
| 787 |
+
Provider Selected: Groq (llama-3.3-70b-versatile)
|
| 788 |
+
Provider Selected: Groq (llama-3.3-70b-versatile)
|
| 789 |
+
|
| 790 |
+
--- INIT ---
|
| 791 |
+
OpenAI Key: True
|
| 792 |
+
HF Key: hf_Js
|
| 793 |
+
Groq Key: gsk_f
|
| 794 |
+
Gemini Key: AIzaS
|
| 795 |
+
Provider Selected: Groq (llama-3.3-70b-versatile)
|
| 796 |
+
|
| 797 |
+
--- INIT ---
|
| 798 |
+
OpenAI Key: True
|
| 799 |
+
HF Key: hf_Js
|
| 800 |
+
Groq Key: gsk_f
|
| 801 |
+
Gemini Key: AIzaS
|
| 802 |
+
Provider Selected: Groq (llama-3.3-70b-versatile)
|
| 803 |
+
|
| 804 |
+
--- INIT ---
|
| 805 |
+
OpenAI Key: True
|
| 806 |
+
HF Key: hf_Js
|
| 807 |
+
Groq Key: gsk_f
|
| 808 |
+
Gemini Key: AIzaS
|
| 809 |
+
Provider Selected: Groq (llama-3.3-70b-versatile)
|
| 810 |
+
|
| 811 |
+
--- INIT ---
|
| 812 |
+
OpenAI Key: True
|
| 813 |
+
HF Key: hf_Js
|
| 814 |
+
Groq Key: gsk_f
|
| 815 |
+
Gemini Key: AIzaS
|
| 816 |
+
Provider Selected: Groq (llama-3.3-70b-versatile)
|
| 817 |
+
|
| 818 |
+
--- INIT ---
|
| 819 |
+
OpenAI Key: True
|
| 820 |
+
HF Key: hf_Js
|
| 821 |
+
Groq Key: gsk_f
|
| 822 |
+
Gemini Key: AIzaS
|
| 823 |
+
Provider Selected: Groq (llama-3.3-70b-versatile)
|
backend/debug_env.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from dotenv import load_dotenv
|
| 3 |
+
load_dotenv()
|
| 4 |
+
|
| 5 |
+
print("--- ENV DEBUG ---")
|
| 6 |
+
hf_key = os.getenv("HUGGINGFACE_API_KEY")
|
| 7 |
+
print(f"HF Key present: {bool(hf_key)}")
|
| 8 |
+
print(f"HF Key start: {hf_key[:4] if hf_key else 'None'}")
|
| 9 |
+
|
| 10 |
+
print("\n--- LIBRARY DEBUG ---")
|
| 11 |
+
try:
|
| 12 |
+
import huggingface_hub
|
| 13 |
+
print(f"huggingface_hub version: {huggingface_hub.__version__}")
|
| 14 |
+
except ImportError:
|
| 15 |
+
print("huggingface_hub NOT installed")
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
from huggingface_hub import AsyncInferenceClient
|
| 19 |
+
print("AsyncInferenceClient imported successfully")
|
| 20 |
+
except ImportError as e:
|
| 21 |
+
print(f"AsyncInferenceClient import FAILED: {e}")
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
from huggingface_hub import InferenceClient
|
| 25 |
+
print("InferenceClient imported successfully")
|
| 26 |
+
except ImportError as e:
|
| 27 |
+
print(f"InferenceClient import FAILED: {e}")
|
| 28 |
+
|
| 29 |
+
print("\n--- VECTOR DB DEBUG ---")
|
| 30 |
+
try:
|
| 31 |
+
import faiss
|
| 32 |
+
print(f"FAISS imported successfully")
|
| 33 |
+
except ImportError:
|
| 34 |
+
print("FAISS NOT installed")
|
| 35 |
+
|
| 36 |
+
try:
|
| 37 |
+
from sentence_transformers import SentenceTransformer
|
| 38 |
+
print("SentenceTransformer imported successfully")
|
| 39 |
+
except ImportError:
|
| 40 |
+
print("SentenceTransformer NOT installed")
|
| 41 |
+
|
| 42 |
+
print("\n--- APP CONFIG DEBUG ---")
|
| 43 |
+
try:
|
| 44 |
+
from app.core.config import settings
|
| 45 |
+
print(f"Settings HF Key: {bool(settings.HUGGINGFACE_API_KEY)}")
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"Could not load settings: {e}")
|
backend/list_available_models.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import google.generativeai as genai
|
| 2 |
+
import os
|
| 3 |
+
from dotenv import load_dotenv
|
| 4 |
+
|
| 5 |
+
load_dotenv()
|
| 6 |
+
api_key = os.getenv("GEMINI_API_KEY")
|
| 7 |
+
|
| 8 |
+
if not api_key:
|
| 9 |
+
print("No API Key found")
|
| 10 |
+
exit()
|
| 11 |
+
|
| 12 |
+
print(f"Checking models for key: {api_key[:10]}...")
|
| 13 |
+
genai.configure(api_key=api_key)
|
| 14 |
+
|
| 15 |
+
try:
|
| 16 |
+
print("List of available models:")
|
| 17 |
+
for m in genai.list_models():
|
| 18 |
+
if 'generateContent' in m.supported_generation_methods:
|
| 19 |
+
print(f"- {m.name}")
|
| 20 |
+
except Exception as e:
|
| 21 |
+
print(f"Error listing models: {e}")
|
backend/list_models.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import google.generativeai as genai
|
| 2 |
+
from app.core.config import settings
|
| 3 |
+
|
| 4 |
+
genai.configure(api_key=settings.GEMINI_API_KEY)
|
| 5 |
+
|
| 6 |
+
print("Listing models...")
|
| 7 |
+
try:
|
| 8 |
+
for m in genai.list_models():
|
| 9 |
+
if 'generateContent' in m.supported_generation_methods:
|
| 10 |
+
print(m.name)
|
| 11 |
+
except Exception as e:
|
| 12 |
+
print(f"Error listing models: {e}")
|
backend/my_models.txt
ADDED
|
Binary file (2.36 kB). View file
|
|
|
backend/rag_summarizer.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dotenv import load_dotenv
|
| 2 |
+
import os
|
| 3 |
+
import google.generativeai as genai
|
| 4 |
+
from tavily import TavilyClient
|
| 5 |
+
|
| 6 |
+
# =====================
|
| 7 |
+
# LOAD ENV
|
| 8 |
+
# =====================
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
GEMINI_KEY = os.getenv("GEMINI_API_KEY")
|
| 12 |
+
TAVILY_KEY = os.getenv("TAVILY_API_KEY")
|
| 13 |
+
|
| 14 |
+
print(f"Gemini Key: {GEMINI_KEY[:10]}...")
|
| 15 |
+
|
| 16 |
+
if not GEMINI_KEY:
|
| 17 |
+
print("❌ Gemini API key not loaded")
|
| 18 |
+
exit(1)
|
| 19 |
+
|
| 20 |
+
# =====================
|
| 21 |
+
# INIT SERVICES
|
| 22 |
+
# =====================
|
| 23 |
+
genai.configure(api_key=GEMINI_KEY)
|
| 24 |
+
model = genai.GenerativeModel("gemini-1.5-flash")
|
| 25 |
+
|
| 26 |
+
# Mock Tavily if key missing (for testing Gemini specifically)
|
| 27 |
+
if TAVILY_KEY:
|
| 28 |
+
tavily = TavilyClient(api_key=TAVILY_KEY)
|
| 29 |
+
else:
|
| 30 |
+
tavily = None
|
| 31 |
+
print("Warning: Tavily Key missing, using mock data")
|
| 32 |
+
|
| 33 |
+
# =====================
|
| 34 |
+
# USER QUERY
|
| 35 |
+
# =====================
|
| 36 |
+
query = "AI in healthcare current and future applications"
|
| 37 |
+
|
| 38 |
+
# =====================
|
| 39 |
+
# SEARCH (TAVILY)
|
| 40 |
+
# =====================
|
| 41 |
+
if tavily:
|
| 42 |
+
search_results = tavily.search(
|
| 43 |
+
query=query,
|
| 44 |
+
max_results=5,
|
| 45 |
+
include_raw_content=True
|
| 46 |
+
)
|
| 47 |
+
context = "\n\n".join(
|
| 48 |
+
[res["content"] for res in search_results["results"]]
|
| 49 |
+
)
|
| 50 |
+
else:
|
| 51 |
+
context = "Artificial Intelligence involves using computers to simulate human intelligence. AI is used in healthcare for diagnosis and treatment planning."
|
| 52 |
+
|
| 53 |
+
# =====================
|
| 54 |
+
# PROMPT
|
| 55 |
+
# =====================
|
| 56 |
+
PROMPT = f"""
|
| 57 |
+
You are an expert research assistant.
|
| 58 |
+
|
| 59 |
+
Task:
|
| 60 |
+
- Generate a concise, accurate, and well-structured answer
|
| 61 |
+
- Use ONLY the information from the sources below
|
| 62 |
+
- If unsure, say "Information not found in sources"
|
| 63 |
+
|
| 64 |
+
Sources:
|
| 65 |
+
{context}
|
| 66 |
+
|
| 67 |
+
Instructions:
|
| 68 |
+
- Summarize in 6–8 bullet points
|
| 69 |
+
- Use simple professional language
|
| 70 |
+
- Focus on real-world applications and future impact
|
| 71 |
+
"""
|
| 72 |
+
|
| 73 |
+
# =====================
|
| 74 |
+
# GENERATE ANSWER
|
| 75 |
+
# =====================
|
| 76 |
+
try:
|
| 77 |
+
response = model.generate_content(PROMPT)
|
| 78 |
+
print("\n✅ FINAL ANSWER:\n")
|
| 79 |
+
print(response.text)
|
| 80 |
+
except Exception as e:
|
| 81 |
+
print(f"\n❌ ERROR: {e}")
|
debug.log
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
--- INIT ---
|
| 3 |
+
OpenAI Key: True
|
| 4 |
+
HF Key: None
|
| 5 |
+
Groq Key: None
|
| 6 |
+
Gemini Key: None
|
| 7 |
+
|
| 8 |
+
--- INIT ---
|
| 9 |
+
OpenAI Key: True
|
| 10 |
+
HF Key: None
|
| 11 |
+
Groq Key: None
|
| 12 |
+
Gemini Key: None
|
| 13 |
+
|
| 14 |
+
--- INIT ---
|
| 15 |
+
OpenAI Key: True
|
| 16 |
+
HF Key: None
|
| 17 |
+
Groq Key: None
|
| 18 |
+
Gemini Key: None
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/out/
|
| 19 |
+
|
| 20 |
+
# production
|
| 21 |
+
/build
|
| 22 |
+
|
| 23 |
+
# misc
|
| 24 |
+
.DS_Store
|
| 25 |
+
*.pem
|
| 26 |
+
|
| 27 |
+
# debug
|
| 28 |
+
npm-debug.log*
|
| 29 |
+
yarn-debug.log*
|
| 30 |
+
yarn-error.log*
|
| 31 |
+
.pnpm-debug.log*
|
| 32 |
+
|
| 33 |
+
# env files (can opt-in for committing if needed)
|
| 34 |
+
.env*
|
| 35 |
+
|
| 36 |
+
# vercel
|
| 37 |
+
.vercel
|
| 38 |
+
|
| 39 |
+
# typescript
|
| 40 |
+
*.tsbuildinfo
|
| 41 |
+
next-env.d.ts
|
frontend/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
| 2 |
+
|
| 3 |
+
## Getting Started
|
| 4 |
+
|
| 5 |
+
First, run the development server:
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
npm run dev
|
| 9 |
+
# or
|
| 10 |
+
yarn dev
|
| 11 |
+
# or
|
| 12 |
+
pnpm dev
|
| 13 |
+
# or
|
| 14 |
+
bun dev
|
| 15 |
+
```
|
| 16 |
+
|
| 17 |
+
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
| 18 |
+
|
| 19 |
+
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
| 20 |
+
|
| 21 |
+
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
| 22 |
+
|
| 23 |
+
## Learn More
|
| 24 |
+
|
| 25 |
+
To learn more about Next.js, take a look at the following resources:
|
| 26 |
+
|
| 27 |
+
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
| 28 |
+
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
| 29 |
+
|
| 30 |
+
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
| 31 |
+
|
| 32 |
+
## Deploy on Vercel
|
| 33 |
+
|
| 34 |
+
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
| 35 |
+
|
| 36 |
+
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
frontend/app/favicon.ico
ADDED
|
|
frontend/app/globals.css
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@import "tailwindcss";
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
@theme {
|
| 5 |
+
--color-background: var(--background);
|
| 6 |
+
--color-foreground: var(--foreground);
|
| 7 |
+
--color-card: var(--card);
|
| 8 |
+
--color-card-foreground: var(--card-foreground);
|
| 9 |
+
--color-popover: var(--popover);
|
| 10 |
+
--color-popover-foreground: var(--popover-foreground);
|
| 11 |
+
--color-primary: var(--primary);
|
| 12 |
+
--color-primary-foreground: var(--primary-foreground);
|
| 13 |
+
--color-secondary: var(--secondary);
|
| 14 |
+
--color-secondary-foreground: var(--secondary-foreground);
|
| 15 |
+
--color-muted: var(--muted);
|
| 16 |
+
--color-muted-foreground: var(--muted-foreground);
|
| 17 |
+
--color-accent: var(--accent);
|
| 18 |
+
--color-accent-foreground: var(--accent-foreground);
|
| 19 |
+
--color-destructive: var(--destructive);
|
| 20 |
+
--color-destructive-foreground: var(--destructive-foreground);
|
| 21 |
+
--color-border: var(--border);
|
| 22 |
+
--color-input: var(--input);
|
| 23 |
+
--color-ring: var(--ring);
|
| 24 |
+
|
| 25 |
+
--radius-lg: var(--radius);
|
| 26 |
+
--radius-md: calc(var(--radius) - 2px);
|
| 27 |
+
--radius-sm: calc(var(--radius) - 4px);
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
:root {
|
| 31 |
+
--background: 0 0% 100%;
|
| 32 |
+
--foreground: 222.2 84% 4.9%;
|
| 33 |
+
|
| 34 |
+
--card: 0 0% 100%;
|
| 35 |
+
--card-foreground: 222.2 84% 4.9%;
|
| 36 |
+
|
| 37 |
+
--popover: 0 0% 100%;
|
| 38 |
+
--popover-foreground: 222.2 84% 4.9%;
|
| 39 |
+
|
| 40 |
+
--primary: 222.2 47.4% 11.2%;
|
| 41 |
+
--primary-foreground: 210 40% 98%;
|
| 42 |
+
|
| 43 |
+
--secondary: 210 40% 96.1%;
|
| 44 |
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
| 45 |
+
|
| 46 |
+
--muted: 210 40% 96.1%;
|
| 47 |
+
--muted-foreground: 215.4 16.3% 46.9%;
|
| 48 |
+
|
| 49 |
+
--accent: 210 40% 96.1%;
|
| 50 |
+
--accent-foreground: 222.2 47.4% 11.2%;
|
| 51 |
+
|
| 52 |
+
--destructive: 0 84.2% 60.2%;
|
| 53 |
+
--destructive-foreground: 210 40% 98%;
|
| 54 |
+
|
| 55 |
+
--border: 214.3 31.8% 91.4%;
|
| 56 |
+
--input: 214.3 31.8% 91.4%;
|
| 57 |
+
--ring: 222.2 84% 4.9%;
|
| 58 |
+
|
| 59 |
+
--radius: 0.5rem;
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
.dark {
|
| 63 |
+
--background: 222.2 84% 4.9%;
|
| 64 |
+
--foreground: 210 40% 98%;
|
| 65 |
+
|
| 66 |
+
--card: 222.2 84% 4.9%;
|
| 67 |
+
--card-foreground: 210 40% 98%;
|
| 68 |
+
|
| 69 |
+
--popover: 222.2 84% 4.9%;
|
| 70 |
+
--popover-foreground: 210 40% 98%;
|
| 71 |
+
|
| 72 |
+
--primary: 210 40% 98%;
|
| 73 |
+
--primary-foreground: 222.2 47.4% 11.2%;
|
| 74 |
+
|
| 75 |
+
--secondary: 217.2 32.6% 17.5%;
|
| 76 |
+
--secondary-foreground: 210 40% 98%;
|
| 77 |
+
|
| 78 |
+
--muted: 217.2 32.6% 17.5%;
|
| 79 |
+
--muted-foreground: 215 20.2% 65.1%;
|
| 80 |
+
|
| 81 |
+
--accent: 217.2 32.6% 17.5%;
|
| 82 |
+
--accent-foreground: 210 40% 98%;
|
| 83 |
+
|
| 84 |
+
--destructive: 0 62.8% 30.6%;
|
| 85 |
+
--destructive-foreground: 210 40% 98%;
|
| 86 |
+
|
| 87 |
+
--border: 217.2 32.6% 17.5%;
|
| 88 |
+
--input: 217.2 32.6% 17.5%;
|
| 89 |
+
--ring: 212.7 26.8% 83.9%;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
@layer base {
|
| 93 |
+
* {
|
| 94 |
+
@apply border-border;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
body {
|
| 98 |
+
@apply bg-background text-foreground;
|
| 99 |
+
}
|
| 100 |
+
}
|
frontend/app/layout.tsx
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Metadata } from "next";
|
| 2 |
+
import { Geist, Geist_Mono } from "next/font/google";
|
| 3 |
+
import "./globals.css";
|
| 4 |
+
|
| 5 |
+
const geistSans = Geist({
|
| 6 |
+
variable: "--font-geist-sans",
|
| 7 |
+
subsets: ["latin"],
|
| 8 |
+
});
|
| 9 |
+
|
| 10 |
+
const geistMono = Geist_Mono({
|
| 11 |
+
variable: "--font-geist-mono",
|
| 12 |
+
subsets: ["latin"],
|
| 13 |
+
});
|
| 14 |
+
|
| 15 |
+
export const metadata: Metadata = {
|
| 16 |
+
title: "Create Next App",
|
| 17 |
+
description: "Generated by create next app",
|
| 18 |
+
};
|
| 19 |
+
|
| 20 |
+
export default function RootLayout({
|
| 21 |
+
children,
|
| 22 |
+
}: Readonly<{
|
| 23 |
+
children: React.ReactNode;
|
| 24 |
+
}>) {
|
| 25 |
+
return (
|
| 26 |
+
<html lang="en" suppressHydrationWarning>
|
| 27 |
+
<body
|
| 28 |
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
| 29 |
+
suppressHydrationWarning
|
| 30 |
+
>
|
| 31 |
+
{children}
|
| 32 |
+
</body>
|
| 33 |
+
</html>
|
| 34 |
+
);
|
| 35 |
+
}
|
frontend/app/page.tsx
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ChatLayout from "../components/Chat/ChatLayout";
|
| 2 |
+
|
| 3 |
+
export default function Home() {
|
| 4 |
+
return <ChatLayout />;
|
| 5 |
+
}
|
frontend/components/Chat/ChatInput.tsx
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { SendHorizontal, Paperclip } from "lucide-react";
|
| 4 |
+
import { useRef, useEffect } from "react";
|
| 5 |
+
|
| 6 |
+
interface ChatInputProps {
|
| 7 |
+
value: string;
|
| 8 |
+
onChange: (val: string) => void;
|
| 9 |
+
onSubmit: () => void;
|
| 10 |
+
isLoading: boolean;
|
| 11 |
+
onFileSelect?: (file: File) => void;
|
| 12 |
+
attachedFile?: File | null;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default function ChatInput({ value, onChange, onSubmit, isLoading, onFileSelect, attachedFile }: ChatInputProps) {
|
| 16 |
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
| 17 |
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
| 18 |
+
|
| 19 |
+
// Auto-resize textarea
|
| 20 |
+
useEffect(() => {
|
| 21 |
+
if (textareaRef.current) {
|
| 22 |
+
textareaRef.current.style.height = "auto";
|
| 23 |
+
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 200)}px`;
|
| 24 |
+
}
|
| 25 |
+
}, [value]);
|
| 26 |
+
|
| 27 |
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
| 28 |
+
if (e.key === "Enter" && !e.shiftKey) {
|
| 29 |
+
e.preventDefault();
|
| 30 |
+
onSubmit();
|
| 31 |
+
}
|
| 32 |
+
};
|
| 33 |
+
|
| 34 |
+
return (
|
| 35 |
+
<div className="w-full max-w-3xl mx-auto px-4 pb-6">
|
| 36 |
+
<div className="relative flex items-end gap-2 bg-white border border-gray-200 rounded-xl p-3 shadow-lg focus-within:ring-2 focus-within:ring-indigo-500/50 focus-within:border-indigo-500 transition-all">
|
| 37 |
+
<input
|
| 38 |
+
type="file"
|
| 39 |
+
ref={fileInputRef}
|
| 40 |
+
className="hidden"
|
| 41 |
+
onChange={(e) => {
|
| 42 |
+
if (e.target.files?.[0] && onFileSelect) onFileSelect(e.target.files[0]);
|
| 43 |
+
}}
|
| 44 |
+
/>
|
| 45 |
+
<button
|
| 46 |
+
onClick={() => fileInputRef.current?.click()}
|
| 47 |
+
className={`p-2 transition-colors ${attachedFile ? "text-indigo-600 bg-indigo-50 rounded" : "text-gray-400 hover:text-gray-600"}`}
|
| 48 |
+
title="Attach file (PDF, Excel, Txt)"
|
| 49 |
+
>
|
| 50 |
+
<Paperclip size={20} />
|
| 51 |
+
</button>
|
| 52 |
+
|
| 53 |
+
<textarea
|
| 54 |
+
ref={textareaRef}
|
| 55 |
+
value={value}
|
| 56 |
+
onChange={(e) => onChange(e.target.value)}
|
| 57 |
+
onKeyDown={handleKeyDown}
|
| 58 |
+
placeholder="Ask anything..."
|
| 59 |
+
className="flex-1 bg-transparent border-0 focus:ring-0 text-gray-900 placeholder-gray-400 resize-none max-h-[200px] min-h-[24px] py-2"
|
| 60 |
+
disabled={isLoading}
|
| 61 |
+
rows={1}
|
| 62 |
+
/>
|
| 63 |
+
|
| 64 |
+
<button
|
| 65 |
+
onClick={onSubmit}
|
| 66 |
+
disabled={isLoading || !value.trim()}
|
| 67 |
+
className={`p-2 rounded-lg transition-all ${value.trim() && !isLoading
|
| 68 |
+
? "bg-indigo-600 text-white hover:bg-indigo-700"
|
| 69 |
+
: "bg-gray-100 text-gray-300 cursor-not-allowed"
|
| 70 |
+
}`}
|
| 71 |
+
>
|
| 72 |
+
<SendHorizontal size={20} />
|
| 73 |
+
</button>
|
| 74 |
+
</div>
|
| 75 |
+
{attachedFile && (
|
| 76 |
+
<div className="mt-2 ml-1 text-xs text-indigo-600 flex items-center gap-1 font-medium animate-fade-in">
|
| 77 |
+
<span>📄 Attached: {attachedFile.name} (will be analyzed)</span>
|
| 78 |
+
</div>
|
| 79 |
+
)}
|
| 80 |
+
<div className="text-center mt-2">
|
| 81 |
+
<p className="text-xs text-gray-400">
|
| 82 |
+
Trust-First Copilot can make mistakes. Check important info.
|
| 83 |
+
</p>
|
| 84 |
+
</div>
|
| 85 |
+
</div>
|
| 86 |
+
);
|
| 87 |
+
}
|
frontend/components/Chat/ChatLayout.tsx
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useState, useEffect } from "react";
|
| 4 |
+
import Sidebar from "./Sidebar";
|
| 5 |
+
import MessageBubble from "./MessageBubble";
|
| 6 |
+
import ChatInput from "./ChatInput";
|
| 7 |
+
import { Menu } from "lucide-react";
|
| 8 |
+
|
| 9 |
+
interface Message {
|
| 10 |
+
role: "user" | "assistant";
|
| 11 |
+
content: string;
|
| 12 |
+
sources?: any[];
|
| 13 |
+
intent?: string;
|
| 14 |
+
thought_process?: string;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
interface Session {
|
| 18 |
+
id: string;
|
| 19 |
+
title: string;
|
| 20 |
+
date: string;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export default function ChatLayout() {
|
| 24 |
+
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
| 25 |
+
const [query, setQuery] = useState("");
|
| 26 |
+
const [attachedFile, setAttachedFile] = useState<File | null>(null);
|
| 27 |
+
const [messages, setMessages] = useState<Message[]>([]);
|
| 28 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 29 |
+
const [sessions, setSessions] = useState<Session[]>([]);
|
| 30 |
+
const [currentSessionId, setCurrentSessionId] = useState<string | null>(null);
|
| 31 |
+
|
| 32 |
+
// Load history from local storage on mount
|
| 33 |
+
useEffect(() => {
|
| 34 |
+
const saved = localStorage.getItem("chat_sessions");
|
| 35 |
+
if (saved) {
|
| 36 |
+
setSessions(JSON.parse(saved));
|
| 37 |
+
}
|
| 38 |
+
}, []);
|
| 39 |
+
|
| 40 |
+
const handleNewChat = () => {
|
| 41 |
+
setMessages([]);
|
| 42 |
+
setCurrentSessionId(null);
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
const handleSearch = async () => {
|
| 46 |
+
if (!query.trim()) return;
|
| 47 |
+
|
| 48 |
+
// Add User Message
|
| 49 |
+
const userMsg: Message = { role: "user", content: query };
|
| 50 |
+
setMessages((prev) => [...prev, userMsg]);
|
| 51 |
+
setQuery("");
|
| 52 |
+
setIsLoading(true);
|
| 53 |
+
|
| 54 |
+
try {
|
| 55 |
+
let finalQuery = userMsg.content;
|
| 56 |
+
|
| 57 |
+
// Upload File if exists
|
| 58 |
+
if (attachedFile) {
|
| 59 |
+
const formData = new FormData();
|
| 60 |
+
formData.append("file", attachedFile);
|
| 61 |
+
|
| 62 |
+
// Optimistic UI update: Show file as uploaded
|
| 63 |
+
setMessages((prev) => [...prev, { role: "assistant", content: `📂 Analyzing ${attachedFile.name}...`, isLoading: true }]);
|
| 64 |
+
|
| 65 |
+
const uploadRes = await fetch(`${process.env.NEXT_PUBLIC_API_BASE || "http://localhost:8000"}/api/v1/upload`, {
|
| 66 |
+
method: "POST",
|
| 67 |
+
body: formData,
|
| 68 |
+
});
|
| 69 |
+
const uploadData = await uploadRes.json();
|
| 70 |
+
|
| 71 |
+
// Remove the loading message logic would be complex, simplfying:
|
| 72 |
+
// Append file context to query
|
| 73 |
+
finalQuery = `[Context from uploaded file ${attachedFile.name}]:\n${uploadData.content}\n\nUser Question: ${userMsg.content}`;
|
| 74 |
+
setAttachedFile(null); // Clear file
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE || "http://localhost:8000"}/api/v1/query`, {
|
| 78 |
+
method: "POST",
|
| 79 |
+
headers: { "Content-Type": "application/json" },
|
| 80 |
+
body: JSON.stringify({ query: finalQuery }),
|
| 81 |
+
});
|
| 82 |
+
|
| 83 |
+
const data = await response.json();
|
| 84 |
+
|
| 85 |
+
// Add Assistant Message
|
| 86 |
+
const aiMsg: Message = {
|
| 87 |
+
role: "assistant",
|
| 88 |
+
content: data.answer,
|
| 89 |
+
sources: data.sources,
|
| 90 |
+
intent: data.intent,
|
| 91 |
+
thought_process: data.thought_process,
|
| 92 |
+
};
|
| 93 |
+
setMessages((prev) => [...prev, aiMsg]);
|
| 94 |
+
|
| 95 |
+
// Save Session (Simple Logic)
|
| 96 |
+
if (!currentSessionId) {
|
| 97 |
+
const newId = Date.now().toString();
|
| 98 |
+
const newSession = {
|
| 99 |
+
id: newId,
|
| 100 |
+
title: userMsg.content.slice(0, 30) + "...",
|
| 101 |
+
date: new Date().toLocaleDateString(),
|
| 102 |
+
};
|
| 103 |
+
setSessions((prev) => [newSession, ...prev]);
|
| 104 |
+
setCurrentSessionId(newId);
|
| 105 |
+
localStorage.setItem("chat_sessions", JSON.stringify([newSession, ...sessions]));
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
} catch (error) {
|
| 109 |
+
console.error("Error:", error);
|
| 110 |
+
setMessages((prev) => [
|
| 111 |
+
...prev,
|
| 112 |
+
{ role: "assistant", content: "Sorry, something went wrong. Please try again." },
|
| 113 |
+
]);
|
| 114 |
+
} finally {
|
| 115 |
+
setIsLoading(false);
|
| 116 |
+
}
|
| 117 |
+
};
|
| 118 |
+
|
| 119 |
+
const handleChallenge = async (msgIndex: number) => {
|
| 120 |
+
const targetMsg = messages[msgIndex];
|
| 121 |
+
if (!targetMsg || targetMsg.role !== "assistant") return;
|
| 122 |
+
|
| 123 |
+
// Find the preceding user message for context (simple heuristic: index - 1)
|
| 124 |
+
const userQuery = messages[msgIndex - 1]?.content || "Unknown context";
|
| 125 |
+
const sourcesText = targetMsg.sources
|
| 126 |
+
? targetMsg.sources.map(s => `Title: ${s.title}\nContent: ${s.snippet}`).join("\n\n")
|
| 127 |
+
: "No sources.";
|
| 128 |
+
|
| 129 |
+
setIsLoading(true);
|
| 130 |
+
|
| 131 |
+
try {
|
| 132 |
+
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE || "http://localhost:8000"}/api/v1/challenge`, {
|
| 133 |
+
method: "POST",
|
| 134 |
+
headers: { "Content-Type": "application/json" },
|
| 135 |
+
body: JSON.stringify({
|
| 136 |
+
original_query: userQuery,
|
| 137 |
+
original_answer: targetMsg.content,
|
| 138 |
+
sources_text: sourcesText
|
| 139 |
+
}),
|
| 140 |
+
});
|
| 141 |
+
|
| 142 |
+
const data = await response.json();
|
| 143 |
+
|
| 144 |
+
// Add Critique Message
|
| 145 |
+
const critiqueMsg: Message = {
|
| 146 |
+
role: "assistant",
|
| 147 |
+
content: data.answer,
|
| 148 |
+
sources: [],
|
| 149 |
+
intent: "CRITIQUE",
|
| 150 |
+
thought_process: "Devil's Advocate Mode: Analyzing potential flaws in the previous answer."
|
| 151 |
+
};
|
| 152 |
+
setMessages((prev) => [...prev, critiqueMsg]);
|
| 153 |
+
|
| 154 |
+
} catch (error) {
|
| 155 |
+
console.error("Challenge Error:", error);
|
| 156 |
+
} finally {
|
| 157 |
+
setIsLoading(false);
|
| 158 |
+
}
|
| 159 |
+
};
|
| 160 |
+
|
| 161 |
+
return (
|
| 162 |
+
<div className="flex h-screen bg-white text-gray-900 font-sans overflow-hidden">
|
| 163 |
+
{/* Sidebar */}
|
| 164 |
+
<Sidebar
|
| 165 |
+
isOpen={isSidebarOpen}
|
| 166 |
+
sessions={sessions}
|
| 167 |
+
currentSessionId={currentSessionId}
|
| 168 |
+
onNewChat={handleNewChat}
|
| 169 |
+
onSelectSession={(id) => console.log("Select session:", id)} // Placeholder for loading specific session
|
| 170 |
+
onDeleteSession={(id, e) => {
|
| 171 |
+
e.stopPropagation();
|
| 172 |
+
const newSessions = sessions.filter(s => s.id !== id);
|
| 173 |
+
setSessions(newSessions);
|
| 174 |
+
localStorage.setItem("chat_sessions", JSON.stringify(newSessions));
|
| 175 |
+
if (currentSessionId === id) handleNewChat();
|
| 176 |
+
}}
|
| 177 |
+
/>
|
| 178 |
+
|
| 179 |
+
{/* Main Content */}
|
| 180 |
+
<div className="flex-1 flex flex-col h-full relative">
|
| 181 |
+
{/* Header / Mobile Toggle */}
|
| 182 |
+
<div className="absolute top-4 left-4 z-20">
|
| 183 |
+
<button
|
| 184 |
+
onClick={() => setIsSidebarOpen(!isSidebarOpen)}
|
| 185 |
+
className="p-2 text-gray-400 hover:text-gray-900 rounded-lg hover:bg-gray-100 transition-colors"
|
| 186 |
+
>
|
| 187 |
+
<Menu size={20} />
|
| 188 |
+
</button>
|
| 189 |
+
</div>
|
| 190 |
+
|
| 191 |
+
{/* Chat Area */}
|
| 192 |
+
<div className="flex-1 overflow-y-auto scroll-smooth">
|
| 193 |
+
{messages.length === 0 ? (
|
| 194 |
+
<div className="h-full flex flex-col items-center justify-center p-8 text-center opacity-50">
|
| 195 |
+
<div className="w-16 h-16 bg-gray-100 rounded-2xl flex items-center justify-center mb-6">
|
| 196 |
+
<span className="text-3xl">✨</span>
|
| 197 |
+
</div>
|
| 198 |
+
<h2 className="text-2xl font-semibold mb-2 text-gray-900">Trust-First Copilot</h2>
|
| 199 |
+
<p className="max-w-md text-gray-500">
|
| 200 |
+
Ask anything. I check reliable sources before answering.
|
| 201 |
+
</p>
|
| 202 |
+
</div>
|
| 203 |
+
) : (
|
| 204 |
+
<div className="pb-32">
|
| 205 |
+
{messages.map((msg, idx) => (
|
| 206 |
+
<MessageBubble
|
| 207 |
+
key={idx}
|
| 208 |
+
role={msg.role}
|
| 209 |
+
content={msg.content}
|
| 210 |
+
sources={msg.sources}
|
| 211 |
+
intent={msg.intent}
|
| 212 |
+
thought_process={msg.thought_process}
|
| 213 |
+
onChallenge={msg.role === "assistant" ? () => handleChallenge(idx) : undefined}
|
| 214 |
+
/>
|
| 215 |
+
))}
|
| 216 |
+
{isLoading && (
|
| 217 |
+
<MessageBubble role="assistant" content="" isLoading={true} />
|
| 218 |
+
)}
|
| 219 |
+
</div>
|
| 220 |
+
)}
|
| 221 |
+
</div>
|
| 222 |
+
|
| 223 |
+
{/* Input Area */}
|
| 224 |
+
<div className="flex-shrink-0 bg-white pt-10">
|
| 225 |
+
<ChatInput
|
| 226 |
+
value={query}
|
| 227 |
+
onChange={setQuery}
|
| 228 |
+
onSubmit={handleSearch}
|
| 229 |
+
isLoading={isLoading}
|
| 230 |
+
onFileSelect={setAttachedFile}
|
| 231 |
+
attachedFile={attachedFile}
|
| 232 |
+
/>
|
| 233 |
+
</div>
|
| 234 |
+
</div>
|
| 235 |
+
</div>
|
| 236 |
+
);
|
| 237 |
+
}
|
frontend/components/Chat/MessageBubble.tsx
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import ReactMarkdown from "react-markdown";
|
| 4 |
+
import { Copy, ThumbsUp, ThumbsDown, User, Bot, Loader2 } from "lucide-react";
|
| 5 |
+
import { SourceList } from "../ui/SourceList";
|
| 6 |
+
import { cleanMessageContent, linkifySources } from "@/lib/utils";
|
| 7 |
+
|
| 8 |
+
interface MessageBubbleProps {
|
| 9 |
+
role: "user" | "assistant";
|
| 10 |
+
content: string;
|
| 11 |
+
sources?: any[];
|
| 12 |
+
isLoading?: boolean;
|
| 13 |
+
intent?: string;
|
| 14 |
+
thought_process?: string;
|
| 15 |
+
onChallenge?: () => void;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export default function MessageBubble({ role, content, sources, isLoading, intent, thought_process, onChallenge }: MessageBubbleProps) {
|
| 19 |
+
const isUser = role === "user";
|
| 20 |
+
|
| 21 |
+
// Intent Badge Config
|
| 22 |
+
const getIntentBadge = (intent?: string) => {
|
| 23 |
+
if (!intent) return null;
|
| 24 |
+
if (intent === "SEARCH_REQUIRED") return <span className="text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded-full bg-blue-100 text-blue-700">Web Search</span>;
|
| 25 |
+
if (intent === "CHAT_ONLY") return <span className="text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700">Direct Chat</span>;
|
| 26 |
+
if (intent === "CODING_TASK") return <span className="text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded-full bg-purple-100 text-purple-700">Coding</span>;
|
| 27 |
+
return <span className="text-[10px] uppercase font-bold tracking-wider px-2 py-0.5 rounded-full bg-gray-100 text-gray-700">{intent}</span>;
|
| 28 |
+
};
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<div className={`w-full py-6 ${isUser ? "" : "bg-gray-50 dark:bg-transparent"}`}>
|
| 32 |
+
<div className="max-w-3xl mx-auto px-4 flex gap-6">
|
| 33 |
+
{/* Avatar */}
|
| 34 |
+
<div className="flex-shrink-0 mt-1">
|
| 35 |
+
{isUser ? (
|
| 36 |
+
<div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center">
|
| 37 |
+
<User size={18} className="text-gray-500" />
|
| 38 |
+
</div>
|
| 39 |
+
) : (
|
| 40 |
+
<div className="w-8 h-8 rounded-full bg-indigo-600 flex items-center justify-center shadow-lg shadow-indigo-500/20">
|
| 41 |
+
<Bot size={18} className="text-white" />
|
| 42 |
+
</div>
|
| 43 |
+
)}
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
{/* Content */}
|
| 47 |
+
<div className="flex-1 overflow-hidden">
|
| 48 |
+
<div className="flex items-center gap-3 mb-1">
|
| 49 |
+
<span className="font-semibold text-sm text-gray-900">
|
| 50 |
+
{isUser ? "You" : "Copilot"}
|
| 51 |
+
</span>
|
| 52 |
+
{!isUser && !isLoading && getIntentBadge(intent)}
|
| 53 |
+
</div>
|
| 54 |
+
|
| 55 |
+
<div className="prose prose-sm max-w-none text-gray-800 leading-relaxed">
|
| 56 |
+
{isLoading ? (
|
| 57 |
+
<div className="flex items-center gap-2 text-gray-500 animate-pulse">
|
| 58 |
+
<Loader2 size={16} className="animate-spin" />
|
| 59 |
+
Thinking...
|
| 60 |
+
</div>
|
| 61 |
+
) : (
|
| 62 |
+
<ReactMarkdown
|
| 63 |
+
components={{
|
| 64 |
+
a: ({ node, ...props }) => <a {...props} className="text-blue-600 hover:underline" target="_blank" rel="noopener noreferrer" />
|
| 65 |
+
}}
|
| 66 |
+
>
|
| 67 |
+
{isUser ? content : linkifySources(cleanMessageContent(content), sources)}
|
| 68 |
+
</ReactMarkdown>
|
| 69 |
+
)}
|
| 70 |
+
</div>
|
| 71 |
+
|
| 72 |
+
{/* Sources for Assistant */}
|
| 73 |
+
{!isUser && sources && sources.length > 0 && (
|
| 74 |
+
<div className="mt-4 pt-4 border-t border-gray-100">
|
| 75 |
+
<SourceList sources={sources} />
|
| 76 |
+
</div>
|
| 77 |
+
)}
|
| 78 |
+
|
| 79 |
+
{/* Reasoning for Assistant (AI-OS Feature) */}
|
| 80 |
+
{!isUser && thought_process && (
|
| 81 |
+
<div className="mt-4">
|
| 82 |
+
<details className="group">
|
| 83 |
+
<summary className="flex items-center gap-2 text-xs font-semibold text-gray-400 cursor-pointer hover:text-gray-600 transition-colors select-none">
|
| 84 |
+
<span>🧠 View Thought Process</span>
|
| 85 |
+
</summary>
|
| 86 |
+
<div className="mt-2 text-xs text-gray-600 font-mono bg-gray-100 p-3 rounded-lg leading-relaxed whitespace-pre-wrap">
|
| 87 |
+
{thought_process}
|
| 88 |
+
</div>
|
| 89 |
+
</details>
|
| 90 |
+
</div>
|
| 91 |
+
)}
|
| 92 |
+
|
| 93 |
+
{/* Actions for Assistant */}
|
| 94 |
+
{!isUser && !isLoading && (
|
| 95 |
+
<div className="flex items-center gap-2 mt-4 text-gray-400">
|
| 96 |
+
<button className="p-1 hover:text-gray-600 transition-colors" title="Copy">
|
| 97 |
+
<Copy size={14} />
|
| 98 |
+
</button>
|
| 99 |
+
<button className="p-1 hover:text-gray-600 transition-colors" title="Good response">
|
| 100 |
+
<ThumbsUp size={14} />
|
| 101 |
+
</button>
|
| 102 |
+
<button className="p-1 hover:text-gray-600 transition-colors" title="Bad response">
|
| 103 |
+
<ThumbsDown size={14} />
|
| 104 |
+
</button>
|
| 105 |
+
{/* "Disagree with Me" Button */}
|
| 106 |
+
{onChallenge && (
|
| 107 |
+
<button
|
| 108 |
+
onClick={onChallenge}
|
| 109 |
+
className="ml-2 flex items-center gap-1 text-xs font-medium text-amber-600 hover:text-amber-700 bg-amber-50 px-2 py-1 rounded border border-amber-200 transition-colors"
|
| 110 |
+
title="Critique this answer"
|
| 111 |
+
>
|
| 112 |
+
<span>⚖️ Challenge</span>
|
| 113 |
+
</button>
|
| 114 |
+
)}
|
| 115 |
+
</div>
|
| 116 |
+
)}
|
| 117 |
+
</div>
|
| 118 |
+
</div>
|
| 119 |
+
</div>
|
| 120 |
+
);
|
| 121 |
+
}
|
frontend/components/Chat/Sidebar.tsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { MessageSquare, Plus, Trash2 } from "lucide-react";
|
| 4 |
+
import { motion } from "framer-motion";
|
| 5 |
+
|
| 6 |
+
interface SidebarProps {
|
| 7 |
+
isOpen: boolean;
|
| 8 |
+
sessions: { id: string; title: string; date: string }[];
|
| 9 |
+
currentSessionId: string | null;
|
| 10 |
+
onNewChat: () => void;
|
| 11 |
+
onSelectSession: (id: string) => void;
|
| 12 |
+
onDeleteSession: (id: string, e: React.MouseEvent) => void;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default function Sidebar({
|
| 16 |
+
isOpen,
|
| 17 |
+
sessions,
|
| 18 |
+
currentSessionId,
|
| 19 |
+
onNewChat,
|
| 20 |
+
onSelectSession,
|
| 21 |
+
onDeleteSession,
|
| 22 |
+
}: SidebarProps) {
|
| 23 |
+
return (
|
| 24 |
+
<motion.div
|
| 25 |
+
initial={{ width: 0, opacity: 0 }}
|
| 26 |
+
animate={{ width: isOpen ? 260 : 0, opacity: isOpen ? 1 : 0 }}
|
| 27 |
+
className="h-screen bg-gray-50 border-r border-gray-200 flex flex-col flex-shrink-0 overflow-hidden"
|
| 28 |
+
>
|
| 29 |
+
{/* New Chat Button */}
|
| 30 |
+
<div className="p-4">
|
| 31 |
+
<button
|
| 32 |
+
onClick={onNewChat}
|
| 33 |
+
className="w-full flex items-center gap-2 px-4 py-3 bg-white hover:bg-gray-100 text-gray-900 rounded-lg transition-colors border border-gray-200 text-sm font-medium shadow-sm"
|
| 34 |
+
>
|
| 35 |
+
<Plus size={16} />
|
| 36 |
+
New Chat
|
| 37 |
+
</button>
|
| 38 |
+
</div>
|
| 39 |
+
|
| 40 |
+
{/* Session List */}
|
| 41 |
+
<div className="flex-1 overflow-y-auto px-2 pb-4">
|
| 42 |
+
<div className="text-xs font-semibold text-gray-500 mb-2 px-2 mt-2">
|
| 43 |
+
Recent
|
| 44 |
+
</div>
|
| 45 |
+
<div className="space-y-1">
|
| 46 |
+
{sessions.map((session) => (
|
| 47 |
+
<button
|
| 48 |
+
key={session.id}
|
| 49 |
+
onClick={() => onSelectSession(session.id)}
|
| 50 |
+
className={`group w-full flex items-center justify-between gap-2 px-3 py-3 rounded-lg text-sm text-left transition-colors ${currentSessionId === session.id
|
| 51 |
+
? "bg-white text-gray-900 shadow-sm border border-gray-100"
|
| 52 |
+
: "text-gray-600 hover:bg-gray-100"
|
| 53 |
+
}`}
|
| 54 |
+
>
|
| 55 |
+
<div className="flex items-center gap-2 overflow-hidden">
|
| 56 |
+
<MessageSquare size={14} className="text-gray-400 flex-shrink-0" />
|
| 57 |
+
<span className="truncate">{session.title}</span>
|
| 58 |
+
</div>
|
| 59 |
+
|
| 60 |
+
<div
|
| 61 |
+
onClick={(e) => onDeleteSession(session.id, e)}
|
| 62 |
+
className={`text-gray-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity p-1`}
|
| 63 |
+
>
|
| 64 |
+
<Trash2 size={14} />
|
| 65 |
+
</div>
|
| 66 |
+
</button>
|
| 67 |
+
))}
|
| 68 |
+
{sessions.length === 0 && (
|
| 69 |
+
<div className="px-4 py-4 text-xs text-gray-400 text-center italic">
|
| 70 |
+
No recent chats
|
| 71 |
+
</div>
|
| 72 |
+
)}
|
| 73 |
+
</div>
|
| 74 |
+
</div>
|
| 75 |
+
|
| 76 |
+
{/* User Profile / Footer (Optional) */}
|
| 77 |
+
<div className="p-4 border-t border-gray-200">
|
| 78 |
+
<div className="flex items-center gap-3 px-2 py-2 text-gray-700 text-sm">
|
| 79 |
+
<div className="w-8 h-8 rounded-full bg-indigo-600 flex items-center justify-center text-white font-bold">
|
| 80 |
+
U
|
| 81 |
+
</div>
|
| 82 |
+
<div className="flex-col flex">
|
| 83 |
+
<span className="font-medium text-gray-900">User</span>
|
| 84 |
+
<span className="text-xs text-gray-500">Pro Plan</span>
|
| 85 |
+
</div>
|
| 86 |
+
</div>
|
| 87 |
+
</div>
|
| 88 |
+
</motion.div>
|
| 89 |
+
);
|
| 90 |
+
}
|
frontend/components/ui/SearchBar.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { Search, ArrowRight } from "lucide-react";
|
| 4 |
+
import { useState } from "react";
|
| 5 |
+
import { cn } from "@/lib/utils";
|
| 6 |
+
|
| 7 |
+
interface SearchBarProps {
|
| 8 |
+
onSearch: (query: string) => void;
|
| 9 |
+
isLoading: boolean;
|
| 10 |
+
centered?: boolean;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function SearchBar({ onSearch, isLoading, centered = false }: SearchBarProps) {
|
| 14 |
+
const [query, setQuery] = useState("");
|
| 15 |
+
|
| 16 |
+
const handleSubmit = (e: React.FormEvent) => {
|
| 17 |
+
e.preventDefault();
|
| 18 |
+
if (query.trim()) {
|
| 19 |
+
onSearch(query);
|
| 20 |
+
}
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
return (
|
| 24 |
+
<div className={cn("w-full transition-all duration-500", centered ? "max-w-2xl" : "max-w-4xl")}>
|
| 25 |
+
<form onSubmit={handleSubmit} className="relative group">
|
| 26 |
+
<div className="absolute inset-y-0 left-4 flex items-center pointer-events-none text-muted-foreground">
|
| 27 |
+
<Search className="w-5 h-5" />
|
| 28 |
+
</div>
|
| 29 |
+
<input
|
| 30 |
+
type="text"
|
| 31 |
+
value={query}
|
| 32 |
+
onChange={(e) => setQuery(e.target.value)}
|
| 33 |
+
placeholder="Ask anything..."
|
| 34 |
+
className="w-full py-4 pl-12 pr-12 text-lg bg-secondary/50 border-none rounded-2xl ring-1 ring-black/5 focus:ring-2 focus:ring-primary/20 focus:bg-background transition-all shadow-sm group-hover:shadow-md"
|
| 35 |
+
disabled={isLoading}
|
| 36 |
+
/>
|
| 37 |
+
<button
|
| 38 |
+
type="submit"
|
| 39 |
+
disabled={!query.trim() || isLoading}
|
| 40 |
+
className="absolute inset-y-2 right-2 flex items-center justify-center p-2 rounded-xl bg-primary text-primary-foreground disabled:opacity-50 disabled:cursor-not-allowed hover:opacity-90 transition-opacity"
|
| 41 |
+
>
|
| 42 |
+
{isLoading ? (
|
| 43 |
+
<div className="w-5 h-5 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
| 44 |
+
) : (
|
| 45 |
+
<ArrowRight className="w-5 h-5" />
|
| 46 |
+
)}
|
| 47 |
+
</button>
|
| 48 |
+
</form>
|
| 49 |
+
{centered && (
|
| 50 |
+
<div className="mt-8 flex gap-2 justify-center text-sm text-muted-foreground">
|
| 51 |
+
<span className="px-3 py-1 bg-secondary rounded-full cursor-pointer hover:bg-secondary/80">Market Trends</span>
|
| 52 |
+
<span className="px-3 py-1 bg-secondary rounded-full cursor-pointer hover:bg-secondary/80">Code Debugging</span>
|
| 53 |
+
<span className="px-3 py-1 bg-secondary rounded-full cursor-pointer hover:bg-secondary/80">Legal Research</span>
|
| 54 |
+
</div>
|
| 55 |
+
)}
|
| 56 |
+
</div>
|
| 57 |
+
);
|
| 58 |
+
}
|
frontend/components/ui/SourceList.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { ExternalLink, FileText } from "lucide-react";
|
| 2 |
+
|
| 3 |
+
interface Source {
|
| 4 |
+
title: string;
|
| 5 |
+
url: string;
|
| 6 |
+
snippet: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
interface SourceListProps {
|
| 10 |
+
sources: Source[];
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
export function SourceList({ sources }: SourceListProps) {
|
| 14 |
+
if (!sources || sources.length === 0) return null;
|
| 15 |
+
|
| 16 |
+
return (
|
| 17 |
+
<div className="mb-8">
|
| 18 |
+
<h3 className="text-sm font-semibold text-muted-foreground mb-3 flex items-center gap-2">
|
| 19 |
+
<FileText className="w-4 h-4" /> Sources
|
| 20 |
+
</h3>
|
| 21 |
+
<div className="flex gap-3 overflow-x-auto pb-4 scrollbar-hide">
|
| 22 |
+
{sources.map((source, idx) => (
|
| 23 |
+
<a
|
| 24 |
+
key={idx}
|
| 25 |
+
href={source.url}
|
| 26 |
+
target="_blank"
|
| 27 |
+
rel="noopener noreferrer"
|
| 28 |
+
className="flex-shrink-0 w-64 p-3 bg-card border rounded-xl hover:bg-secondary/50 transition-colors group cursor-pointer"
|
| 29 |
+
>
|
| 30 |
+
<div className="text-xs text-muted-foreground mb-1 truncate">{new URL(source.url).hostname}</div>
|
| 31 |
+
<div className="font-medium text-sm line-clamp-2 mb-2 group-hover:text-primary transition-colors">
|
| 32 |
+
{source.title}
|
| 33 |
+
</div>
|
| 34 |
+
<div className="text-xs text-muted-foreground line-clamp-2">
|
| 35 |
+
{source.snippet}
|
| 36 |
+
</div>
|
| 37 |
+
</a>
|
| 38 |
+
))}
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
);
|
| 42 |
+
}
|
frontend/env_example.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
NEXT_PUBLIC_API_BASE=https://YOUR-BACKEND-URL.onrender.com
|
frontend/eslint.config.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
+
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 3 |
+
import nextTs from "eslint-config-next/typescript";
|
| 4 |
+
|
| 5 |
+
const eslintConfig = defineConfig([
|
| 6 |
+
...nextVitals,
|
| 7 |
+
...nextTs,
|
| 8 |
+
// Override default ignores of eslint-config-next.
|
| 9 |
+
globalIgnores([
|
| 10 |
+
// Default ignores of eslint-config-next:
|
| 11 |
+
".next/**",
|
| 12 |
+
"out/**",
|
| 13 |
+
"build/**",
|
| 14 |
+
"next-env.d.ts",
|
| 15 |
+
]),
|
| 16 |
+
]);
|
| 17 |
+
|
| 18 |
+
export default eslintConfig;
|
frontend/lib/utils.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { clsx, type ClassValue } from "clsx";
|
| 2 |
+
import { twMerge } from "tailwind-merge";
|
| 3 |
+
|
| 4 |
+
export function cn(...inputs: ClassValue[]) {
|
| 5 |
+
return twMerge(clsx(inputs));
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export function cleanMessageContent(content: string): string {
|
| 9 |
+
if (!content) return "";
|
| 10 |
+
|
| 11 |
+
// Remove "Assumptions & Risks" section and everything until the next header or end
|
| 12 |
+
let cleaned = content.replace(/### ⚠️ Assumptions & Risks[\s\S]*?(?=### |$)/g, "");
|
| 13 |
+
|
| 14 |
+
// Remove "Unknowns / Limitations" section and everything until the next header or end
|
| 15 |
+
cleaned = cleaned.replace(/### ❌ Unknowns \/ Limitations[\s\S]*?(?=### |$)/g, "");
|
| 16 |
+
|
| 17 |
+
return cleaned.trim();
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
export function linkifySources(content: string, sources: any[] = []): string {
|
| 21 |
+
if (!content || !sources.length) return content;
|
| 22 |
+
|
| 23 |
+
let linkedContent = content;
|
| 24 |
+
|
| 25 |
+
sources.forEach((source, index) => {
|
| 26 |
+
const sourceNum = index + 1;
|
| 27 |
+
const url = source.url || "#";
|
| 28 |
+
// Regex to find "Source X" or "(Source X)" case insensitive
|
| 29 |
+
// We use a complex regex to avoid double linking if run multiple times or overlapping
|
| 30 |
+
// Simplified: Look for "Source X" that isn't already inside a markdown link
|
| 31 |
+
const regex = new RegExp(`(?<!\\[)Source ${sourceNum}(?!\\])`, 'gi');
|
| 32 |
+
|
| 33 |
+
linkedContent = linkedContent.replace(regex, `[Source ${sourceNum}](${url})`);
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
return linkedContent;
|
| 37 |
+
}
|
frontend/next.config.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { NextConfig } from "next";
|
| 2 |
+
|
| 3 |
+
const nextConfig: NextConfig = {
|
| 4 |
+
/* config options here */
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default nextConfig;
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"version": "0.1.0",
|
| 4 |
+
"private": true,
|
| 5 |
+
"scripts": {
|
| 6 |
+
"dev": "next dev",
|
| 7 |
+
"build": "next build",
|
| 8 |
+
"start": "next start",
|
| 9 |
+
"lint": "eslint"
|
| 10 |
+
},
|
| 11 |
+
"dependencies": {
|
| 12 |
+
"clsx": "^2.1.1",
|
| 13 |
+
"framer-motion": "^12.26.2",
|
| 14 |
+
"lucide-react": "^0.562.0",
|
| 15 |
+
"next": "16.1.1",
|
| 16 |
+
"react": "19.2.3",
|
| 17 |
+
"react-dom": "19.2.3",
|
| 18 |
+
"react-markdown": "^10.1.0",
|
| 19 |
+
"tailwind-merge": "^3.4.0",
|
| 20 |
+
"tailwindcss-animate": "^1.0.7"
|
| 21 |
+
},
|
| 22 |
+
"devDependencies": {
|
| 23 |
+
"@tailwindcss/postcss": "^4",
|
| 24 |
+
"@types/node": "^20",
|
| 25 |
+
"@types/react": "^19",
|
| 26 |
+
"@types/react-dom": "^19",
|
| 27 |
+
"eslint": "^9",
|
| 28 |
+
"eslint-config-next": "16.1.1",
|
| 29 |
+
"tailwindcss": "^4",
|
| 30 |
+
"typescript": "^5"
|
| 31 |
+
}
|
| 32 |
+
}
|
frontend/postcss.config.mjs
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const config = {
|
| 2 |
+
plugins: {
|
| 3 |
+
"@tailwindcss/postcss": {},
|
| 4 |
+
},
|
| 5 |
+
};
|
| 6 |
+
|
| 7 |
+
export default config;
|
frontend/public/file.svg
ADDED
|
|
frontend/public/globe.svg
ADDED
|
|