diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6c37b2a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,19 @@ +__pycache__/ +*.py[cod] +*$py.class +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.vscode/ +.idea/ +.git/ +.gitignore +*.db +faiss_index.bin +faiss_meta.pkl +node_modules/ +frontend/ diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..c2911cb --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,35 @@ +# Use an official Python runtime as a parent image +FROM python:3.10-slim + +# Set environment variables +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 +ENV PORT=8000 + +# Set work directory +WORKDIR /app + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + libpq-dev \ + libgomp1 \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy project files +COPY app/ ./app/ +COPY data/ ./data/ +COPY legal_ai.db . +COPY faiss_index.bin . +COPY faiss_meta.pkl . + +# Expose the port the app runs on +EXPOSE 8000 + +# Command to run the application +CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT}"] diff --git a/README.md b/README.md index dd2e149..81ee7f8 100644 --- a/README.md +++ b/README.md @@ -81,19 +81,13 @@ It does **NOT replace a lawyer**, but acts as a **first step guidance system**, ``` legal-ai/ │ -├── frontend/ -│ ├── src/ -│ │ ├── components/ -│ │ ├── context/ -│ │ ├── pages/ -│ │ └── services/ -│ -├── backend/ -│ ├── app/ -│ ├── routes/ -│ └── chat.py -│ -├── .gitignore +├── app/ # FastAPI backend +├── frontend/ # React + Vite frontend +├── data/ # Data files +├── requirements.txt # Backend dependencies +├── docker-compose.yml # Docker orchestration +├── Dockerfile.backend # Backend Docker configuration +├── .dockerignore # Docker ignore rules └── README.md ``` @@ -162,9 +156,25 @@ npm install npm run dev ``` + --- -## 🌍 Environment Variables +### 🔹 4. Docker Setup (Recommended) + +If you have Docker and Docker Compose installed, you can run the entire stack with a single command: + +```bash +# Build and start the containers +docker-compose up --build +``` + +The application will be available at: +* Frontend: http://localhost:3000 +* Backend: http://localhost:8000 + +**Note:** Make sure to create a `.env` file in the root directory with your `GROQ_API_KEY` before running Docker. + +--- ### Backend diff --git a/app/api/chat.py b/app/api/chat.py index 2b00d3b..2216197 100644 --- a/app/api/chat.py +++ b/app/api/chat.py @@ -1,7 +1,7 @@ from fastapi import APIRouter, Request, Depends from pydantic import BaseModel -from app.services.memory import add_message, get_history +from app.services.memory import add_message, get_history, clear_history from app.services.intent import detect_intent from app.services.llm import query_groq_llm from app.services.safety import validate_response @@ -11,6 +11,14 @@ router = APIRouter() +@router.delete("/chat/{session_id}") +async def delete_chat(session_id: str, user=Depends(get_current_user)): + user_id = user.id + full_session_id = f"{user_id}-{session_id}" + clear_history(full_session_id) + return {"message": "Chat history cleared successfully"} + + class ChatRequest(BaseModel): message: str session_id: str = "default-session" diff --git a/app/main.py b/app/main.py index 6e6718d..0d11704 100644 --- a/app/main.py +++ b/app/main.py @@ -42,6 +42,12 @@ async def lifespan(app: FastAPI): except Exception as e: logger.warning(f"DB init failed: {e}") + # Check for critical environment variables + if not os.getenv("GROQ_API_KEY"): + logger.error("❌ GROQ_API_KEY is missing! Backend will use fallback responses.") + if not os.getenv("HF_TOKEN"): + logger.warning("⚠️ HF_TOKEN is missing! HuggingFace API may fail or be rate-limited.") + try: vector_store.load_index() logger.info("✅ FAISS index loaded") diff --git a/app/services/llm.py b/app/services/llm.py index cd7979c..4d92838 100644 --- a/app/services/llm.py +++ b/app/services/llm.py @@ -48,17 +48,18 @@ def query_groq_llm( api_key = os.getenv("GROQ_API_KEY") if not api_key: - raise ValueError("GROQ_API_KEY not set") + logger.error("GROQ_API_KEY is not set") + return fallback_response("english") # Detect style style = detect_language_style(user_query) - # ✅ NEW: Build context from FAISS + # ✅ Build context from FAISS context = "" if retrieved_chunks: context = "\n\n".join(retrieved_chunks[:5]) - # ✅ UPDATED system prompt (RAG aware) + # ✅ System prompt (RAG aware) system_prompt = f""" You are a legal awareness assistant for Indian users. @@ -99,8 +100,7 @@ def query_groq_llm( Conversation History: {history} -User Query: -{user_query} +User Query: {user_query} """ payload = { @@ -110,7 +110,7 @@ def query_groq_llm( {"role": "user", "content": user_prompt}, ], "temperature": 0.3, - "max_tokens": 500, + "max_tokens": 800, } headers = { @@ -129,79 +129,24 @@ def query_groq_llm( ) if response.status_code != 200: - logger.error(f"Groq API Error: {response.text}") + logger.error(f"Groq API Error: {response.status_code} - {response.text}") return fallback_response(style) data = response.json() if "choices" not in data or not data["choices"]: + logger.error(f"Invalid response structure from Groq: {data}") return fallback_response(style) answer = data["choices"][0]["message"]["content"].strip() - return answer if answer else fallback_response(style) - - except Exception as e: - logger.error(f"Groq request failed: {e}") - return fallback_response(style) - - # User prompt - user_prompt = f""" -User communication style: {style} - -Conversation History: -{history} - -User Query: -{user_query} -""" - - payload = { - "model": MODEL_NAME, - "messages": [ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt}, - ], - "temperature": 0.3, - "max_tokens": 500, - } - - headers = { - "Authorization": f"Bearer {api_key}", - "Content-Type": "application/json", - } - - # ───────────────────────────── - # API CALL - # ───────────────────────────── - try: - logger.info(f"Calling Groq... (Style: {style})") - - response = requests.post( - GROQ_API_URL, - headers=headers, - json=payload, - timeout=30, - ) - - if response.status_code != 200: - logger.error(f"Groq API Error: {response.text}") - return fallback_response(style) - - data = response.json() - - if "choices" not in data or not data["choices"]: - logger.error(f"Invalid response: {data}") - return fallback_response(style) - - answer = data["choices"][0]["message"]["content"].strip() - + if not answer: return fallback_response(style) return answer except Exception as e: - logger.error(f"Groq request failed: {e}") + logger.error(f"Groq request failed: {str(e)}") return fallback_response(style) diff --git a/app/services/memory.py b/app/services/memory.py index fb83c20..4fd4111 100644 --- a/app/services/memory.py +++ b/app/services/memory.py @@ -8,19 +8,39 @@ def add_message(session_id: str, role: str, content: str): + # Save to DB for persistence + save_message(session_id, role, content) + + # Also update in-memory cache conversation_store[session_id].append({ "role": role, "content": content }) - # keep only last N messages + # keep only last N messages in memory if len(conversation_store[session_id]) > MAX_HISTORY: conversation_store[session_id] = conversation_store[session_id][-MAX_HISTORY:] def get_history(session_id: str): + # If not in cache, try to load from DB + if not conversation_store[session_id]: + db_messages = get_messages(session_id) + if db_messages: + conversation_store[session_id] = db_messages[-MAX_HISTORY:] + return conversation_store[session_id] def clear_history(session_id: str): - conversation_store[session_id] = [] \ No newline at end of file + # Clear cache + conversation_store[session_id] = [] + + # Clear DB + from app.services.database import DB_PATH + import sqlite3 + conn = sqlite3.connect(DB_PATH) + cursor = conn.cursor() + cursor.execute("DELETE FROM messages WHERE session_id = ?", (session_id,)) + conn.commit() + conn.close() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..98e31da --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +services: + backend: + build: + context: . + dockerfile: Dockerfile.backend + ports: + - "8000:8000" + volumes: + - .env:/app/.env + - ./legal_ai.db:/app/legal_ai.db + - ./faiss_index.bin:/app/faiss_index.bin + - ./faiss_meta.pkl:/app/faiss_meta.pkl + environment: + - PORT=8000 + restart: always + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "3000:80" + depends_on: + - backend + restart: always diff --git a/frontend/.dockerignore b/frontend/.dockerignore new file mode 100644 index 0000000..1cacdc3 --- /dev/null +++ b/frontend/.dockerignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +.env +.npmrc +*.log +.vscode/ +.idea/ +.git/ +.gitignore diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..5497395 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,26 @@ +# Stage 1: Build the React application +FROM node:18-alpine AS build + +WORKDIR /app + +# Copy package files and install dependencies +COPY package*.json ./ +RUN npm install + +# Copy the rest of the application code +COPY . . + +# Build the application +RUN npm run build + +# Stage 2: Serve the application using Nginx +FROM nginx:stable-alpine + +# Copy the build output from the previous stage +COPY --from=build /app/dist /usr/share/nginx/html + +# Expose port 80 +EXPOSE 80 + +# Start Nginx +CMD ["nginx", "-g", "daemon off;"] diff --git a/frontend/src/components/chat/ChatWindow.jsx b/frontend/src/components/chat/ChatWindow.jsx index 1de5c52..f80f7ff 100644 --- a/frontend/src/components/chat/ChatWindow.jsx +++ b/frontend/src/components/chat/ChatWindow.jsx @@ -103,7 +103,7 @@ export default function ChatWindow() { try { const token = await getToken() - const data = await sendMessageToBackend(query, token) + const data = await sendMessageToBackend(query, token, chatId) const aiMsg = { id: uuidv4(), diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index e2bc54d..34c9970 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -1,19 +1,24 @@ -const BASE_URL = "http://127.0.0.1:8000"; +const BASE_URL = import.meta.env.VITE_API_URL || "http://127.0.0.1:8000"; -export const sendMessageToBackend = async (message) => { +export const sendMessageToBackend = async (message, token, session_id = "default-session") => { try { + const headers = { + "Content-Type": "application/json", + }; + + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + const res = await fetch(`${BASE_URL}/chat`, { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: headers, body: JSON.stringify({ message: message, - session_id: "default-session", + session_id: session_id, }), }); - // ✅ Handle non-200 responses properly if (!res.ok) { const errorText = await res.text(); console.error("API ERROR:", errorText); @@ -21,8 +26,6 @@ export const sendMessageToBackend = async (message) => { } const data = await res.json(); - - // ✅ Safety check if (!data) { throw new Error("Empty response from server"); } @@ -31,12 +34,33 @@ export const sendMessageToBackend = async (message) => { } catch (error) { console.error("🚨 Chat API error:", error); - - // ✅ Return safe fallback (prevents UI crash) return { answer: "⚠️ Unable to connect to server. Please try again.", confidence: 0, sources: [] }; } +}; + +export const clearChatHistory = async (token, session_id = "default-session") => { + try { + const headers = {}; + if (token) { + headers["Authorization"] = `Bearer ${token}`; + } + + const res = await fetch(`${BASE_URL}/chat/${session_id}`, { + method: "DELETE", + headers: headers, + }); + + if (!res.ok) { + throw new Error(`Failed to clear history: ${res.status}`); + } + + return await res.json(); + } catch (error) { + console.error("🚨 Clear history error:", error); + throw error; + } }; \ No newline at end of file