Skip to content
Open
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
17 changes: 17 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ['eslint:recommended'],
parserOptions: {
ecmaVersion: 2021,
},
globals: {
marked: 'readonly',
},
rules: {
'no-unused-vars': 'warn',
'no-console': 'warn',
},
};
44 changes: 44 additions & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: Claude Code Review

on:
pull_request:
types: [opened, synchronize, ready_for_review, reopened]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"

jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'

runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write

steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1

- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

50 changes: 50 additions & 0 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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: read
pull-requests: read
issues: read
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 }}

# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read

# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'

# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'

11 changes: 11 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"printWidth": 88,
"tabWidth": 4,
"useTabs": false,
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "lf"
}
97 changes: 97 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# CLAUDE.md

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

## Development Commands

```bash
# Run the application
./run.sh
# or manually:
cd backend && uv run uvicorn app:app --reload --port 8000

# Install dependencies
uv sync

# Install dev dependencies (required for linting/formatting)
uv sync --group dev


# Add a new dependency
uv add package_name

# Run all tests
uv run pytest

# Run a single test file
uv run pytest backend/tests/test_rag_system.py

# Run a single test
uv run pytest backend/tests/test_rag_system.py::TestClassName::test_method_name

# Format code (modifies files: isort → black → flake8 → mypy)
./scripts/format.sh

# Lint only, no modifications
./scripts/lint.sh
```

- Web UI: http://localhost:8000
- API docs: http://localhost:8000/docs
- Requires `ANTHROPIC_API_KEY` in a `.env` file at the project root

## Architecture

This is a RAG (Retrieval-Augmented Generation) chatbot for course materials. FastAPI serves both the API and the vanilla JS frontend as static files.

### API endpoints

- `POST /api/query` — main query endpoint, returns `{ answer, sources, source_links, session_id }`
- `GET /api/courses` — returns course catalog stats `{ total_courses, course_titles }`
- `POST /api/clear-session` — clears a session by `{ session_id }`

### Query flow

1. Frontend (`frontend/script.js`) sends `POST /api/query` with `{ query, session_id }`
2. `app.py` creates a session if none exists, delegates to `RAGSystem.query()`
3. `RAGSystem` fetches conversation history from `SessionManager`, then calls `AIGenerator.generate_response()`
4. `AIGenerator` runs a **tool-calling loop** (max 2 rounds) with the Claude API:
- Claude may call `search_course_content` (semantic chunk search) or `get_course_outline` (lesson list)
- Tool results are appended to the message list and sent back to Claude
- After max rounds, a final API call without tools forces a text response
5. `ToolManager` collects `last_sources` and `last_source_links` from whichever tool ran last
6. Response, sources, and lesson links are returned to the frontend
7. Frontend renders the answer as Markdown (`marked.js`) with a collapsible sources block

### Key design decisions

- **Course name resolution**: Partial/fuzzy course names are resolved via a semantic search against the `course_catalog` ChromaDB collection before filtering `course_content`. This lets Claude pass "MCP" and still find "Introduction to MCP Servers".
- **Dual ChromaDB collections**: `course_catalog` stores one document per course (title + metadata including `lessons_json`). `course_content` stores all text chunks with `course_title`/`lesson_number` metadata for filtered search.
- **Session storage**: Sessions are in-memory only — they are lost on server restart. `SessionManager` keeps the last 2 exchange pairs (4 messages) per session. Conversation history is injected into the system prompt, not the message list.
- **AI generation config**: `AIGenerator` uses `temperature=0` and `max_tokens=800`. Model is set in `config.py` (`ANTHROPIC_MODEL`). These are not exposed via env vars — change them in code.
- **Deduplication on startup**: `add_course_folder()` checks existing titles in `course_catalog` and skips already-ingested courses.

### Document format

Course files (`.txt`, `.pdf`, `.docx`) in `docs/` must follow this structure for `.txt` — `.pdf`/`.docx` support is parsed but the required header fields are the same:

```
Course Title: <title>
Course Link: <url>
Course Instructor: <name>

Lesson 0: <title>
Lesson Link: <url>
<lesson content>

Lesson 1: <title>
...
```

`DocumentProcessor` splits content into sentence-aware chunks (800 chars, 100 char overlap). The first chunk of each lesson is prefixed with `"Lesson N content: ..."` for retrieval context.

### Adding a new search tool

1. Create a class extending `Tool` (ABC in `search_tools.py`) implementing `get_tool_definition()` and `execute()`
2. Register it: `self.tool_manager.register_tool(your_tool)` in `RAGSystem.__init__()`
3. If it should surface sources in the UI, add `last_sources` and `last_source_links` instance attributes — `ToolManager.get_last_sources()` checks all registered tools for these
28 changes: 28 additions & 0 deletions backend-tool-refactor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Refactor @backend/ai_generator.py to support sequential tool calling where Claude can make up to 2 tool calls in separate API rounds.

Current behavior:
- Claude makes 1 tool call → tools are removed from API params → final response
- If Claude wants another tool call after seeing results, it can't (gets empty response)

Desired behavior:
- Each tool call should be a separate API request where Claude can reason about previous results
- Support for complex queries requiring multiple searches for comparisons, multi-part questions, or when information from different courses/lessons is needed

Example flow:
1. User: "Search for a course that discusses the same topic as lesson 4 of course X"
2. Claude: get course outline for course X → gets title of lesson 4
3. Claude: uses the title to search for a course that discusses the same topic → returns course information
4. Claude: provides complete answer

Requirements:
- Maximum 2 sequential rounds per user query
- Terminate when: (a) 2 rounds completed, (b) Claude's response has no tool_use blocks, or (c) tool call fails
- Preserve conversation context between rounds
- Handle tool execution errors gracefully

Notes:
- Update the system prompt in @backend/ai_generator.py
- Update the test @backend/tests/test_ai_generator.py
- Write tests that verify the external behavior (API calls made, tools executed, results returned) rather than internal state details.

Use two parallel subagents to brainstorm possible plans. Do not implement any code.
Loading