Skip to content

Latest commit

 

History

History
888 lines (678 loc) · 33.2 KB

File metadata and controls

888 lines (678 loc) · 33.2 KB

AI Toolkit — Developer Documentation

Complete technical reference for architecture, implementation details, data flows, and extension guide.


Table of Contents

  1. Project Overview
  2. Tech Stack
  3. Project Structure
  4. Architecture Overview
  5. Backend — Setup & Configuration
  6. Backend — Module Reference
  7. API Reference
  8. Frontend — Setup
  9. Frontend — Component Reference
  10. Feature Implementation Deep Dives
  11. State Management
  12. LLM Strategy
  13. Adding New Features
  14. Known Limitations
  15. Environment Variables
  16. Dependencies

Project Overview

AI Toolkit is a full-stack AI application with two tools:

  1. PDF Chat — upload PDFs, perform RAG-based Q&A, generate summaries, quizzes, flashcards, and mind maps
  2. Data Analytics — upload CSV/Excel files, visualise data with interactive charts, detect anomalies, and query data with natural language

The project is designed for local use. The LLM inference backend uses Ollama (local models) for most features, with Groq API (free, remote) for the mind map feature that requires higher RAM than typical local machines support.


Tech Stack

Layer Technology Version Purpose
Frontend framework React 18 UI rendering
Language TypeScript 5 Type safety
Styling Tailwind CSS 3.x Utility-first CSS
Routing React Router 6 Client-side navigation
Charts Recharts 2.x Data visualisation
Graph/nodes @xyflow/react Latest Interactive mind map
Markdown react-markdown Latest Render LLM markdown output
Export html2canvas Latest Chart PNG export
Build tool Vite 5 Dev server + bundler
Backend framework FastAPI Latest REST API
Language Python 3.10 Backend logic
Local LLM Ollama + llama3.2 Latest Chat, summary, quiz, flashcards
Remote LLM Groq API (llama-3.3-70b) Latest Mind map generation
Embeddings sentence-transformers Latest all-MiniLM-L6-v2 model
Vector DB ChromaDB Latest In-memory vector storage
PDF parsing PyMuPDF (fitz) Latest Text extraction
Data parsing pandas + openpyxl Latest CSV/Excel processing

Project Structure

RAG-local/
├── backend/
│   ├── venv/                  # Python virtual environment
│   ├── .env                   # API keys (GROQ_API_KEY)
│   ├── main.py                # FastAPI app + all route definitions
│   ├── pdf_parser.py          # PDF text extraction + chunking
│   ├── rag.py                 # ChromaDB vector store operations
│   ├── llm.py                 # LLM abstraction (Ollama + Groq)
│   ├── actions.py             # PDF AI feature logic
│   ├── data_analyzer.py       # CSV/Excel analysis logic
│   └── requirements.txt       # Python dependencies
│
└── frontend/
    ├── src/
    │   ├── main.tsx            # App entry point + React Router setup
    │   ├── HomePage.tsx        # Landing page with tool cards
    │   ├── App.tsx             # PDF Chat full page component
    │   ├── DataPage.tsx        # Data Analytics full page component
    │   ├── Flashcards.tsx      # Flip card modal component
    │   ├── MindMap.tsx         # Interactive node graph modal
    │   ├── VoiceInput.tsx      # Mic button using Web Speech API
    │   ├── MarkdownMessage.tsx # Markdown renderer for chat messages
    │   ├── api.ts              # All fetch calls to backend
    │   └── index.css           # Tailwind CSS directives
    ├── tailwind.config.js      # Tailwind content paths
    ├── vite.config.ts          # Vite build config
    └── package.json            # Node dependencies

Architecture Overview

Browser (React + TypeScript)
        │
        │  HTTP / SSE
        ▼
FastAPI (Python) — port 8000
        │
        ├── PDF routes ──► pdf_parser.py ──► PyMuPDF
        │                ──► rag.py       ──► ChromaDB (in-memory)
        │                ──► actions.py   ──► llm.py
        │                                         ├── Ollama (local, port 11434)
        │                                         └── Groq API (remote, mind map only)
        │
        └── Data routes ──► data_analyzer.py ──► pandas
                         ──► llm.py (ask_gemini → Groq) ──► Groq API

