Spaces:
Sleeping
Sleeping
Upload 10 files
Browse files- Dockerfile +25 -0
- app/config.py +27 -0
- app/main.py +102 -0
- app/rag_setup.py +71 -0
- app/schemas.py +56 -0
- app/services.py +195 -0
- main.py +17 -0
- requirements.txt +10 -0
- static/app.js +315 -0
- templates/index.html +133 -0
Dockerfile
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install system dependencies
|
| 6 |
+
RUN apt-get update && apt-get install -y \
|
| 7 |
+
build-essential \
|
| 8 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 9 |
+
|
| 10 |
+
# Copy requirements and install Python dependencies
|
| 11 |
+
COPY requirements.txt .
|
| 12 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
+
|
| 14 |
+
# Copy the entire application
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
# Expose the port
|
| 18 |
+
EXPOSE 7860
|
| 19 |
+
|
| 20 |
+
# Set environment variables
|
| 21 |
+
ENV PYTHONPATH=/app
|
| 22 |
+
ENV PORT=7860
|
| 23 |
+
|
| 24 |
+
# Run the application
|
| 25 |
+
CMD ["python", "main.py"]
|
app/config.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import os
|
| 4 |
+
|
| 5 |
+
# Handle both local and Hugging Face environments
|
| 6 |
+
if os.path.exists("/app"): # Hugging Face
|
| 7 |
+
BASE_DIR = Path("/app")
|
| 8 |
+
ENV_PATH = BASE_DIR / ".env"
|
| 9 |
+
else: # Local
|
| 10 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 11 |
+
ENV_PATH = BASE_DIR / ".env"
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class Settings(BaseSettings):
|
| 15 |
+
OPENROUTER_API_KEY: str = ""
|
| 16 |
+
MODEL_NAME: str = "deepseek/deepseek-r1-0528:free"
|
| 17 |
+
OPENROUTER_URL: str = "https://openrouter.ai/api/v1"
|
| 18 |
+
|
| 19 |
+
model_config = SettingsConfigDict(
|
| 20 |
+
env_file=ENV_PATH if ENV_PATH.exists() else None,
|
| 21 |
+
env_file_encoding="utf-8",
|
| 22 |
+
case_sensitive=False,
|
| 23 |
+
extra='ignore'
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
settings = Settings()
|
app/main.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Request, HTTPException
|
| 2 |
+
from fastapi.responses import HTMLResponse
|
| 3 |
+
from fastapi.staticfiles import StaticFiles
|
| 4 |
+
from fastapi.templating import Jinja2Templates
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import uvicorn
|
| 7 |
+
import os
|
| 8 |
+
import schemas
|
| 9 |
+
import services
|
| 10 |
+
import config
|
| 11 |
+
|
| 12 |
+
# Get the base directory (works both locally and on Hugging Face)
|
| 13 |
+
if os.path.exists("/app"): # Hugging Face environment
|
| 14 |
+
BASE_DIR = Path("/app")
|
| 15 |
+
else: # Local environment
|
| 16 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 17 |
+
|
| 18 |
+
app = FastAPI(
|
| 19 |
+
title="ContextIQ RAG - Intelligent Context-Aware Assistant",
|
| 20 |
+
description="A sophisticated RAG-powered backend using FastAPI and OpenRouter.",
|
| 21 |
+
version="2.0.0"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
# Mount static files and templates
|
| 25 |
+
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
|
| 26 |
+
templates = Jinja2Templates(directory=BASE_DIR / "templates")
|
| 27 |
+
|
| 28 |
+
# Your existing endpoints remain exactly the same
|
| 29 |
+
@app.post("/api/v1/index", response_model=schemas.IndexResponse)
|
| 30 |
+
async def index_context(document_request: schemas.DocumentRequest):
|
| 31 |
+
"""
|
| 32 |
+
Receives text, clears the old index, chunks the new text,
|
| 33 |
+
and stores its embeddings in the vector DB.
|
| 34 |
+
"""
|
| 35 |
+
try:
|
| 36 |
+
docs_added = services.index_document(document_request)
|
| 37 |
+
return schemas.IndexResponse(
|
| 38 |
+
message="Context has been successfully indexed.",
|
| 39 |
+
documents_added=docs_added
|
| 40 |
+
)
|
| 41 |
+
except Exception as e:
|
| 42 |
+
raise HTTPException(status_code=500, detail=f"Failed to index document: {e}")
|
| 43 |
+
|
| 44 |
+
@app.post("/api/v1/clear_index", response_model=schemas.GeneralResponse)
|
| 45 |
+
async def clear_context_index():
|
| 46 |
+
"""
|
| 47 |
+
Clears all data from the vector database index.
|
| 48 |
+
"""
|
| 49 |
+
try:
|
| 50 |
+
services.clear_index()
|
| 51 |
+
return schemas.GeneralResponse(message="Knowledge base has been successfully cleared.")
|
| 52 |
+
except Exception as e:
|
| 53 |
+
raise HTTPException(status_code=500, detail=f"Failed to clear index: {e}")
|
| 54 |
+
|
| 55 |
+
@app.post("/api/v1/generate", response_model=schemas.ChatResponse)
|
| 56 |
+
async def generate_response(chat_request: schemas.ChatRequest):
|
| 57 |
+
"""
|
| 58 |
+
Receives a prompt, retrieves relevant context from the vector DB,
|
| 59 |
+
and returns an AI-generated response.
|
| 60 |
+
"""
|
| 61 |
+
if not config.settings.OPENROUTER_API_KEY:
|
| 62 |
+
raise HTTPException(
|
| 63 |
+
status_code=400,
|
| 64 |
+
detail="OpenRouter API key is not configured on the server."
|
| 65 |
+
)
|
| 66 |
+
try:
|
| 67 |
+
ai_message = await services.get_rag_response(chat_request)
|
| 68 |
+
return schemas.ChatResponse(response=ai_message)
|
| 69 |
+
except Exception as e:
|
| 70 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 71 |
+
|
| 72 |
+
@app.post("/api/v1/task", response_model=schemas.TaskResponse)
|
| 73 |
+
async def execute_task(task_request: schemas.TaskRequest):
|
| 74 |
+
"""
|
| 75 |
+
Executes a specific task (e.g., summarize, plan) based on the provided context.
|
| 76 |
+
"""
|
| 77 |
+
if not config.settings.OPENROUTER_API_KEY:
|
| 78 |
+
raise HTTPException(
|
| 79 |
+
status_code=400,
|
| 80 |
+
detail="OpenRouter API key is not configured on the server."
|
| 81 |
+
)
|
| 82 |
+
try:
|
| 83 |
+
result = await services.execute_task(task_request)
|
| 84 |
+
return schemas.TaskResponse(result=result)
|
| 85 |
+
except Exception as e:
|
| 86 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 87 |
+
|
| 88 |
+
@app.get("/", response_class=HTMLResponse)
|
| 89 |
+
async def read_root(request: Request):
|
| 90 |
+
"""
|
| 91 |
+
Serves the main index.html page from the templates directory.
|
| 92 |
+
"""
|
| 93 |
+
return templates.TemplateResponse("index.html", {"request": request})
|
| 94 |
+
|
| 95 |
+
@app.get("/health")
|
| 96 |
+
async def health_check():
|
| 97 |
+
"""Health check endpoint for Hugging Face."""
|
| 98 |
+
return {"status": "healthy", "message": "ContextIQ RAG is running!"}
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
port = int(os.environ.get("PORT", 7860))
|
| 102 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
app/rag_setup.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import chromadb
|
| 2 |
+
import logging
|
| 3 |
+
from openai import OpenAI
|
| 4 |
+
from app.config import settings
|
| 5 |
+
from chromadb.utils import embedding_functions
|
| 6 |
+
import time
|
| 7 |
+
import os
|
| 8 |
+
import shutil
|
| 9 |
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
| 10 |
+
logger = logging.getLogger("rag-setup")
|
| 11 |
+
|
| 12 |
+
# Initialize variables
|
| 13 |
+
embedding_model = None
|
| 14 |
+
generation_model = None
|
| 15 |
+
|
| 16 |
+
# Use the default SentenceTransformer for creating embeddings locally.
|
| 17 |
+
# This is efficient as it doesn't require an API call for embedding.
|
| 18 |
+
sentence_transformer_ef = embedding_functions.SentenceTransformerEmbeddingFunction()
|
| 19 |
+
db_path = "./chroma_db"
|
| 20 |
+
if os.path.exists(db_path):
|
| 21 |
+
logger.info("Removing existing ChromaDB to avoid embedding conflicts")
|
| 22 |
+
shutil.rmtree(db_path)
|
| 23 |
+
|
| 24 |
+
# Set up a persistent ChromaDB client
|
| 25 |
+
client = chromadb.PersistentClient(path="./chroma_db")
|
| 26 |
+
|
| 27 |
+
collection = client.get_or_create_collection(
|
| 28 |
+
name="context_aware_collection",
|
| 29 |
+
embedding_function=sentence_transformer_ef
|
| 30 |
+
)
|
| 31 |
+
logger.info("ChromaDB collection 'context_aware_collection' loaded/created.")
|
| 32 |
+
class OpenRouterLLM:
|
| 33 |
+
def __init__(self, api_key: str, base_url: str, model: str):
|
| 34 |
+
if not api_key:
|
| 35 |
+
raise ValueError("OpenRouter API key is missing. Please set it in your .env file.")
|
| 36 |
+
self.client = OpenAI(base_url=base_url, api_key=api_key,timeout=45.0)
|
| 37 |
+
self.model = model
|
| 38 |
+
logger.info(f"OpenRouter client initialized for model: {self.model}")
|
| 39 |
+
|
| 40 |
+
def generate_content(self, prompt: str) -> str:
|
| 41 |
+
max_retries = 2
|
| 42 |
+
retry_count = 0
|
| 43 |
+
|
| 44 |
+
while retry_count <= max_retries:
|
| 45 |
+
try:
|
| 46 |
+
completion = self.client.chat.completions.create(
|
| 47 |
+
extra_headers={
|
| 48 |
+
"HTTP-Referer": "https://github.com/Ab-Romia/ContextIQ-RAG",
|
| 49 |
+
"X-Title": "Context Aware AI",
|
| 50 |
+
},
|
| 51 |
+
model=self.model,
|
| 52 |
+
messages=[{"role": "user", "content": prompt}],
|
| 53 |
+
max_tokens=6000
|
| 54 |
+
)
|
| 55 |
+
return completion.choices[0].message.content
|
| 56 |
+
except Exception as e:
|
| 57 |
+
logger.error(f"API call failed (attempt {retry_count + 1}): {str(e)}")
|
| 58 |
+
retry_count += 1
|
| 59 |
+
if retry_count > max_retries:
|
| 60 |
+
return f"Error: The model did not respond in time. Try a shorter question or simplify your context."
|
| 61 |
+
time.sleep(2) # Wait before retrying
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
# Initialize the generation model with settings from config.py
|
| 65 |
+
generation_model = OpenRouterLLM(
|
| 66 |
+
api_key=settings.OPENROUTER_API_KEY,
|
| 67 |
+
base_url=settings.OPENROUTER_URL,
|
| 68 |
+
model=settings.MODEL_NAME
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
logger.info("RAG setup initialized successfully.")
|
app/schemas.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, Field
|
| 2 |
+
from typing import Optional
|
| 3 |
+
|
| 4 |
+
class DocumentRequest(BaseModel):
|
| 5 |
+
"""
|
| 6 |
+
Schema for the request to index a new document.
|
| 7 |
+
"""
|
| 8 |
+
context: str = Field(
|
| 9 |
+
...,
|
| 10 |
+
min_length=10,
|
| 11 |
+
description="The full document or text to be indexed."
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
class ChatRequest(BaseModel):
|
| 15 |
+
"""
|
| 16 |
+
Schema for the request to generate a response.
|
| 17 |
+
"""
|
| 18 |
+
prompt: str = Field(
|
| 19 |
+
...,
|
| 20 |
+
min_length=2,
|
| 21 |
+
description="The user's question to be answered based on the indexed context."
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
class TaskRequest(BaseModel):
|
| 25 |
+
"""
|
| 26 |
+
Schema for executing a specific task like summarization or planning.
|
| 27 |
+
"""
|
| 28 |
+
context: str = Field(..., description="The full context for the task.")
|
| 29 |
+
task_type: str = Field(..., description="The type of task to perform (e.g., 'summarize', 'plan').")
|
| 30 |
+
prompt: Optional[str] = Field(None, description="An optional prompt to guide the task.")
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class ChatResponse(BaseModel):
|
| 34 |
+
"""
|
| 35 |
+
Schema for the AI's response.
|
| 36 |
+
"""
|
| 37 |
+
response: str
|
| 38 |
+
|
| 39 |
+
class TaskResponse(BaseModel):
|
| 40 |
+
"""
|
| 41 |
+
Schema for the result of a task.
|
| 42 |
+
"""
|
| 43 |
+
result: str
|
| 44 |
+
|
| 45 |
+
class IndexResponse(BaseModel):
|
| 46 |
+
"""
|
| 47 |
+
Schema for the response after indexing a document.
|
| 48 |
+
"""
|
| 49 |
+
message: str
|
| 50 |
+
documents_added: int
|
| 51 |
+
|
| 52 |
+
class GeneralResponse(BaseModel):
|
| 53 |
+
"""
|
| 54 |
+
A generic response model for simple status messages.
|
| 55 |
+
"""
|
| 56 |
+
message: str
|
app/services.py
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import functools
|
| 3 |
+
import logging
|
| 4 |
+
import textwrap
|
| 5 |
+
import time
|
| 6 |
+
import rag_setup
|
| 7 |
+
from schemas import ChatRequest, DocumentRequest, TaskRequest
|
| 8 |
+
logging.basicConfig(
|
| 9 |
+
level=logging.INFO,
|
| 10 |
+
format='%(asctime)s [%(levelname)s] %(message)s',
|
| 11 |
+
datefmt='%H:%M:%S'
|
| 12 |
+
)
|
| 13 |
+
logger = logging.getLogger("rag-service")
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# A simple cache to store recent responses to avoid redundant API calls for the same query.
|
| 17 |
+
# The cache stores a tuple of (timestamp, response).
|
| 18 |
+
_response_cache = {}
|
| 19 |
+
CACHE_EXPIRATION_SECONDS = 600 # 10 minutes
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def index_document(request_data: DocumentRequest) -> int:
|
| 23 |
+
logger.info("Starting document indexing process.")
|
| 24 |
+
|
| 25 |
+
try:
|
| 26 |
+
# Step 1: Clear any existing documents properly
|
| 27 |
+
existing_ids = rag_setup.collection.get()["ids"]
|
| 28 |
+
if existing_ids:
|
| 29 |
+
rag_setup.collection.delete(ids=existing_ids)
|
| 30 |
+
logger.info("Cleared existing documents from vector collection.")
|
| 31 |
+
|
| 32 |
+
# Step 2: Chunk document
|
| 33 |
+
text_chunks = textwrap.wrap(
|
| 34 |
+
request_data.context,
|
| 35 |
+
width=800,
|
| 36 |
+
break_long_words=False,
|
| 37 |
+
replace_whitespace=False
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
if not text_chunks:
|
| 41 |
+
logger.warning("No text chunks were generated.")
|
| 42 |
+
return 0
|
| 43 |
+
|
| 44 |
+
# Step 3: Add chunks to ChromaDB
|
| 45 |
+
chunk_ids = [f"doc_chunk_{i}_{int(time.time())}" for i in range(len(text_chunks))]
|
| 46 |
+
logger.info(f"Attempting to add {len(chunk_ids)} chunks to ChromaDB...")
|
| 47 |
+
rag_setup.collection.add(documents=text_chunks, ids=chunk_ids)
|
| 48 |
+
|
| 49 |
+
return len(text_chunks)
|
| 50 |
+
except Exception as e:
|
| 51 |
+
logger.error(f"Error during indexing: {str(e)}", exc_info=True)
|
| 52 |
+
raise
|
| 53 |
+
|
| 54 |
+
def clear_index():
|
| 55 |
+
"""Clears all documents from the vector database."""
|
| 56 |
+
rag_setup.collection.delete(where={})
|
| 57 |
+
logger.info("Successfully cleared the vector index.")
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
async def get_rag_response(request_data: ChatRequest) -> str:
|
| 61 |
+
"""
|
| 62 |
+
Performs the RAG pipeline: checks cache, retrieves context, generates a response.
|
| 63 |
+
"""
|
| 64 |
+
start_total = time.time()
|
| 65 |
+
logger.info(f"Processing query: '{request_data.prompt}'")
|
| 66 |
+
|
| 67 |
+
try:
|
| 68 |
+
# Step 1: Check cache for a recent, identical query
|
| 69 |
+
cached_response = _get_cached_response(request_data.prompt)
|
| 70 |
+
if cached_response:
|
| 71 |
+
logger.info("Cache hit! Returning cached response.")
|
| 72 |
+
return f"{cached_response}\n\n(This response was retrieved from cache)"
|
| 73 |
+
|
| 74 |
+
logger.info("Cache miss. Proceeding with RAG pipeline.")
|
| 75 |
+
|
| 76 |
+
# Step 2: Check if the vector database has any content
|
| 77 |
+
if rag_setup.collection.count() == 0:
|
| 78 |
+
logger.warning("Vector DB is empty. Cannot answer query.")
|
| 79 |
+
return "The knowledge base is empty. Please provide some context in the left panel and click 'Index Context' before asking questions."
|
| 80 |
+
|
| 81 |
+
# Step 3: Retrieve relevant chunks from ChromaDB
|
| 82 |
+
logger.info("Retrieving relevant chunks from vector DB...")
|
| 83 |
+
retrieved_chunks = await _retrieve_chunks_async(request_data.prompt)
|
| 84 |
+
|
| 85 |
+
if not retrieved_chunks or not retrieved_chunks.get('documents') or not retrieved_chunks['documents'][0]:
|
| 86 |
+
logger.warning("No relevant chunks found in the vector DB for this query.")
|
| 87 |
+
return "I could not find any relevant information in the provided context to answer your question."
|
| 88 |
+
|
| 89 |
+
context_for_prompt = "\n\n---\n\n".join(retrieved_chunks['documents'][0])
|
| 90 |
+
|
| 91 |
+
# Step 4: Construct the final prompt for the LLM
|
| 92 |
+
full_prompt = (
|
| 93 |
+
"You are a helpful AI assistant. Based strictly and only on the following context, "
|
| 94 |
+
"please answer the user's question. Do not use any external knowledge or make assumptions. "
|
| 95 |
+
"If the answer cannot be found in the context, state that clearly.\n\n"
|
| 96 |
+
"--- CONTEXT START ---\n"
|
| 97 |
+
f"{context_for_prompt}\n"
|
| 98 |
+
"--- CONTEXT END ---\n\n"
|
| 99 |
+
f'User\'s Question: "{request_data.prompt}"'
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
# Step 5: Generate the response using the LLM
|
| 103 |
+
logger.info("Generating response from OpenRouter...")
|
| 104 |
+
response_text = await _generate_response_async(full_prompt)
|
| 105 |
+
|
| 106 |
+
# Step 6: Cache the newly generated response
|
| 107 |
+
_cache_response(request_data.prompt, response_text)
|
| 108 |
+
|
| 109 |
+
total_time = time.time() - start_total
|
| 110 |
+
logger.info(f"Total processing time: {total_time:.2f}s")
|
| 111 |
+
return response_text
|
| 112 |
+
|
| 113 |
+
except asyncio.TimeoutError:
|
| 114 |
+
logger.error("Request timed out during retrieval or generation.")
|
| 115 |
+
return "The request timed out. Please try again or simplify your question."
|
| 116 |
+
except Exception as e:
|
| 117 |
+
logger.error(f"An unexpected error occurred: {e}", exc_info=True)
|
| 118 |
+
return f"An unexpected error occurred: {e}"
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
async def execute_task(request_data: TaskRequest) -> str:
|
| 122 |
+
"""
|
| 123 |
+
Executes a specific task on the given context.
|
| 124 |
+
"""
|
| 125 |
+
start_total = time.time()
|
| 126 |
+
logger.info(f"Executing task '{request_data.task_type}' with prompt: '{request_data.prompt}'")
|
| 127 |
+
|
| 128 |
+
try:
|
| 129 |
+
# For tasks, we use the full context, not just retrieved chunks
|
| 130 |
+
context = request_data.context
|
| 131 |
+
if not context:
|
| 132 |
+
return "Context is empty. Please provide some text in the 'Knowledge Base' to perform a task."
|
| 133 |
+
|
| 134 |
+
# Construct the prompt based on the task type
|
| 135 |
+
if request_data.task_type == "summarize":
|
| 136 |
+
full_prompt = f"Summarize the following text:\n\n---\n{context}"
|
| 137 |
+
elif request_data.task_type == "plan":
|
| 138 |
+
full_prompt = f"Based on the following context, create a detailed action plan. If a specific goal is provided in the prompt, tailor the plan to that goal.\n\n--- CONTEXT ---\n{context}\n\n--- GOAL ---\n{request_data.prompt or 'General objective derived from context'}"
|
| 139 |
+
elif request_data.task_type == "creative":
|
| 140 |
+
full_prompt = f"Use the following text as inspiration to write a creative piece (e.g., a poem, a short story, a metaphor). The user's prompt can guide the style or topic.\n\n--- INSPIRATION ---\n{context}\n\n--- PROMPT ---\n{request_data.prompt or 'Write a short poem'}"
|
| 141 |
+
else:
|
| 142 |
+
return "Invalid task type specified."
|
| 143 |
+
|
| 144 |
+
# Generate the response
|
| 145 |
+
logger.info("Generating task-based response from OpenRouter...")
|
| 146 |
+
response_text = await _generate_response_async(full_prompt)
|
| 147 |
+
|
| 148 |
+
total_time = time.time() - start_total
|
| 149 |
+
logger.info(f"Task execution time: {total_time:.2f}s")
|
| 150 |
+
return response_text
|
| 151 |
+
|
| 152 |
+
except asyncio.TimeoutError:
|
| 153 |
+
logger.error("Request timed out during task execution.")
|
| 154 |
+
return "The request timed out. Please try again."
|
| 155 |
+
except Exception as e:
|
| 156 |
+
logger.error(f"An unexpected error occurred during task execution: {e}", exc_info=True)
|
| 157 |
+
return f"An unexpected error occurred: {e}"
|
| 158 |
+
|
| 159 |
+
# --- ASYNC WRAPPERS & CACHE HELPERS ---
|
| 160 |
+
|
| 161 |
+
async def _retrieve_chunks_async(prompt: str):
|
| 162 |
+
"""Asynchronously queries the ChromaDB collection."""
|
| 163 |
+
loop = asyncio.get_event_loop()
|
| 164 |
+
return await loop.run_in_executor(
|
| 165 |
+
None,
|
| 166 |
+
functools.partial(rag_setup.collection.query, query_texts=[prompt], n_results=3)
|
| 167 |
+
)
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
async def _generate_response_async(full_prompt: str):
|
| 171 |
+
"""Asynchronously calls the LLM to generate content."""
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
loop = asyncio.get_event_loop()
|
| 175 |
+
return await loop.run_in_executor(
|
| 176 |
+
None,
|
| 177 |
+
rag_setup.generation_model.generate_content,
|
| 178 |
+
full_prompt
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
def _get_cached_response(key: str):
|
| 182 |
+
"""Checks the cache for a valid (non-expired) entry."""
|
| 183 |
+
if key in _response_cache:
|
| 184 |
+
timestamp, response = _response_cache[key]
|
| 185 |
+
if time.time() - timestamp < CACHE_EXPIRATION_SECONDS:
|
| 186 |
+
return response
|
| 187 |
+
else:
|
| 188 |
+
# Expired, remove from cache
|
| 189 |
+
del _response_cache[key]
|
| 190 |
+
return None
|
| 191 |
+
|
| 192 |
+
|
| 193 |
+
def _cache_response(key: str, response: str):
|
| 194 |
+
"""Adds a response to the cache with the current timestamp."""
|
| 195 |
+
_response_cache[key] = (time.time(), response)
|
main.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
# Add the app directory to Python path
|
| 6 |
+
current_dir = Path(__file__).parent
|
| 7 |
+
app_dir = current_dir / "app"
|
| 8 |
+
sys.path.insert(0, str(app_dir))
|
| 9 |
+
sys.path.insert(0, str(current_dir))
|
| 10 |
+
|
| 11 |
+
# Import and run the FastAPI app
|
| 12 |
+
from app.main import app
|
| 13 |
+
import uvicorn
|
| 14 |
+
|
| 15 |
+
if __name__ == "__main__":
|
| 16 |
+
port = int(os.environ.get("PORT", 7860)) # Hugging Face uses port 7860
|
| 17 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn[standard]==0.24.0
|
| 3 |
+
pydantic==2.5.0
|
| 4 |
+
pydantic-settings==2.1.0
|
| 5 |
+
chromadb==0.4.15
|
| 6 |
+
openai==1.3.7
|
| 7 |
+
jinja2==3.1.2
|
| 8 |
+
python-multipart==0.0.6
|
| 9 |
+
numpy==1.24.3
|
| 10 |
+
requests==2.31.0
|
static/app.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ContextAwareApp {
|
| 2 |
+
constructor() {
|
| 3 |
+
// Centralized DOM element references
|
| 4 |
+
this.elements = {
|
| 5 |
+
contextInput: document.getElementById('context-input'),
|
| 6 |
+
chatInput: document.getElementById('chat-input'),
|
| 7 |
+
sendButton: document.getElementById('send-button'),
|
| 8 |
+
chatContainer: document.getElementById('chat-container'),
|
| 9 |
+
statusIndicator: document.getElementById('status-indicator'),
|
| 10 |
+
clearContextBtn: document.getElementById('clear-context-btn'),
|
| 11 |
+
indexContextBtn: document.getElementById('index-context-btn'),
|
| 12 |
+
taskSelect: document.getElementById('task-select'),
|
| 13 |
+
charCount: document.getElementById('char-count'),
|
| 14 |
+
wordCount: document.getElementById('word-count'),
|
| 15 |
+
};
|
| 16 |
+
// Application state
|
| 17 |
+
this.state = {
|
| 18 |
+
isIndexing: false,
|
| 19 |
+
isGenerating: false,
|
| 20 |
+
isIndexed: false,
|
| 21 |
+
};
|
| 22 |
+
this.initialize();
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
/**
|
| 26 |
+
* Sets up the application, event listeners, and initial state.
|
| 27 |
+
*/
|
| 28 |
+
initialize() {
|
| 29 |
+
this.addEventListeners();
|
| 30 |
+
this.addMessageToChat(
|
| 31 |
+
"Welcome! Here's how to get started:\n1. Paste your context into the 'Knowledge Base' on the left.\n2. Click 'Index Context' for Q&A, or select a different action from the dropdown.\n3. Provide a prompt if needed and click the send button.",
|
| 32 |
+
'ai'
|
| 33 |
+
);
|
| 34 |
+
this.updateUI();
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
/**
|
| 38 |
+
* Binds all necessary event listeners to DOM elements.
|
| 39 |
+
*/
|
| 40 |
+
addEventListeners() {
|
| 41 |
+
this.elements.indexContextBtn.addEventListener('click', () => this.handleIndexContext());
|
| 42 |
+
this.elements.clearContextBtn.addEventListener('click', () => this.handleClearContext());
|
| 43 |
+
this.elements.sendButton.addEventListener('click', () => this.handleSubmit());
|
| 44 |
+
this.elements.chatInput.addEventListener('keydown', e => {
|
| 45 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 46 |
+
e.preventDefault();
|
| 47 |
+
this.handleSubmit();
|
| 48 |
+
}
|
| 49 |
+
});
|
| 50 |
+
this.elements.contextInput.addEventListener('input', () => this.updateContextStats());
|
| 51 |
+
this.elements.chatInput.addEventListener('input', () => this.autoResizeTextarea(this.elements.chatInput));
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
/**
|
| 55 |
+
* Main handler for the send button. Directs to the correct function based on the selected task.
|
| 56 |
+
*/
|
| 57 |
+
handleSubmit() {
|
| 58 |
+
const selectedTask = this.elements.taskSelect.value;
|
| 59 |
+
if (selectedTask === 'q_and_a') {
|
| 60 |
+
this.handleSendPrompt();
|
| 61 |
+
} else {
|
| 62 |
+
this.handleExecuteTask();
|
| 63 |
+
}
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
/**
|
| 68 |
+
* Handles the logic for indexing the provided context.
|
| 69 |
+
*/
|
| 70 |
+
async handleIndexContext() {
|
| 71 |
+
const context = this.elements.contextInput.value.trim();
|
| 72 |
+
if (context.length < 20) {
|
| 73 |
+
this.showStatus('Context is too short. Please provide at least 20 characters.', 'error');
|
| 74 |
+
return;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
this.state.isIndexing = true;
|
| 78 |
+
this.updateUI();
|
| 79 |
+
this.showStatus('Indexing context... This may take a moment.', 'loading');
|
| 80 |
+
|
| 81 |
+
try {
|
| 82 |
+
const response = await fetch('/api/v1/index', {
|
| 83 |
+
method: 'POST',
|
| 84 |
+
headers: { 'Content-Type': 'application/json' },
|
| 85 |
+
body: JSON.stringify({ context }),
|
| 86 |
+
});
|
| 87 |
+
|
| 88 |
+
if (!response.ok) throw new Error(`Server error: ${response.statusText}`);
|
| 89 |
+
|
| 90 |
+
const result = await response.json();
|
| 91 |
+
this.state.isIndexed = true;
|
| 92 |
+
this.showStatus(`Successfully indexed ${result.documents_added} document chunks.`, 'success');
|
| 93 |
+
} catch (error) {
|
| 94 |
+
this.showStatus(`Error indexing context: ${error.message}`, 'error');
|
| 95 |
+
this.state.isIndexed = false;
|
| 96 |
+
} finally {
|
| 97 |
+
this.state.isIndexing = false;
|
| 98 |
+
this.updateUI();
|
| 99 |
+
}
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
/**
|
| 103 |
+
* Handles sending a user's prompt to the backend for a response.
|
| 104 |
+
*/
|
| 105 |
+
async handleSendPrompt() {
|
| 106 |
+
const prompt = this.elements.chatInput.value.trim();
|
| 107 |
+
if (prompt.length < 2 || this.state.isGenerating || !this.state.isIndexed) {
|
| 108 |
+
if (!this.state.isIndexed) {
|
| 109 |
+
this.showStatus('Please index your context before asking questions.', 'error');
|
| 110 |
+
}
|
| 111 |
+
return;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
this.addMessageToChat(prompt, 'user');
|
| 115 |
+
this.elements.chatInput.value = '';
|
| 116 |
+
this.autoResizeTextarea(this.elements.chatInput);
|
| 117 |
+
|
| 118 |
+
this.state.isGenerating = true;
|
| 119 |
+
this.updateUI();
|
| 120 |
+
this.showStatus('AI is thinking...', 'loading');
|
| 121 |
+
|
| 122 |
+
try {
|
| 123 |
+
const response = await fetch('/api/v1/generate', {
|
| 124 |
+
method: 'POST',
|
| 125 |
+
headers: { 'Content-Type': 'application/json' },
|
| 126 |
+
body: JSON.stringify({ prompt }),
|
| 127 |
+
});
|
| 128 |
+
|
| 129 |
+
if (!response.ok) {
|
| 130 |
+
const errorBody = await response.json();
|
| 131 |
+
throw new Error(errorBody.detail || 'An unknown error occurred.');
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
const result = await response.json();
|
| 135 |
+
this.addMessageToChat(result.response, 'ai');
|
| 136 |
+
this.showStatus('Ready for your next question.', 'success');
|
| 137 |
+
} catch (error) {
|
| 138 |
+
this.addMessageToChat(`An error occurred: ${error.message}`, 'system');
|
| 139 |
+
this.showStatus(`Error: ${error.message}`, 'error');
|
| 140 |
+
} finally {
|
| 141 |
+
this.state.isGenerating = false;
|
| 142 |
+
this.updateUI();
|
| 143 |
+
}
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
/**
|
| 147 |
+
* Handles executing a non-Q&A task like summarization or planning.
|
| 148 |
+
*/
|
| 149 |
+
async handleExecuteTask() {
|
| 150 |
+
const context = this.elements.contextInput.value.trim();
|
| 151 |
+
const task_type = this.elements.taskSelect.value;
|
| 152 |
+
const prompt = this.elements.chatInput.value.trim();
|
| 153 |
+
|
| 154 |
+
if (context.length < 20) {
|
| 155 |
+
this.showStatus('Please provide at least 20 characters of context for this task.', 'error');
|
| 156 |
+
return;
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
let userMessage = `Task: ${task_type}`;
|
| 160 |
+
if (prompt) {
|
| 161 |
+
userMessage += `\nPrompt: ${prompt}`;
|
| 162 |
+
}
|
| 163 |
+
this.addMessageToChat(userMessage, 'user');
|
| 164 |
+
|
| 165 |
+
this.elements.chatInput.value = '';
|
| 166 |
+
this.autoResizeTextarea(this.elements.chatInput);
|
| 167 |
+
|
| 168 |
+
this.state.isGenerating = true;
|
| 169 |
+
this.updateUI();
|
| 170 |
+
this.showStatus('AI is performing the task...', 'loading');
|
| 171 |
+
|
| 172 |
+
try {
|
| 173 |
+
const response = await fetch('/api/v1/task', {
|
| 174 |
+
method: 'POST',
|
| 175 |
+
headers: { 'Content-Type': 'application/json' },
|
| 176 |
+
body: JSON.stringify({ context, task_type, prompt }),
|
| 177 |
+
});
|
| 178 |
+
|
| 179 |
+
if (!response.ok) {
|
| 180 |
+
const errorBody = await response.json();
|
| 181 |
+
throw new Error(errorBody.detail || 'An unknown error occurred.');
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
const result = await response.json();
|
| 185 |
+
this.addMessageToChat(result.result, 'ai');
|
| 186 |
+
this.showStatus('Task completed successfully.', 'success');
|
| 187 |
+
} catch (error) {
|
| 188 |
+
this.addMessageToChat(`An error occurred: ${error.message}`, 'system');
|
| 189 |
+
this.showStatus(`Error: ${error.message}`, 'error');
|
| 190 |
+
} finally {
|
| 191 |
+
this.state.isGenerating = false;
|
| 192 |
+
this.updateUI();
|
| 193 |
+
}
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
|
| 197 |
+
/**
|
| 198 |
+
* Clears the context input and the indexed data on the backend.
|
| 199 |
+
*/
|
| 200 |
+
async handleClearContext() {
|
| 201 |
+
this.elements.contextInput.value = '';
|
| 202 |
+
this.updateContextStats();
|
| 203 |
+
this.state.isIndexed = false;
|
| 204 |
+
this.updateUI();
|
| 205 |
+
this.showStatus('Clearing knowledge base...', 'loading');
|
| 206 |
+
|
| 207 |
+
try {
|
| 208 |
+
await fetch('/api/v1/clear_index', { method: 'POST' });
|
| 209 |
+
this.showStatus('Knowledge base cleared. Ready for new context.', 'success');
|
| 210 |
+
} catch (error) {
|
| 211 |
+
this.showStatus(`Error clearing index: ${error.message}`, 'error');
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
|
| 215 |
+
/**
|
| 216 |
+
* Updates all UI elements based on the current application state.
|
| 217 |
+
*/
|
| 218 |
+
updateUI() {
|
| 219 |
+
const hasContext = this.elements.contextInput.value.trim().length > 10;
|
| 220 |
+
const isQandA = this.elements.taskSelect.value === 'q_and_a';
|
| 221 |
+
|
| 222 |
+
this.elements.indexContextBtn.disabled = this.state.isIndexing || !hasContext;
|
| 223 |
+
|
| 224 |
+
if (isQandA) {
|
| 225 |
+
this.elements.sendButton.disabled = this.state.isGenerating || !this.state.isIndexed;
|
| 226 |
+
} else {
|
| 227 |
+
this.elements.sendButton.disabled = this.state.isGenerating || !hasContext;
|
| 228 |
+
}
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
/**
|
| 233 |
+
* Displays a status message to the user.
|
| 234 |
+
* @param {string} message - The message to display.
|
| 235 |
+
* @param {'loading'|'success'|'error'} type - The type of message.
|
| 236 |
+
*/
|
| 237 |
+
showStatus(message, type) {
|
| 238 |
+
const indicator = this.elements.statusIndicator;
|
| 239 |
+
indicator.classList.remove('hidden');
|
| 240 |
+
let colorClass = 'text-slate-400';
|
| 241 |
+
if (type === 'success') colorClass = 'text-green-400';
|
| 242 |
+
if (type === 'error') colorClass = 'text-red-400';
|
| 243 |
+
|
| 244 |
+
indicator.innerHTML = `<span class="${colorClass}">${message}</span>`;
|
| 245 |
+
|
| 246 |
+
// Hide the message after a delay unless it's a loading indicator
|
| 247 |
+
if (type !== 'loading') {
|
| 248 |
+
setTimeout(() => indicator.classList.add('hidden'), 5000);
|
| 249 |
+
}
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
/**
|
| 253 |
+
* Adds a new message to the chat display.
|
| 254 |
+
* @param {string} message - The message content.
|
| 255 |
+
* @param {'user'|'ai'|'system'} sender - The sender of the message.
|
| 256 |
+
*/
|
| 257 |
+
addMessageToChat(message, sender) {
|
| 258 |
+
const messageDiv = document.createElement('div');
|
| 259 |
+
messageDiv.className = 'chat-message flex items-start space-x-3 animate-fade-in';
|
| 260 |
+
const timestamp = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
| 261 |
+
|
| 262 |
+
const icons = {
|
| 263 |
+
user: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`,
|
| 264 |
+
ai: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>`,
|
| 265 |
+
system: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>`
|
| 266 |
+
};
|
| 267 |
+
const bubbleClasses = {
|
| 268 |
+
user: 'bg-gradient-to-br from-emerald-500 to-teal-600',
|
| 269 |
+
ai: 'bg-gradient-to-br from-indigo-500 to-purple-600',
|
| 270 |
+
system: 'bg-gradient-to-br from-red-500 to-orange-600'
|
| 271 |
+
};
|
| 272 |
+
|
| 273 |
+
// Use marked to parse markdown, but sanitize first
|
| 274 |
+
const formattedMessage = sender === 'user' ?
|
| 275 |
+
this.escapeHtml(message).replace(/\n/g, '<br>') :
|
| 276 |
+
marked.parse(message);
|
| 277 |
+
|
| 278 |
+
messageDiv.innerHTML = `
|
| 279 |
+
<div class="w-8 h-8 ${bubbleClasses[sender]} rounded-full flex items-center justify-center flex-shrink-0 text-white">${icons[sender]}</div>
|
| 280 |
+
<div class="flex-1">
|
| 281 |
+
<div class="bg-slate-800/50 rounded-xl p-4 border border-slate-600/30">
|
| 282 |
+
<div class="text-slate-200 leading-relaxed markdown-content">${formattedMessage}</div>
|
| 283 |
+
</div>
|
| 284 |
+
<div class="text-xs text-slate-500 mt-2">${timestamp}</div>
|
| 285 |
+
</div>`;
|
| 286 |
+
|
| 287 |
+
this.elements.chatContainer.appendChild(messageDiv);
|
| 288 |
+
this.elements.chatContainer.scrollTop = this.elements.chatContainer.scrollHeight;
|
| 289 |
+
}
|
| 290 |
+
|
| 291 |
+
/**
|
| 292 |
+
* Updates character and word counts for the context input.
|
| 293 |
+
*/
|
| 294 |
+
updateContextStats() {
|
| 295 |
+
const text = this.elements.contextInput.value;
|
| 296 |
+
this.elements.charCount.textContent = text.length.toLocaleString();
|
| 297 |
+
this.elements.wordCount.textContent = (text.trim().split(/\s+/).filter(Boolean).length).toLocaleString();
|
| 298 |
+
this.updateUI();
|
| 299 |
+
}
|
| 300 |
+
|
| 301 |
+
autoResizeTextarea(element) {
|
| 302 |
+
element.style.height = 'auto';
|
| 303 |
+
element.style.height = `${Math.min(element.scrollHeight, 120)}px`;
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
escapeHtml(text) {
|
| 307 |
+
const div = document.createElement('div');
|
| 308 |
+
div.textContent = text;
|
| 309 |
+
return div.innerHTML;
|
| 310 |
+
}
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 314 |
+
new ContextAwareApp();
|
| 315 |
+
});
|
templates/index.html
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>ContextIQ - Intelligent Context-Aware Assistant</title>
|
| 7 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
| 9 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 10 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 11 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
| 12 |
+
<script>
|
| 13 |
+
tailwind.config = {
|
| 14 |
+
theme: {
|
| 15 |
+
extend: {
|
| 16 |
+
fontFamily: { 'inter': ['Inter', 'sans-serif'] },
|
| 17 |
+
animation: {
|
| 18 |
+
'pulse-slow': 'pulse 4s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
| 19 |
+
'fade-in': 'fadeIn 0.5s ease-in-out',
|
| 20 |
+
},
|
| 21 |
+
keyframes: {
|
| 22 |
+
fadeIn: {
|
| 23 |
+
'0%': { opacity: '0', transform: 'translateY(10px)' },
|
| 24 |
+
'100%': { opacity: '1', transform: 'translateY(0)' }
|
| 25 |
+
},
|
| 26 |
+
}
|
| 27 |
+
}
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
</script>
|
| 31 |
+
<style>
|
| 32 |
+
body { font-family: 'Inter', sans-serif; background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); }
|
| 33 |
+
.glass-effect { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(148, 163, 184, 0.1); }
|
| 34 |
+
.gradient-text { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #06b6d4 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
| 35 |
+
.typing-indicator { display: inline-flex; align-items: center; }
|
| 36 |
+
.typing-dot { width: 6px; height: 6px; border-radius: 50%; background-color: #8b5cf6; animation: typing 1.4s infinite ease-in-out; margin-right: 4px; }
|
| 37 |
+
.typing-dot:nth-child(1) { animation-delay: -0.32s; }
|
| 38 |
+
.typing-dot:nth-child(2) { animation-delay: -0.16s; }
|
| 39 |
+
@keyframes typing { 0%, 80%, 100% { transform: scale(0.8); opacity: 0.3; } 40% { transform: scale(1); opacity: 1; } }
|
| 40 |
+
.scroll-container::-webkit-scrollbar { width: 6px; }
|
| 41 |
+
.scroll-container::-webkit-scrollbar-track { background: transparent; }
|
| 42 |
+
.scroll-container::-webkit-scrollbar-thumb { background: #475569; border-radius: 3px; }
|
| 43 |
+
.markdown-content { word-wrap: break-word; }
|
| 44 |
+
.markdown-content p { margin-bottom: 0.75rem; }
|
| 45 |
+
.markdown-content p:last-child { margin-bottom: 0; }
|
| 46 |
+
.markdown-content pre { background-color: rgba(15, 23, 42, 0.5); padding: 0.75rem; border-radius: 0.375rem; overflow-x: auto; }
|
| 47 |
+
.markdown-content code { font-family: monospace; background-color: rgba(15, 23, 42, 0.5); padding: 0.1rem 0.3rem; border-radius: 0.25rem; }
|
| 48 |
+
.markdown-content pre code { padding: 0; background-color: transparent; }
|
| 49 |
+
.markdown-content ul, .markdown-content ol { margin-left: 1.5rem; margin-bottom: 0.75rem; }
|
| 50 |
+
.markdown-content ul { list-style-type: disc; }
|
| 51 |
+
.markdown-content ol { list-style-type: decimal; }
|
| 52 |
+
.markdown-content table { border-collapse: collapse; width: 100%; margin-bottom: 0.75rem; }
|
| 53 |
+
.markdown-content th, .markdown-content td { border: 1px solid rgba(148, 163, 184, 0.2); padding: 0.5rem; text-align: left; }
|
| 54 |
+
|
| 55 |
+
</style>
|
| 56 |
+
</head>
|
| 57 |
+
<body class="font-inter text-white overflow-hidden">
|
| 58 |
+
<div class="fixed inset-0 -z-10">
|
| 59 |
+
<div class="absolute -top-40 -right-40 w-80 h-80 bg-purple-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse-slow"></div>
|
| 60 |
+
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-blue-500 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse-slow animation-delay-2000"></div>
|
| 61 |
+
</div>
|
| 62 |
+
|
| 63 |
+
<div class="relative z-10 flex items-center justify-center min-h-screen p-4">
|
| 64 |
+
<div class="w-[98%] h-[95vh] glass-effect rounded-3xl shadow-2xl flex flex-col">
|
| 65 |
+
|
| 66 |
+
<header class="flex items-center justify-between p-6 border-b border-slate-600/50">
|
| 67 |
+
<div class="flex items-center space-x-4">
|
| 68 |
+
<div class="w-10 h-10 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center">
|
| 69 |
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
| 70 |
+
</div>
|
| 71 |
+
<div>
|
| 72 |
+
<h1 class="text-2xl font-bold gradient-text">Context AI by Ab-Romia</h1>
|
| 73 |
+
<p class="text-sm text-slate-400">Refactored for Clarity & Efficiency</p>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
</header>
|
| 77 |
+
|
| 78 |
+
<div class="flex-1 flex overflow-hidden">
|
| 79 |
+
<div class="w-1/2 flex flex-col border-r border-slate-600/50">
|
| 80 |
+
<div class="p-6 border-b border-slate-600/30">
|
| 81 |
+
<h2 class="text-lg font-semibold text-slate-200">Knowledge Base</h2>
|
| 82 |
+
<p class="text-sm text-slate-400 mt-1">Provide context for the AI to learn from.</p>
|
| 83 |
+
</div>
|
| 84 |
+
<div class="flex-1 p-6 flex flex-col">
|
| 85 |
+
<textarea id="context-input" class="w-full h-full bg-slate-900/50 border border-slate-600/50 rounded-xl p-4 text-slate-200 placeholder-slate-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none resize-none transition scroll-container" placeholder="Paste your documents, meeting notes, or any relevant context here..."></textarea>
|
| 86 |
+
<div class="mt-4 flex items-center justify-between">
|
| 87 |
+
<div class="text-xs text-slate-400">
|
| 88 |
+
Chars: <span id="char-count">0</span> | Words: <span id="word-count">0</span>
|
| 89 |
+
</div>
|
| 90 |
+
<div class="flex items-center space-x-2">
|
| 91 |
+
<button id="clear-context-btn" class="px-3 py-1.5 text-sm bg-red-500/20 text-red-300 rounded-lg hover:bg-red-500/30 transition-colors">Clear</button>
|
| 92 |
+
<button id="index-context-btn" class="px-4 py-1.5 text-sm bg-indigo-600 text-white rounded-lg hover:bg-indigo-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">Index Context</button>
|
| 93 |
+
</div>
|
| 94 |
+
</div>
|
| 95 |
+
<div class="mt-4">
|
| 96 |
+
<label for="task-select" class="block text-sm font-medium text-slate-300 mb-2">Choose an Action:</label>
|
| 97 |
+
<select id="task-select" class="w-full bg-slate-900/50 border border-slate-600/50 rounded-lg p-2 text-slate-200 focus:ring-2 focus:ring-indigo-500">
|
| 98 |
+
<option value="q_and_a">Question & Answer</option>
|
| 99 |
+
<option value="summarize">Summarize</option>
|
| 100 |
+
<option value="plan">Generate Action Plan</option>
|
| 101 |
+
<option value="creative">Creative Writing</option>
|
| 102 |
+
</select>
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<div class="w-1/2 flex flex-col">
|
| 108 |
+
<div class="p-6 border-b border-slate-600/30">
|
| 109 |
+
<h2 class="text-lg font-semibold text-slate-200">AI Assistant</h2>
|
| 110 |
+
<p class="text-sm text-slate-400 mt-1">Ask questions or select a task.</p>
|
| 111 |
+
</div>
|
| 112 |
+
<div id="chat-container" class="flex-1 overflow-y-auto scroll-container p-6 space-y-6">
|
| 113 |
+
</div>
|
| 114 |
+
<div class="p-6 border-t border-slate-600/30">
|
| 115 |
+
<div class="relative">
|
| 116 |
+
<textarea id="chat-input" rows="1" class="w-full bg-slate-900/50 border border-slate-600/50 rounded-xl p-3 pr-12 text-slate-200 placeholder-slate-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none resize-none transition" placeholder="Ask a question or provide a prompt..." style="max-height: 120px;"></textarea>
|
| 117 |
+
<button id="send-button" class="absolute right-2.5 bottom-2.5 p-2 bg-gradient-to-r from-indigo-500 to-purple-600 text-white rounded-lg hover:opacity-90 transition disabled:opacity-50 disabled:cursor-not-allowed">
|
| 118 |
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m22 2-7 20-4-9-9-4z"/><path d="M22 2 11 13"/></svg>
|
| 119 |
+
</button>
|
| 120 |
+
</div>
|
| 121 |
+
<div class="flex items-center justify-between mt-3 text-xs text-slate-400 h-5">
|
| 122 |
+
<div id="status-indicator" class="hidden">
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
</div>
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
</div>
|
| 129 |
+
</div>
|
| 130 |
+
|
| 131 |
+
<script src="/static/app.js"></script>
|
| 132 |
+
</body>
|
| 133 |
+
</html>
|