diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..4b41e7b7e
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,90 @@
+# AGENTS.md
+
+## Project overview
+
+This repository contains a course-materials RAG chatbot:
+
+- FastAPI serves the API and static frontend.
+- OpenAI's Responses API generates answers and calls the local course-search tool.
+- ChromaDB stores course metadata and embedded transcript chunks.
+- Sentence Transformers generates local embeddings.
+- The frontend is plain HTML, CSS, and JavaScript.
+
+## Important paths
+
+- `backend/app.py`: FastAPI application and HTTP endpoints.
+- `backend/rag_system.py`: orchestration for ingestion, retrieval, generation, and sessions.
+- `backend/ai_generator.py`: OpenAI Responses API integration and tool-call handling.
+- `backend/document_processor.py`: transcript parsing and chunking.
+- `backend/vector_store.py`: ChromaDB collections and semantic search.
+- `backend/search_tools.py`: model-facing course-search tool.
+- `frontend/`: browser interface.
+- `docs/`: source course transcripts.
+
+## Setup and run
+
+Use `uv` for Python dependencies. The application requires Python 3.13 or newer.
+
+```bash
+uv sync
+./run.sh
+```
+
+The application runs at `http://localhost:8000`; FastAPI documentation is at
+`http://localhost:8000/docs`.
+
+The required environment variable is:
+
+```dotenv
+OPENAI_API_KEY=your_key
+```
+
+Never print, log, hard-code, or commit API keys. Do not read or modify `.env`
+unless the user explicitly asks for environment configuration.
+
+## Architecture and behavior
+
+- Preserve the public API contracts for `POST /api/query` and `GET /api/courses`.
+- Keep retrieval source labels available to the frontend.
+- Course-specific questions should use semantic retrieval; general questions may
+ be answered without retrieval.
+- The model may perform at most one course search for each user query.
+- Conversation sessions are intentionally in-memory and retain a small history.
+- ChromaDB uses separate `course_catalog` and `course_content` collections.
+- Start the server through `run.sh`; current imports and data paths assume Uvicorn
+ runs from the `backend` directory.
+
+## Development conventions
+
+- Keep changes small and consistent with the current straightforward architecture.
+- Prefer type hints for new Python functions and Pydantic models for API schemas.
+- Keep provider-specific API handling inside `backend/ai_generator.py`.
+- Do not replace the local embedding or ChromaDB stack unless explicitly requested.
+- Do not edit the course transcripts or generated `chroma_db` data unless the task
+ concerns ingestion or the knowledge base.
+- Avoid adding frontend frameworks for small UI changes.
+- Update `README.md` when setup, configuration, endpoints, or runtime behavior changes.
+- Update both `pyproject.toml` and `uv.lock` when dependencies change.
+
+## Verification
+
+There is currently no automated test suite. At minimum, after backend changes run:
+
+```bash
+uv run python -m compileall -q backend
+git diff --check
+```
+
+For changes to retrieval or generation, add an API-free focused test or smoke check
+when practical. Do not make a live OpenAI request unless the user explicitly asks
+for live integration testing and a valid key is configured.
+
+For API or frontend behavior changes, start the app and verify the affected endpoint
+or browser flow. Report any verification that could not be run.
+
+## Security and repository hygiene
+
+- Treat model output and retrieved documents as untrusted input.
+- Do not expose stack traces, secrets, or environment values through API responses.
+- Preserve unrelated user changes in the working tree.
+- Do not commit `.env`, `.venv`, ChromaDB data, caches, or generated artifacts.
diff --git a/README.md b/README.md
index e5420d50a..6ee4a906b 100644
--- a/README.md
+++ b/README.md
@@ -4,14 +4,14 @@ A Retrieval-Augmented Generation (RAG) system designed to answer questions about
## Overview
-This application is a full-stack web application that enables users to query course materials and receive intelligent, context-aware responses. It uses ChromaDB for vector storage, Anthropic's Claude for AI generation, and provides a web interface for interaction.
+This application is a full-stack web application that enables users to query course materials and receive intelligent, context-aware responses. It uses ChromaDB for vector storage, OpenAI for AI generation, and provides a web interface for interaction.
## Prerequisites
- Python 3.13 or higher
- uv (Python package manager)
-- An Anthropic API key (for Claude AI)
+- An OpenAI API key
- **For Windows**: Use Git Bash to run the application commands - [Download Git for Windows](https://git-scm.com/downloads/win)
## Installation
@@ -30,7 +30,7 @@ This application is a full-stack web application that enables users to query cou
Create a `.env` file in the root directory:
```bash
- ANTHROPIC_API_KEY=your_anthropic_api_key_here
+ OPENAI_API_KEY=your_openai_api_key_here
```
## Running the Application
diff --git a/backend/ai_generator.py b/backend/ai_generator.py
index 0363ca90c..64102dc83 100644
--- a/backend/ai_generator.py
+++ b/backend/ai_generator.py
@@ -1,135 +1,94 @@
-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
-
-Response Protocol:
-- **General knowledge questions**: Answer using existing knowledge without searching
-- **Course-specific questions**: Search first, then answer
-- **No meta-commentary**:
- - Provide direct answers only — no reasoning process, search explanations, or question-type analysis
- - Do not mention "based on the search results"
-
-
-All responses must be:
-1. **Brief, Concise and focused** - Get to the point quickly
-2. **Educational** - Maintain instructional value
-3. **Clear** - Use accessible language
-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:
- """
- 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
- else self.SYSTEM_PROMPT
- )
-
- # Prepare API call parameters efficiently
- api_params = {
- **self.base_params,
- "messages": [{"role": "user", "content": query}],
- "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):
- """
- 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
- )
-
- 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"]
- }
-
- # Get final response
- final_response = self.client.messages.create(**final_params)
- return final_response.content[0].text
\ No newline at end of file
+import json
+from typing import Any, Dict, List, Optional
+
+from openai import OpenAI
+
+
+class AIGenerator:
+ """Generate answers with OpenAI and execute local course-search tools."""
+
+ 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.
+- Use at most one search per query.
+- Synthesize search results into accurate, fact-based responses.
+- If search 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.
+- Provide direct answers only. Do not describe your reasoning or search process.
+- Do not say "based on the search results."
+
+Keep responses brief, educational, clear, and focused. Include an example when it materially improves understanding.
+"""
+
+ def __init__(self, api_key: str, model: str):
+ self.client = OpenAI(api_key=api_key)
+ self.model = model
+
+ def generate_response(
+ self,
+ query: str,
+ conversation_history: Optional[str] = None,
+ tools: Optional[List[Dict[str, Any]]] = None,
+ tool_manager=None,
+ ) -> str:
+ """Generate a response, executing at most one round of tool calls."""
+ instructions = self.SYSTEM_PROMPT
+ if conversation_history:
+ instructions += f"\n\nPrevious conversation:\n{conversation_history}"
+
+ openai_tools = self._convert_tools(tools or [])
+ input_items: List[Any] = [{"role": "user", "content": query}]
+ request: Dict[str, Any] = {
+ "model": self.model,
+ "instructions": instructions,
+ "input": input_items,
+ "reasoning": {"effort": "low"},
+ "max_output_tokens": 800,
+ }
+ if openai_tools:
+ request["tools"] = openai_tools
+ request["tool_choice"] = "auto"
+
+ response = self.client.responses.create(**request)
+ function_calls = [item for item in response.output if item.type == "function_call"]
+ if not function_calls or not tool_manager:
+ return response.output_text
+
+ input_items.extend(response.output)
+ for function_call in function_calls:
+ arguments = json.loads(function_call.arguments)
+ result = tool_manager.execute_tool(function_call.name, **arguments)
+ input_items.append(
+ {
+ "type": "function_call_output",
+ "call_id": function_call.call_id,
+ "output": result,
+ }
+ )
+
+ final_response = self.client.responses.create(
+ model=self.model,
+ instructions=instructions,
+ input=input_items,
+ tools=openai_tools,
+ reasoning={"effort": "low"},
+ max_output_tokens=800,
+ )
+ return final_response.output_text
+
+ @staticmethod
+ def _convert_tools(tools: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
+ """Convert the existing tool definitions to OpenAI function tools."""
+ return [
+ {
+ "type": "function",
+ "name": tool["name"],
+ "description": tool.get("description", ""),
+ "parameters": tool["input_schema"],
+ }
+ for tool in tools
+ ]
diff --git a/backend/app.py b/backend/app.py
index 5a69d741d..6bf38cf47 100644
--- a/backend/app.py
+++ b/backend/app.py
@@ -40,10 +40,15 @@ class QueryRequest(BaseModel):
query: str
session_id: Optional[str] = None
+class SourceCitation(BaseModel):
+ """A display label and optional destination for a retrieved source."""
+ text: str
+ link: Optional[str] = None
+
class QueryResponse(BaseModel):
"""Response model for course queries"""
answer: str
- sources: List[str]
+ sources: List[SourceCitation]
session_id: str
class CourseStats(BaseModel):
diff --git a/backend/config.py b/backend/config.py
index d9f6392ef..836271997 100644
--- a/backend/config.py
+++ b/backend/config.py
@@ -8,9 +8,9 @@
@dataclass
class Config:
"""Configuration settings for the RAG system"""
- # Anthropic API settings
- ANTHROPIC_API_KEY: str = os.getenv("ANTHROPIC_API_KEY", "")
- ANTHROPIC_MODEL: str = "claude-sonnet-4-20250514"
+ # OpenAI API settings
+ OPENAI_API_KEY: str = os.getenv("OPENAI_API_KEY", "")
+ OPENAI_MODEL: str = "gpt-5.6"
# Embedding model settings
EMBEDDING_MODEL: str = "all-MiniLM-L6-v2"
@@ -26,4 +26,3 @@ class Config:
config = Config()
-
diff --git a/backend/rag_system.py b/backend/rag_system.py
index 50d848c8e..5fba93533 100644
--- a/backend/rag_system.py
+++ b/backend/rag_system.py
@@ -4,7 +4,7 @@
from vector_store import VectorStore
from ai_generator import AIGenerator
from session_manager import SessionManager
-from search_tools import ToolManager, CourseSearchTool
+from search_tools import ToolManager, CourseSearchTool, SourceCitation
from models import Course, Lesson, CourseChunk
class RAGSystem:
@@ -16,7 +16,7 @@ def __init__(self, config):
# Initialize core components
self.document_processor = DocumentProcessor(config.CHUNK_SIZE, config.CHUNK_OVERLAP)
self.vector_store = VectorStore(config.CHROMA_PATH, config.EMBEDDING_MODEL, config.MAX_RESULTS)
- self.ai_generator = AIGenerator(config.ANTHROPIC_API_KEY, config.ANTHROPIC_MODEL)
+ self.ai_generator = AIGenerator(config.OPENAI_API_KEY, config.OPENAI_MODEL)
self.session_manager = SessionManager(config.MAX_HISTORY)
# Initialize search tools
@@ -99,7 +99,7 @@ def add_course_folder(self, folder_path: str, clear_existing: bool = False) -> T
return total_courses, total_chunks
- def query(self, query: str, session_id: Optional[str] = None) -> Tuple[str, List[str]]:
+ def query(self, query: str, session_id: Optional[str] = None) -> Tuple[str, List[SourceCitation]]:
"""
Process a user query using the RAG system with tool-based search.
@@ -144,4 +144,4 @@ def get_course_analytics(self) -> Dict:
return {
"total_courses": self.vector_store.get_course_count(),
"course_titles": self.vector_store.get_existing_course_titles()
- }
\ No newline at end of file
+ }
diff --git a/backend/search_tools.py b/backend/search_tools.py
index adfe82352..89e61c370 100644
--- a/backend/search_tools.py
+++ b/backend/search_tools.py
@@ -1,8 +1,15 @@
-from typing import Dict, Any, Optional, Protocol
+from typing import Dict, Any, Optional, TypedDict
from abc import ABC, abstractmethod
from vector_store import VectorStore, SearchResults
+class SourceCitation(TypedDict):
+ """Structured source metadata returned to API consumers."""
+
+ text: str
+ link: Optional[str]
+
+
class Tool(ABC):
"""Abstract base class for all tools"""
@@ -22,7 +29,7 @@ class CourseSearchTool(Tool):
def __init__(self, vector_store: VectorStore):
self.store = vector_store
- self.last_sources = [] # Track sources from last search
+ self.last_sources: list[SourceCitation] = [] # Track sources from last search
def get_tool_definition(self) -> Dict[str, Any]:
"""Return Anthropic tool definition for this tool"""
@@ -88,7 +95,8 @@ def execute(self, query: str, course_name: Optional[str] = None, lesson_number:
def _format_results(self, results: SearchResults) -> str:
"""Format search results with course and lesson context"""
formatted = []
- sources = [] # Track sources for the UI
+ sources: list[SourceCitation] = [] # Track sources for the UI
+ seen_sources: set[str] = set()
for doc, meta in zip(results.documents, results.metadata):
course_title = meta.get('course_title', 'unknown')
@@ -100,11 +108,18 @@ def _format_results(self, results: SearchResults) -> str:
header += f" - Lesson {lesson_num}"
header += "]"
- # Track source for the UI
- source = course_title
+ # Track each lesson once for the UI, even when several chunks match.
+ source_text = course_title
if lesson_num is not None:
- source += f" - Lesson {lesson_num}"
- sources.append(source)
+ source_text += f" - Lesson {lesson_num}"
+ if source_text not in seen_sources:
+ lesson_link = (
+ self.store.get_lesson_link(course_title, lesson_num)
+ if lesson_num is not None
+ else None
+ )
+ sources.append({"text": source_text, "link": lesson_link})
+ seen_sources.add(source_text)
formatted.append(f"{header}\n{doc}")
@@ -139,7 +154,7 @@ def execute_tool(self, tool_name: str, **kwargs) -> str:
return self.tools[tool_name].execute(**kwargs)
- def get_last_sources(self) -> list:
+ def get_last_sources(self) -> list[SourceCitation]:
"""Get sources from the last search operation"""
# Check all tools for last_sources attribute
for tool in self.tools.values():
@@ -151,4 +166,4 @@ def reset_sources(self):
"""Reset sources from all tools that track sources"""
for tool in self.tools.values():
if hasattr(tool, 'last_sources'):
- tool.last_sources = []
\ No newline at end of file
+ tool.last_sources = []
diff --git a/frontend/script.js b/frontend/script.js
index 562a8a363..bc59c0aef 100644
--- a/frontend/script.js
+++ b/frontend/script.js
@@ -119,24 +119,67 @@ function addMessage(content, type, sources = null, isWelcome = false) {
// Convert markdown to HTML for assistant messages
const displayContent = type === 'assistant' ? marked.parse(content) : escapeHtml(content);
- let html = `
${displayContent}
`;
-
+ messageDiv.innerHTML = `${displayContent}
`;
+
if (sources && sources.length > 0) {
- html += `
-
-
- ${sources.join(', ')}
-
- `;
+ messageDiv.appendChild(createSourcesSection(sources));
}
-
- messageDiv.innerHTML = html;
+
chatMessages.appendChild(messageDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
return messageId;
}
+function createSourcesSection(sources) {
+ const details = document.createElement('details');
+ details.className = 'sources-collapsible';
+
+ const summary = document.createElement('summary');
+ summary.className = 'sources-header';
+ summary.textContent = 'Sources';
+ details.appendChild(summary);
+
+ const content = document.createElement('div');
+ content.className = 'sources-content';
+
+ sources.forEach((source, index) => {
+ if (index > 0) {
+ content.appendChild(document.createTextNode(', '));
+ }
+
+ const label = typeof source === 'string' ? source : source.text;
+ const safeUrl = typeof source === 'object' ? getSafeHttpUrl(source.link) : null;
+
+ if (safeUrl) {
+ const link = document.createElement('a');
+ link.href = safeUrl;
+ link.target = '_blank';
+ link.rel = 'noopener noreferrer';
+ link.textContent = label;
+ content.appendChild(link);
+ } else {
+ const text = document.createElement('span');
+ text.textContent = label;
+ content.appendChild(text);
+ }
+ });
+
+ details.appendChild(content);
+ return details;
+}
+
+function getSafeHttpUrl(value) {
+ if (!value) return null;
+
+ try {
+ const url = new URL(value);
+ return ['http:', 'https:'].includes(url.protocol) ? url.href : null;
+ } catch {
+ return null;
+ }
+}
+
// Helper function to escape HTML for user messages
function escapeHtml(text) {
const div = document.createElement('div');
@@ -188,4 +231,4 @@ async function loadCourseStats() {
courseTitles.innerHTML = 'Failed to load courses';
}
}
-}
\ No newline at end of file
+}
diff --git a/frontend/style.css b/frontend/style.css
index 825d03675..86d746bcc 100644
--- a/frontend/style.css
+++ b/frontend/style.css
@@ -245,6 +245,22 @@ header h1 {
color: var(--text-secondary);
}
+.sources-content a {
+ color: #60a5fa;
+ text-decoration: underline;
+ text-underline-offset: 2px;
+}
+
+.sources-content a:hover {
+ color: #93c5fd;
+}
+
+.sources-content a:focus-visible {
+ outline: 2px solid var(--primary-color);
+ outline-offset: 2px;
+ border-radius: 2px;
+}
+
/* Markdown formatting styles */
.message-content h1,
.message-content h2,
diff --git a/pyproject.toml b/pyproject.toml
index 3f05e2de0..a84e7a0dc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,10 +6,10 @@ readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"chromadb==1.0.15",
- "anthropic==0.58.2",
"sentence-transformers==5.0.0",
"fastapi==0.116.1",
"uvicorn==0.35.0",
"python-multipart==0.0.20",
"python-dotenv==1.1.1",
+ "openai>=2.53.0",
]
diff --git a/run.sh b/run.sh
index 80e3853d8..b05485af5 100755
--- a/run.sh
+++ b/run.sh
@@ -10,7 +10,7 @@ if [ ! -d "backend" ]; then
fi
echo "Starting Course Materials RAG System..."
-echo "Make sure you have set your ANTHROPIC_API_KEY in .env"
+echo "Make sure you have set your OPENAI_API_KEY in .env"
# Change to backend directory and start the server
-cd backend && uv run uvicorn app:app --reload --port 8000
\ No newline at end of file
+cd backend && uv run uvicorn app:app --reload --port 8000
diff --git a/tests/test_search_tools.py b/tests/test_search_tools.py
new file mode 100644
index 000000000..fdbe4fa15
--- /dev/null
+++ b/tests/test_search_tools.py
@@ -0,0 +1,82 @@
+import sys
+import unittest
+from pathlib import Path
+
+
+BACKEND_DIR = Path(__file__).resolve().parents[1] / "backend"
+sys.path.insert(0, str(BACKEND_DIR))
+
+from search_tools import CourseSearchTool
+from vector_store import SearchResults
+
+
+class FakeVectorStore:
+ def __init__(self, lesson_links=None):
+ self.lesson_links = lesson_links or {}
+ self.lesson_link_calls = []
+
+ def get_lesson_link(self, course_title, lesson_number):
+ self.lesson_link_calls.append((course_title, lesson_number))
+ return self.lesson_links.get((course_title, lesson_number))
+
+
+class CourseSearchToolFormattingTests(unittest.TestCase):
+ def test_returns_structured_deduplicated_sources_without_changing_model_text(self):
+ store = FakeVectorStore({("RAG Course", 2): "https://example.com/lesson-2"})
+ tool = CourseSearchTool(store)
+ results = SearchResults(
+ documents=["First chunk", "Second chunk"],
+ metadata=[
+ {"course_title": "RAG Course", "lesson_number": 2},
+ {"course_title": "RAG Course", "lesson_number": 2},
+ ],
+ distances=[0.1, 0.2],
+ )
+
+ formatted = tool._format_results(results)
+
+ self.assertEqual(
+ formatted,
+ "[RAG Course - Lesson 2]\nFirst chunk\n\n"
+ "[RAG Course - Lesson 2]\nSecond chunk",
+ )
+ self.assertEqual(
+ tool.last_sources,
+ [{"text": "RAG Course - Lesson 2", "link": "https://example.com/lesson-2"}],
+ )
+ self.assertEqual(store.lesson_link_calls, [("RAG Course", 2)])
+
+ def test_missing_lesson_link_is_returned_as_null(self):
+ tool = CourseSearchTool(FakeVectorStore())
+ results = SearchResults(
+ documents=["Chunk"],
+ metadata=[{"course_title": "RAG Course", "lesson_number": 3}],
+ distances=[0.1],
+ )
+
+ tool._format_results(results)
+
+ self.assertEqual(
+ tool.last_sources,
+ [{"text": "RAG Course - Lesson 3", "link": None}],
+ )
+
+ def test_lesson_zero_uses_its_lesson_link(self):
+ store = FakeVectorStore({("RAG Course", 0): "https://example.com/lesson-0"})
+ tool = CourseSearchTool(store)
+ results = SearchResults(
+ documents=["Introduction"],
+ metadata=[{"course_title": "RAG Course", "lesson_number": 0}],
+ distances=[0.1],
+ )
+
+ tool._format_results(results)
+
+ self.assertEqual(
+ tool.last_sources,
+ [{"text": "RAG Course - Lesson 0", "link": "https://example.com/lesson-0"}],
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/uv.lock b/uv.lock
index 9ae65c557..c8fe9de88 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,5 +1,5 @@
version = 1
-revision = 2
+revision = 3
requires-python = ">=3.13"
[[package]]
@@ -11,24 +11,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
-[[package]]
-name = "anthropic"
-version = "0.58.2"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "anyio" },
- { name = "distro" },
- { name = "httpx" },
- { name = "jiter" },
- { name = "pydantic" },
- { name = "sniffio" },
- { name = "typing-extensions" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/95/b9/ab06c586aa5a5e7499017cee5ebab94ee260e75975c45395f32b8592abdd/anthropic-0.58.2.tar.gz", hash = "sha256:86396cc45530a83acea25ae6bca9f86656af81e3d598b4d22a1300e0e4cf8df8", size = 425125, upload-time = "2025-07-18T13:38:55.94Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/d0/f2/68d908ff308c9a65af5749ec31952e01d32f19ea073b0268affc616e6ebc/anthropic-0.58.2-py3-none-any.whl", hash = "sha256:3742181c634c725f337b71096839b6404145e33a8e190c75387c4028b825864d", size = 292896, upload-time = "2025-07-18T13:38:54.782Z" },
-]
-
[[package]]
name = "anyio"
version = "4.9.0"
@@ -484,38 +466,52 @@ wheels = [
[[package]]
name = "jiter"
-version = "0.10.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ee/9d/ae7ddb4b8ab3fb1b51faf4deb36cb48a4fbbd7cb36bad6a5fca4741306f7/jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500", size = 162759, upload-time = "2025-05-18T19:04:59.73Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/2e/b0/279597e7a270e8d22623fea6c5d4eeac328e7d95c236ed51a2b884c54f70/jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644", size = 311617, upload-time = "2025-05-18T19:04:02.078Z" },
- { url = "https://files.pythonhosted.org/packages/91/e3/0916334936f356d605f54cc164af4060e3e7094364add445a3bc79335d46/jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a", size = 318947, upload-time = "2025-05-18T19:04:03.347Z" },
- { url = "https://files.pythonhosted.org/packages/6a/8e/fd94e8c02d0e94539b7d669a7ebbd2776e51f329bb2c84d4385e8063a2ad/jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6", size = 344618, upload-time = "2025-05-18T19:04:04.709Z" },
- { url = "https://files.pythonhosted.org/packages/6f/b0/f9f0a2ec42c6e9c2e61c327824687f1e2415b767e1089c1d9135f43816bd/jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3", size = 368829, upload-time = "2025-05-18T19:04:06.912Z" },
- { url = "https://files.pythonhosted.org/packages/e8/57/5bbcd5331910595ad53b9fd0c610392ac68692176f05ae48d6ce5c852967/jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2", size = 491034, upload-time = "2025-05-18T19:04:08.222Z" },
- { url = "https://files.pythonhosted.org/packages/9b/be/c393df00e6e6e9e623a73551774449f2f23b6ec6a502a3297aeeece2c65a/jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25", size = 388529, upload-time = "2025-05-18T19:04:09.566Z" },
- { url = "https://files.pythonhosted.org/packages/42/3e/df2235c54d365434c7f150b986a6e35f41ebdc2f95acea3036d99613025d/jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041", size = 350671, upload-time = "2025-05-18T19:04:10.98Z" },
- { url = "https://files.pythonhosted.org/packages/c6/77/71b0b24cbcc28f55ab4dbfe029f9a5b73aeadaba677843fc6dc9ed2b1d0a/jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca", size = 390864, upload-time = "2025-05-18T19:04:12.722Z" },
- { url = "https://files.pythonhosted.org/packages/6a/d3/ef774b6969b9b6178e1d1e7a89a3bd37d241f3d3ec5f8deb37bbd203714a/jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4", size = 522989, upload-time = "2025-05-18T19:04:14.261Z" },
- { url = "https://files.pythonhosted.org/packages/0c/41/9becdb1d8dd5d854142f45a9d71949ed7e87a8e312b0bede2de849388cb9/jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e", size = 513495, upload-time = "2025-05-18T19:04:15.603Z" },
- { url = "https://files.pythonhosted.org/packages/9c/36/3468e5a18238bdedae7c4d19461265b5e9b8e288d3f86cd89d00cbb48686/jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d", size = 211289, upload-time = "2025-05-18T19:04:17.541Z" },
- { url = "https://files.pythonhosted.org/packages/7e/07/1c96b623128bcb913706e294adb5f768fb7baf8db5e1338ce7b4ee8c78ef/jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4", size = 205074, upload-time = "2025-05-18T19:04:19.21Z" },
- { url = "https://files.pythonhosted.org/packages/54/46/caa2c1342655f57d8f0f2519774c6d67132205909c65e9aa8255e1d7b4f4/jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca", size = 318225, upload-time = "2025-05-18T19:04:20.583Z" },
- { url = "https://files.pythonhosted.org/packages/43/84/c7d44c75767e18946219ba2d703a5a32ab37b0bc21886a97bc6062e4da42/jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070", size = 350235, upload-time = "2025-05-18T19:04:22.363Z" },
- { url = "https://files.pythonhosted.org/packages/01/16/f5a0135ccd968b480daad0e6ab34b0c7c5ba3bc447e5088152696140dcb3/jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca", size = 207278, upload-time = "2025-05-18T19:04:23.627Z" },
- { url = "https://files.pythonhosted.org/packages/1c/9b/1d646da42c3de6c2188fdaa15bce8ecb22b635904fc68be025e21249ba44/jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522", size = 310866, upload-time = "2025-05-18T19:04:24.891Z" },
- { url = "https://files.pythonhosted.org/packages/ad/0e/26538b158e8a7c7987e94e7aeb2999e2e82b1f9d2e1f6e9874ddf71ebda0/jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8", size = 318772, upload-time = "2025-05-18T19:04:26.161Z" },
- { url = "https://files.pythonhosted.org/packages/7b/fb/d302893151caa1c2636d6574d213e4b34e31fd077af6050a9c5cbb42f6fb/jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216", size = 344534, upload-time = "2025-05-18T19:04:27.495Z" },
- { url = "https://files.pythonhosted.org/packages/01/d8/5780b64a149d74e347c5128d82176eb1e3241b1391ac07935693466d6219/jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4", size = 369087, upload-time = "2025-05-18T19:04:28.896Z" },
- { url = "https://files.pythonhosted.org/packages/e8/5b/f235a1437445160e777544f3ade57544daf96ba7e96c1a5b24a6f7ac7004/jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426", size = 490694, upload-time = "2025-05-18T19:04:30.183Z" },
- { url = "https://files.pythonhosted.org/packages/85/a9/9c3d4617caa2ff89cf61b41e83820c27ebb3f7b5fae8a72901e8cd6ff9be/jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12", size = 388992, upload-time = "2025-05-18T19:04:32.028Z" },
- { url = "https://files.pythonhosted.org/packages/68/b1/344fd14049ba5c94526540af7eb661871f9c54d5f5601ff41a959b9a0bbd/jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9", size = 351723, upload-time = "2025-05-18T19:04:33.467Z" },
- { url = "https://files.pythonhosted.org/packages/41/89/4c0e345041186f82a31aee7b9d4219a910df672b9fef26f129f0cda07a29/jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a", size = 392215, upload-time = "2025-05-18T19:04:34.827Z" },
- { url = "https://files.pythonhosted.org/packages/55/58/ee607863e18d3f895feb802154a2177d7e823a7103f000df182e0f718b38/jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853", size = 522762, upload-time = "2025-05-18T19:04:36.19Z" },
- { url = "https://files.pythonhosted.org/packages/15/d0/9123fb41825490d16929e73c212de9a42913d68324a8ce3c8476cae7ac9d/jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86", size = 513427, upload-time = "2025-05-18T19:04:37.544Z" },
- { url = "https://files.pythonhosted.org/packages/d8/b3/2bd02071c5a2430d0b70403a34411fc519c2f227da7b03da9ba6a956f931/jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357", size = 210127, upload-time = "2025-05-18T19:04:38.837Z" },
- { url = "https://files.pythonhosted.org/packages/03/0c/5fe86614ea050c3ecd728ab4035534387cd41e7c1855ef6c031f1ca93e3f/jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00", size = 318527, upload-time = "2025-05-18T19:04:40.612Z" },
- { url = "https://files.pythonhosted.org/packages/b3/4a/4175a563579e884192ba6e81725fc0448b042024419be8d83aa8a80a3f44/jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5", size = 354213, upload-time = "2025-05-18T19:04:41.894Z" },
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" },
+ { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" },
+ { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" },
+ { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" },
+ { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" },
+ { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" },
+ { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" },
+ { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" },
+ { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" },
+ { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" },
+ { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" },
+ { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" },
+ { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" },
+ { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" },
+ { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" },
+ { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" },
+ { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" },
+ { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" },
+ { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" },
+ { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" },
+ { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" },
]
[[package]]
@@ -860,6 +856,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/15/d75fd66aba116ce3732bb1050401394c5ec52074c4f7ee18db8838dd4667/onnxruntime-1.22.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7e823624b015ea879d976cbef8bfaed2f7e2cc233d7506860a76dd37f8f381", size = 16477261, upload-time = "2025-07-10T19:16:03.226Z" },
]
+[[package]]
+name = "openai"
+version = "2.53.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "anyio" },
+ { name = "distro" },
+ { name = "httpx" },
+ { name = "jiter" },
+ { name = "pydantic" },
+ { name = "sniffio" },
+ { name = "tqdm" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/cf/36e3e7235fdf6d125c052acc0970924611b17a20a4fe580596faf4566a65/openai-2.53.0.tar.gz", hash = "sha256:baf5802ad08980e1d9d561e1b996e800c8bcd14af5847c6d0e7a5cc59e4d4116", size = 1099435, upload-time = "2026-08-03T21:42:01.664Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/78/0f/cc6afea3542a5142c5d8fc8211c5e059a8375105d004a41dfa2c7948dbb0/openai-2.53.0-py3-none-any.whl", hash = "sha256:c694ffc747a3c4d1663ef2b07b811315a476164ee5efa3a993967349ebca7618", size = 1659829, upload-time = "2026-08-03T21:41:59.581Z" },
+]
+
[[package]]
name = "opentelemetry-api"
version = "1.35.0"
@@ -1552,9 +1567,9 @@ name = "starting-codebase"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
- { name = "anthropic" },
{ name = "chromadb" },
{ name = "fastapi" },
+ { name = "openai" },
{ name = "python-dotenv" },
{ name = "python-multipart" },
{ name = "sentence-transformers" },
@@ -1563,9 +1578,9 @@ dependencies = [
[package.metadata]
requires-dist = [
- { name = "anthropic", specifier = "==0.58.2" },
{ name = "chromadb", specifier = "==1.0.15" },
{ name = "fastapi", specifier = "==0.116.1" },
+ { name = "openai", specifier = ">=2.53.0" },
{ name = "python-dotenv", specifier = "==1.1.1" },
{ name = "python-multipart", specifier = "==0.0.20" },
{ name = "sentence-transformers", specifier = "==5.0.0" },