Key design decisions:

  1. Stateless sessions with UUIDs — each uploaded file (PDF or CSV) gets a UUID as a session key. The backend stores data in memory (_data_store dict for DataFrames, ChromaDB for vectors). Sessions are lost on restart.

  2. RAG only for chat — summary, quiz, key points use the full document context (all chunks joined). Chat uses RAG (top-3 relevant chunks) to avoid context overflow on long documents.

  3. Streaming for chat, blocking for actions — chat responses stream via SSE for instant feedback. Actions like summary/quiz block until complete because the full response is needed for markdown rendering.

  4. Groq for mind map only — Ollama runs locally and is RAM-constrained. The mind map prompt (JSON extraction with structured output) needs a stronger model. Groq's llama-3.3-70b is used remotely for this specific task.

  5. localStorage for history — chat sessions and file history are persisted in browser localStorage. This avoids needing a database for a local application.


Backend — Setup & Configuration

Install dependencies:

cd backend
python -m venv venv
venv\Scripts\activate        # Windows
source venv/bin/activate     # Mac/Linux
pip install -r requirements.txt

Start the server:

uvicorn main:app --reload

The --reload flag watches for file changes and auto-restarts. Do not use in production.

CORS is configured in main.py to allow only http://localhost:5173 (Vite dev server). For deployment, update allow_origins to your production domain.


Backend — Module Reference

pdf_parser.py

Purpose: Extract text from PDFs and split into overlapping chunks for RAG.

Functions:

extract_text_from_pdf(file_bytes: bytes) -> str

Opens a PDF from raw bytes using fitz.open(stream=...), iterates over all pages calling page.get_text(), and concatenates the result. Returns a single string of all text.

chunk_text(text: str, chunk_size: int = 500, overlap: int = 50) -> list[str]

Splits text by whitespace into words, then creates sliding windows of chunk_size words with overlap words of overlap between consecutive chunks. The overlap ensures context is preserved at chunk boundaries. Chunks shorter than 30 characters are filtered out (headers, page numbers, etc.).

Why overlapping chunks? Without overlap, a sentence that spans a chunk boundary would be split, making retrieval miss relevant context. Overlap ensures every sentence appears fully in at least one chunk.


rag.py

Purpose: Manage the vector store — embed, store, and retrieve document chunks.

Configuration:

EMBED_MODEL = "all-MiniLM-L6-v2"  # 80MB model, downloads once on first run

This model produces 384-dimensional embeddings and is a strong balance of speed and quality for semantic search.

ChromaDB client: Uses chromadb.Client() (in-memory). Data lives only while the server is running. For persistence across restarts, change to:

client = chromadb.PersistentClient(path="./chroma_db")

Functions:

store_chunks(session_id: str, chunks: list[str]) -> int

Deletes any existing collection for this session, creates a new one, embeds all chunks using the sentence-transformer model, and adds them to ChromaDB with IDs like chunk_0, chunk_1, etc. Returns the number of chunks stored.

retrieve_chunks(session_id: str, query: str, n_results: int = 5) -> list[str]

Embeds the query using the same model, performs cosine similarity search in ChromaDB, and returns the top-N most similar chunks.

get_all_chunks(session_id: str) -> list[str]

Returns all chunks in the collection (used for summary/quiz/flashcards which need full document context).

Embedding process: ChromaDB's SentenceTransformerEmbeddingFunction wraps the model and handles batching automatically. On first call it downloads the model weights (~80MB) and caches them locally.


llm.py

Purpose: Abstraction layer for all LLM calls. Keeps model-switching isolated to one file.

Functions:

ask_llm(prompt: str) -> str

Sends a prompt to Ollama using the ollama.chat() synchronous API. Returns the full response as a string. Used for: summary, quiz, questions, key points, flashcards.

ask_llm_stream(prompt: str) -> Generator[str, None, None]

Same as above but with stream=True. Yields one token chunk at a time as they are generated. Used exclusively for chat streaming. Each chunk from Ollama contains chunk["message"]["content"] — the token text.

