Skip to content
Open

PR #155

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .claude/commands/implement-feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
You will be implementing a new feature in this codebase

$ARGUMENTS

IMPORTANT: Only do this for front-end features.
Once this feature is built, make sure to write the changes you made to file called frontend-changes.md
Do not ask for permissions to modify this file, assume you can always do it.
8 changes: 8 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(python3:*)",
"Bash(uv run:*)"
]
}
}
2 changes: 0 additions & 2 deletions .env.example

This file was deleted.

58 changes: 58 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: Claude Code

on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]

jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"

# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"

# Optional: Configure Claude's behavior with CLI arguments
# claude_args: |
# --model claude-opus-4-1-20250805
# --max-turns 10
# --allowedTools "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
# --system-prompt "Follow our coding standards. Ensure all new code has tests. Use TypeScript for new files."

# Optional: Advanced settings configuration
# settings: |
# {
# "env": {
# "NODE_ENV": "test"
# }
# }
87 changes: 87 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

A full-stack RAG (Retrieval-Augmented Generation) chatbot that enables semantic search and AI-powered Q&A over course documents. Uses ChromaDB for vector storage, sentence-transformers for embeddings, and Anthropic Claude for response generation.

## Setup

Requires Python 3.13+, `uv` package manager, and an Anthropic API key.

```bash
uv sync
cp .env.example .env # then add your ANTHROPIC_API_KEY
```

## Running

```bash
# Quick start
./run.sh

# Manual (from repo root)
cd backend && uv run uvicorn app:app --reload --port 8000
```

Access the app at `http://localhost:8000`, API docs at `http://localhost:8000/docs`.

On startup, `app.py` auto-loads all `.txt` files from `../docs/` into ChromaDB.

## Architecture

**Request flow:**

```
Frontend (frontend/) → POST /api/query → RAGSystem.query()
→ ai_generator (Claude with tools) → search_tools (if needed)
→ vector_store (ChromaDB semantic search) → response to frontend
```

**Backend modules** (`backend/`):

| File | Role |
|------|------|
| `app.py` | FastAPI entry point; mounts frontend as static files; startup doc loading |
| `rag_system.py` | Orchestrator — wires all components together for a query |
| `document_processor.py` | Parses structured `.txt` course files into chunks |
| `vector_store.py` | ChromaDB wrapper; two collections: `course_catalog` and `course_content` |
| `ai_generator.py` | Anthropic Claude wrapper with tool-calling support |
| `search_tools.py` | Tool definitions and execution (`search_course_content`) |
| `session_manager.py` | In-memory conversation history (max 2 exchanges) |
| `models.py` | Pydantic models: `Course`, `Lesson`, `CourseChunk` |
| `config.py` | All configuration via `Config` dataclass (model, chunk size, paths, etc.) |

**Frontend** (`frontend/`): Vanilla HTML/CSS/JS SPA; uses `marked.js` from CDN for markdown rendering; chat UI with collapsible course stats sidebar.

**Course document format** (files in `docs/`):
```
Course Title: [name]
Course Link: [url]
Course Instructor: [name]

Lesson 0: [title]
Lesson Link: [url]
[content...]
```

## Key Configuration (`backend/config.py`)

- `ANTHROPIC_MODEL`: `claude-sonnet-4-20250514`
- `EMBEDDING_MODEL`: `all-MiniLM-L6-v2` (384-dim, via sentence-transformers)
- `CHUNK_SIZE` / `CHUNK_OVERLAP`: 800 / 100 characters
- `MAX_RESULTS`: 5 search results returned
- `MAX_HISTORY`: 2 conversation exchanges retained
- `CHROMA_PATH`: `./chroma_db` (persistent, relative to `backend/`)

## Dependencies

Managed via `uv`. Key packages: `fastapi`, `uvicorn`, `chromadb`, `anthropic`, `sentence-transformers`, `python-dotenv`. Lock file is `uv.lock`.

## Notes

- No test framework or linting tools are configured.
- ChromaDB persists to `backend/chroma_db/` — delete this directory to reset the vector store.
- The backend serves the frontend as static files; no separate frontend build step.
- Windows users must use Git Bash (not PowerShell/CMD) to run `run.sh`.
99 changes: 52 additions & 47 deletions backend/ai_generator.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import anthropic
from typing import List, Optional, Dict, Any


class AIGenerator:
"""Handles interactions with Anthropic's Claude API for generating responses"""

