Ab-Romia commited on
Commit
15451b9
·
verified ·
1 Parent(s): e0e3f83

Upload 10 files

Browse files
Files changed (10) hide show
  1. Dockerfile +25 -0
  2. app/config.py +27 -0
  3. app/main.py +102 -0
  4. app/rag_setup.py +71 -0
  5. app/schemas.py +56 -0
  6. app/services.py +195 -0
  7. main.py +17 -0
  8. requirements.txt +10 -0
  9. static/app.js +315 -0
  10. 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>