ask_gemini(prompt: str) -> str

Despite the name (legacy from when Gemini was intended), this actually calls Groq API. Loads GROQ_API_KEY from .env, initialises a Groq client, and calls chat.completions.create() with model="llama-3.3-70b-versatile". Returns the response text. Used only for mind map generation.

Why separate functions instead of parameters? The streaming generator pattern is fundamentally different from blocking calls — they cannot share the same function signature cleanly. Separating them makes the call sites explicit about whether they expect streaming or blocking.


actions.py

Purpose: All PDF AI feature logic. Each function takes a session_id, retrieves document chunks, constructs a prompt, calls the LLM, and returns the result.

Helper:

_join_chunks(chunks: list[str], max_words: int = 3000) -> str

Joins chunks into a single string but caps at max_words to avoid overflowing the LLM context window (llama3.2 has a 128k context but performance degrades on very long inputs).

Feature functions:

Function Chunks used Prompt strategy
generate_summary All chunks "Write a 3-5 paragraph summary covering main topics"
generate_questions All chunks "Generate 10 insightful numbered questions"
generate_quiz All chunks "Create 5 MCQ questions with A/B/C/D options and correct answer"
generate_key_points All chunks "Extract 8-10 key points as a numbered list"
generate_flashcards All chunks "Create 8 flashcards as JSON array with question/answer keys"
generate_mindmap All chunks (1500 words) "Extract mind map as JSON with center + topics + subtopics"
chat_with_pdf Top-3 relevant chunks "Answer using ONLY the context provided"
stream_chat_with_pdf Top-3 relevant chunks Same prompt but yields SSE events

Flashcard JSON parsing:

raw = ask_llm(prompt)
if raw.startswith("```"):
    raw = raw.split("```")[1]
    if raw.startswith("json"):
        raw = raw[4:]
json.loads(raw)

LLMs often wrap JSON in markdown code fences. This strips them before parsing.

Streaming SSE format:

yield f"data: {json.dumps({'type': 'sources', 'sources': sources})}\n\n"
yield f"data: {json.dumps({'type': 'token', 'token': token})}\n\n"
yield f"data: {json.dumps({'type': 'done'})}\n\n"

Three event types: sources sent first (so the UI can show them immediately), token for each word fragment, done to signal completion.


data_analyzer.py

Purpose: All CSV/Excel analysis logic using pandas.

Functions:

parse_file(file_bytes: bytes, filename: str) -> pd.DataFrame

Detects file type by extension and uses pd.read_csv() or pd.read_excel(). Returns a DataFrame.

get_table_preview(df, rows=10) -> dict

Returns first N rows as a list of lists (serialisable), plus column metadata. Column type detection: if pandas dtype contains "int" or "float" → numeric, "datetime" → datetime, otherwise → text.

get_column_stats(df) -> list[dict]

For numeric columns: min, max, mean, median, null count. For text columns: unique count, most common value, null count.

_aggregate(df, x_col, y_cols, agg) -> pd.DataFrame

Internal helper. Groups by x_col and aggregates y_cols using sum/mean/count. Detects if x_col is categorical (non-numeric) to decide whether to group or just select.

get_chart_data(df, x_col, y_col, chart_type, agg) -> dict

Single Y column. Calls _aggregate, returns labels + values lists.

get_multi_chart_data(df, x_col, y_cols, chart_type, agg) -> dict

Multiple Y columns. Returns series dict mapping each Y column name to its values list. The frontend reconstructs per-row objects from this.

get_top_n(df, x_col, y_col, n, order, agg) -> dict

Aggregates, sorts ascending (bottom) or descending (top), takes first N rows.

detect_anomalies(df, col) -> dict

IQR method: lower = Q1 - 1.5*IQR, upper = Q3 + 1.5*IQR. Returns bounds, stats, count of anomalous rows, and the anomalous rows themselves as a list of lists for table rendering.

dataframe_to_context(df, max_rows=100) -> str

Converts DataFrame to CSV string for LLM context. Caps at max_rows to avoid context overflow.


main.py

Purpose: FastAPI application definition, middleware, routes, and in-memory state.