# Static system prompt to avoid rebuilding on each call
SYSTEM_PROMPT = """ You are an AI assistant specialized in course materials and educational content with access to a comprehensive search tool for course information.

Search Tool Usage:
- Use the search tool **only** for questions about specific course content or detailed educational materials
- **One search per query maximum**
- Synthesize search results into accurate, fact-based responses
- If search yields no results, state this clearly without offering alternatives
- Use `search_course_content` only for questions about specific course content or detailed educational materials
- Use `get_course_outline` for any question asking about a course's structure, outline, syllabus, or list of lessons
- One tool call per query maximum
- Synthesize results into accurate, fact-based responses
- If a tool yields no results, state this clearly without offering alternatives

Response Protocol:
- **General knowledge questions**: Answer using existing knowledge without searching
- **Course-specific questions**: Search first, then answer
- **Course-specific content questions**: Use `search_course_content`, then answer
- **Course outline / structure questions**: Use `get_course_outline` and return the course title, course link, and the number and title of each lesson
- **No meta-commentary**:
- Provide direct answers only — no reasoning process, search explanations, or question-type analysis
- Do not mention "based on the search results"
Expand All @@ -28,108 +31,110 @@ class AIGenerator:
4. **Example-supported** - Include relevant examples when they aid understanding
Provide only the direct answer to what was asked.
"""

def __init__(self, api_key: str, model: str):
self.client = anthropic.Anthropic(api_key=api_key)
self.model = model

# Pre-build base API parameters
self.base_params = {
"model": self.model,
"temperature": 0,
"max_tokens": 800
}

def generate_response(self, query: str,
conversation_history: Optional[str] = None,
tools: Optional[List] = None,
tool_manager=None) -> str:
self.base_params = {"model": self.model, "temperature": 0, "max_tokens": 800}

def generate_response(
self,
query: str,
conversation_history: Optional[str] = None,
tools: Optional[List] = None,
tool_manager=None,
) -> str:
"""
Generate AI response with optional tool usage and conversation context.

Args:
query: The user's question or request
conversation_history: Previous messages for context
tools: Available tools the AI can use
tool_manager: Manager to execute tools

Returns:
Generated response as string
"""

# Build system content efficiently - avoid string ops when possible
system_content = (
f"{self.SYSTEM_PROMPT}\n\nPrevious conversation:\n{conversation_history}"
if conversation_history
if conversation_history
else self.SYSTEM_PROMPT
)

# Prepare API call parameters efficiently
api_params = {
**self.base_params,
"messages": [{"role": "user", "content": query}],
"system": system_content
"system": system_content,
}

# Add tools if available
if tools:
api_params["tools"] = tools
api_params["tool_choice"] = {"type": "auto"}

# Get response from Claude
response = self.client.messages.create(**api_params)

# Handle tool execution if needed
if response.stop_reason == "tool_use" and tool_manager:
return self._handle_tool_execution(response, api_params, tool_manager)

# Return direct response
return response.content[0].text

def _handle_tool_execution(self, initial_response, base_params: Dict[str, Any], tool_manager):

def _handle_tool_execution(
self, initial_response, base_params: Dict[str, Any], tool_manager
):
"""
Handle execution of tool calls and get follow-up response.

Args:
initial_response: The response containing tool use requests
base_params: Base API parameters
tool_manager: Manager to execute tools

Returns:
Final response text after tool execution
"""
# Start with existing messages
messages = base_params["messages"].copy()

# Add AI's tool use response
messages.append({"role": "assistant", "content": initial_response.content})

# Execute all tool calls and collect results
tool_results = []
for content_block in initial_response.content:
if content_block.type == "tool_use":
tool_result = tool_manager.execute_tool(
content_block.name,
**content_block.input
content_block.name, **content_block.input
)

tool_results.append(
{
"type": "tool_result",
"tool_use_id": content_block.id,
"content": tool_result,
}
)

tool_results.append({
"type": "tool_result",
"tool_use_id": content_block.id,
"content": tool_result
})


# Add tool results as single message
if tool_results:
messages.append({"role": "user", "content": tool_results})

# Prepare final API call without tools
final_params = {
**self.base_params,
"messages": messages,
"system": base_params["system"]
"system": base_params["system"],
}

# Get final response
final_response = self.client.messages.create(**final_params)
return final_response.content[0].text
return final_response.content[0].text
Loading