CORS middleware:

allow_origins=["http://localhost:5173"]

Only the Vite dev server is allowed. Update for deployment.

In-memory DataFrame store:

_data_store: dict[str, pd.DataFrame] = {}

Maps session_id (UUID string) to a pandas DataFrame. Lives in process memory — cleared on restart. For production, use Redis or a database.

Request models (Pydantic):

  • ActionRequest{session_id: str}
  • ChatRequest{session_id: str, question: str}
  • DataChatRequest{session_id: str, question: str}
  • ChartRequest{session_id, x_col, y_col, chart_type, agg}
  • MultiChartRequest{session_id, x_col, y_cols: list[str], chart_type, agg}
  • TopNRequest{session_id, x_col, y_col, n, order, agg}
  • AnomalyRequest{session_id, col}

Streaming endpoint:

@app.post("/chat/stream")
async def chat_stream(req: ChatRequest):
    return StreamingResponse(
        stream_chat_with_pdf(req.session_id, req.question),
        media_type="text/event-stream",
        headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
    )

X-Accel-Buffering: no disables nginx response buffering when deployed behind a reverse proxy.


API Reference

PDF Chat Endpoints

Method Path Request Body Response
POST /upload multipart/form-data (file) {session_id, filename, chunks_stored, page_count, file_size}
POST /summary {session_id} {result: string}
POST /questions {session_id} {result: string}
POST /quiz {session_id} {result: string}
POST /keypoints {session_id} {result: string}
POST /flashcards {session_id} {cards: [{question, answer}]}
POST /mindmap {session_id} {center: string, topics: [{label, subtopics}]}
POST /chat {session_id, question} {result: string, sources: string[]}
POST /chat/stream {session_id, question} SSE stream of {type, token/sources/done}
GET / {status: string}

Data Analytics Endpoints

Method Path Request Body Response
POST /data/upload multipart/form-data (file) {session_id, filename, preview, stats}
POST /data/chart {session_id, x_col, y_col, chart_type, agg} {labels, values, x_col, y_col}
POST /data/multichart {session_id, x_col, y_cols[], chart_type, agg} {labels, series: {col: values[]}, x_col, y_cols}
POST /data/topn {session_id, x_col, y_col, n, order, agg} {labels, values, x_col, y_col, order, n}
POST /data/anomalies {session_id, col} {column, lower_bound, upper_bound, mean, std, total_rows, anomaly_count, anomaly_rows, anomaly_cols}
POST /data/summary {session_id} {summary: string}
POST /data/chat {session_id, question} {result: string}

Frontend — Setup

cd frontend
npm install
npm run dev          # Dev server on http://localhost:5173
npm run build        # Production build to dist/

Environment: The backend URL is hardcoded in api.ts as http://localhost:8000. For deployment, change this to an environment variable using Vite's import.meta.env.VITE_API_URL.


Frontend — Component Reference

main.tsx

Entry point. Wraps the app in <BrowserRouter> and defines three routes:

  • /HomePage
  • /pdfApp (PDF Chat)
  • /dataDataPage

HomePage.tsx

Static landing page. Reads theme from localStorage and applies it. The two tool cards are defined as a tools array — adding a third tool only requires adding an object to this array.

App.tsx (PDF Chat)

The largest component (~600 lines). Manages all PDF Chat state.

Key state:

sessions: Session[]        // All chat sessions (persisted to localStorage)
sessionId: string | null   // Active session UUID
messages: Message[]        // Current chat messages
typingIdx: number | null   // Index of message currently being typed
dark: boolean              // Theme
flashcards: Card[] | null  // Shown when non-null
mindmap: MapData | null    // Shown when non-null

Message type:

type Message = {
  role: "user" | "assistant" | "system";
  content: string;
  typing?: boolean;     // true while streaming
  sources?: string[];   // source chunks from RAG
}

Typing effect: Implemented in the TypingMessage component using setInterval at 8ms per character. This simulates streaming for non-streaming responses (summary, quiz, etc.) while real SSE streaming is used for chat.

Real streaming (chat): sendQuestion() calls streamChat() from api.ts, which opens a ReadableStream from the SSE response. Tokens are appended to the last message in state via setMessages(prev => ...) with functional updates to avoid stale closures.

DataPage.tsx

Large component (~700 lines). Manages all Data Analytics state.

Key state:

sessions: DataSession[]          // File sessions (persisted)
activeSession: DataSession | null // Currently viewed session
chartMode: "single"|"multi"|"topn"
chartData: {name, value}[]       // Single/TopN chart data
multiChartData: {name, ...}[]    // Multi-column chart data
anomalyResult: AnomalyResult | null
aiMessages: DataMessage[]        // Data chat history

Chart rendering: renderChart() inspects chartMode and chartType to pick the right Recharts component. Multi-column charts map yCols to <Bar> or <Line> elements dynamically.

Export PNG flow:

const html2canvas = (await import("html2canvas")).default;  // lazy import
const canvas = await html2canvas(chartRef.current, {...});
canvas.toDataURL("image/png")  download link

The chartRef is attached to the chart panel <div>.

Flashcards.tsx

Modal component. Manages flip animation using CSS transform: rotateY(180deg) with preserve-3d. The front face has backface-visibility: hidden and the back face has the same but pre-rotated 180°.

State: current (card index), flipped (boolean), finished (boolean).

Flip transition: When navigating between cards, flipped is set to false with a 200ms delay before advancing the index, so the card visually unflips before showing the next one.

MindMap.tsx

Wraps @xyflow/react. The buildGraph() function converts the JSON from the backend into ReactFlow Node and Edge arrays:

// Center node at (0, 0)
// Topics arranged in a circle using trigonometry:
const angle = (2 * Math.PI * ti) / topicCount - Math.PI / 2;
const tx = Math.cos(angle) * topicRadius;  // topicRadius = 280
const ty = Math.sin(angle) * topicRadius;

// Subtopics fanned out from their parent topic:
const spreadAngle = 0.55;
const subAngle = startAngle + si * spreadAngle;
const sx = tx + Math.cos(subAngle) * subRadius;  // subRadius = 180

The useEffect on data calls setNodes and setEdges to re-render when the backend returns a new map.

VoiceInput.tsx

Uses the browser's Web Speech API (window.SpeechRecognition || window.webkitSpeechRecognition). The recognition object is created once in useEffect and stored in a ref to persist across renders.

recognition.continuous     = false  // stop after first utterance
recognition.interimResults = false  // only return final transcripts
recognition.lang           = "en-US"

onresult calls onResult(transcript) which triggers auto-send in the parent. onerror handles not-allowed (mic denied), no-speech, and generic errors with user-visible messages.

MarkdownMessage.tsx

Thin wrapper around react-markdown. Provides custom renderers for: p, strong, em, h1-h3, ul, ol, li, code (inline vs block), blockquote, hr, a. Code blocks detect whether they are inline or fenced using the className prop (fenced blocks get a language-* class).

api.ts

All fetch calls to the backend in one file. The base URL is:

const BASE = "http://localhost:8000";

Streaming function:

async function streamChat(session_id, question, onToken, onSources, onDone)

Uses response.body.getReader() to read the SSE stream. Maintains a buffer string for incomplete lines. Each complete data: ... line is JSON-parsed and routed to the appropriate callback.


Feature Implementation Deep Dives

RAG Pipeline

Full flow:

User uploads PDF
    → extract_text_from_pdf() → raw text string
    → chunk_text(text, size=500, overlap=50) → list of chunks
    → store_chunks(session_id, chunks):
        → SentenceTransformer embeds each chunk → 384-dim vector
        → ChromaDB stores [text, vector, id] triples
        → Returns chunk count

User sends question
    → retrieve_chunks(session_id, question, n=3):
        → SentenceTransformer embeds question → 384-dim vector
        → ChromaDB cosine similarity search → top-3 chunks
    → Build prompt: "Context: {chunks}\n\nQuestion: {question}"
    → ask_llm_stream(prompt) → SSE tokens to frontend

Why cosine similarity? It measures the angle between vectors (semantic similarity) rather than distance. Two sentences about the same topic will have a small angle even if they use different words.

Why top-3 chunks? More chunks = more context = better answers, but also more tokens = slower responses and potential context confusion. Top-3 is a good balance for llama3.2's context window.


Streaming Responses (SSE)

Server-Sent Events allow the server to push data to the client over a single HTTP connection.

Backend (FastAPI):

# Generator yields SSE-formatted strings
def stream_chat_with_pdf(session_id, question):
    yield f"data: {json.dumps({'type': 'sources', ...})}\n\n"
    for token in ask_llm_stream(prompt):
        yield f"data: {json.dumps({'type': 'token', 'token': token})}\n\n"
    yield f"data: {json.dumps({'type': 'done'})}\n\n"

# Route wraps generator in StreamingResponse
return StreamingResponse(generator, media_type="text/event-stream")

Frontend (React):

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split("\n");
  buffer = lines.pop() ?? "";  // incomplete line stays in buffer
  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const event = JSON.parse(line.slice(6));
    if (event.type === "token") onToken(event.token);
    // ...
  }
}

The buffer handles the case where a chunk boundary falls in the middle of a JSON string.


Mind Map Generation

The challenge: Structured JSON output from LLMs is unreliable — models add explanatory text, markdown fences, or malformed JSON.

Solution:

  1. Prompt explicitly says "Return ONLY valid JSON, no explanation, no markdown fences"
  2. Post-process strips markdown fences if present
  3. try/except around json.loads() returns a fallback error map on failure

Layout algorithm: Circular arrangement using trigonometry ensures nodes don't overlap regardless of how many topics there are. Subtopics fan out at ±0.55 radians from their parent topic's direction.


Flashcard Generation

Same JSON reliability challenge as mind map. The prompt specifies exact format:

[{"question": "...", "answer": "..."}, ...]

Post-processing strips markdown, parses JSON, validates each object has both question and answer keys. Invalid cards are filtered out.


Anomaly Detection

IQR (Interquartile Range) method:

Q1 = df[col].quantile(0.25)
Q3 = df[col].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
anomalies = df[(df[col] < lower_bound) | (df[col] > upper_bound)]

Why IQR over Z-score? Z-score assumes normal distribution. IQR is non-parametric — it works on any distribution including skewed sales data. IQR also isn't affected by the outliers themselves (since Q1/Q3 are percentiles).


Multi-Column Charts

Backend: get_multi_chart_data returns:

{
  "labels": ["Jan", "Feb", "Mar"],
  "series": {
    "Sales": [100, 150, 120],
    "Profit": [20, 35, 28]
  }
}

Frontend reconstruction:

const formatted = labels.map((label, i) => {
  const row: any = { name: label };
  yCols.forEach(col => { row[col] = series[col]?.[i] ?? 0; });
  return row;
});
// Result: [{name:"Jan", Sales:100, Profit:20}, ...]

Recharts needs this format — each data point as a single object with all series as keys. The yCols.map(col => <Bar dataKey={col} />) then picks each key from the object.


Voice Input

The SpeechRecognition API is created once and stored in useRef to avoid re-creating on every render. Key lifecycle:

User clicks mic → recognition.start() → setListening(true)
Browser captures audio → recognition.onresult fires
→ extract transcript → onResult(transcript) → parent auto-sends
→ recognition.onend fires → setListening(false)

The parent (App.tsx) handleVoiceResult() sets the input value and waits 600ms before sending — giving the user visual feedback that their speech was captured.


Chat History Persistence

Both PDF Chat and Data Analytics use the same pattern:

const STORAGE_KEY = "pdf_chat_sessions";  // or "data_analytics_sessions"

// Load on mount
const [sessions, setSessions] = useState<Session[]>(() => {
  return JSON.parse(localStorage.getItem(STORAGE_KEY) || "[]");
});

// Save on every change
useEffect(() => {
  localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions));
}, [sessions]);

What is saved vs what isn't:

Saved in localStorage Not saved
Filenames PDF text / vectors (in ChromaDB memory)
Dates DataFrame (in Python memory)
Message history Voice input state
Page count / row count Chart data
Rename edits

This means chat history is visual only after a backend restart. Re-uploading the file restores AI functionality.


State Management

The application uses React local state only — no Redux, Zustand, or Context API. Each page component is self-contained.

Why no global state manager? The two tools (PDF Chat, Data Analytics) are completely independent and never share state. Within each tool, the state tree is manageable within a single component. Adding a global state manager would add complexity without benefit.

Prop drilling is minimal — modal components (Flashcards, MindMap) receive only what they need: the data, dark mode flag, and callbacks.


LLM Strategy

Feature LLM Why
Summary, Quiz, Questions, Key Points Ollama llama3.2 Full context needed, local is fine
Flashcards Ollama llama3.2 JSON output, local is fine
Chat (streaming) Ollama llama3.2 Speed matters, local streaming works
Mind Map Groq llama-3.3-70b Needs structured JSON + RAM-intensive
Data Summary Groq (via ask_gemini) Needs reasoning over tabular data
Data Chat Groq (via ask_gemini) Needs reasoning over tabular data

Groq function is named ask_gemini — legacy naming from when Google Gemini was attempted. The function now calls Groq. Renaming was avoided to prevent breaking other files that import it.


Adding New Features

Adding a new PDF action

  1. Add prompt logic in actions.py:
def generate_my_feature(session_id: str) -> str:
    chunks = get_all_chunks(session_id)
    context = _join_chunks(chunks)
    prompt = f"Your prompt here...\n\n{context}"
    return ask_llm(prompt)
  1. Add import and route in main.py:
from actions import ..., generate_my_feature

@app.post("/my-feature")
async def my_feature(req: ActionRequest):
    return {"result": generate_my_feature(req.session_id)}
  1. Add API call in api.ts:
export async function runMyFeature(session_id: string) {
  const res = await fetch(`${BASE}/my-feature`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ session_id }),
  });
  return (await res.json()).result as string;
}
  1. Add button to ACTIONS array in App.tsx:
const ACTIONS = [
  ...,
  { id: "my-feature", label: "🆕 My Feature", desc: "...", loadMsg: "Processing..." },
];

Adding a new data chart type

  1. Add Recharts import in DataPage.tsx
  2. Add an entry to CHART_TYPES array
  3. Add a render case in renderChart()

Switching from Ollama to another local model

In llm.py, change:

OLLAMA_MODEL = "llama3.2"  # change to "mistral", "llama3.1", "gemma2", etc.

The model must be pulled first: ollama pull modelname


Known Limitations

Limitation Description Potential fix
Sessions lost on restart ChromaDB and DataFrames are in-memory Use chromadb.PersistentClient + save DataFrames to disk
No auth Anyone on localhost can use the app Add JWT auth with FastAPI-Users
Single PDF per session Each upload creates a new session Implement multi-document RAG with namespaced collections
Scanned PDFs PyMuPDF can't extract text from image-only PDFs Add OCR with pytesseract or pdf2image
Large files slow Very large CSVs take time to parse Add chunked upload + streaming parse
Mind map RAM Ollama can't handle structured JSON on low RAM Groq solves this but requires internet
No streaming for data chat Data chat uses blocking Groq call Implement SSE for Groq calls too

Environment Variables

File: backend/.env

Variable Required Description
GROQ_API_KEY Yes (for mind map + data AI) Get free at console.groq.com

Dependencies

Backend (requirements.txt):

fastapi          # Web framework
uvicorn          # ASGI server
pymupdf          # PDF text extraction (fitz)
chromadb         # Vector database
sentence-transformers  # all-MiniLM-L6-v2 embeddings
ollama           # Local LLM client
python-multipart # File upload support
pydantic         # Request validation
groq             # Groq API client
python-dotenv    # Load .env file
pandas           # DataFrame operations
openpyxl         # Excel file support

Frontend (package.json key deps):

react              # UI framework
react-dom          # DOM rendering
react-router-dom   # Client-side routing
typescript         # Type safety
tailwindcss        # Utility CSS
recharts           # Data visualisation
@xyflow/react      # Mind map node graph
react-markdown     # Markdown rendering
html2canvas        # Chart PNG export
vite               # Build tool