diff --git a/.env.example b/.env.example index 18b34cb7e..f88aa79c3 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ # Copy this file to .env and add your actual API key -ANTHROPIC_API_KEY=your-anthropic-api-key-here \ No newline at end of file +ANTHROPIC_API_KEY=your-anthropic-api-key-here +GEMINI_API_KEY=your-gemini-api-key-here \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..af1d1b0a8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,57 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +# Run the application +./run.sh +# or manually: +cd backend && uv run uvicorn app:app --reload --port 8000 + +# Install dependencies +uv sync + +# Run a specific backend file directly (useful for quick testing) +cd backend && uv run python .py +``` + +The app is served at `http://localhost:8000`. FastAPI's auto-generated API docs are at `http://localhost:8000/docs`. + +## Architecture + +This is a RAG (Retrieval-Augmented Generation) chatbot that answers questions about course materials. FastAPI serves both the API and the static frontend from a single process. + +**Request flow:** +1. Frontend (`frontend/script.js`) POSTs `{ query, session_id }` to `/api/query` +2. `app.py` routes to `RAGSystem.query()` — the main orchestrator in `rag_system.py` +3. `RAGSystem` fetches conversation history from `SessionManager`, then calls `AIGenerator` +4. `AIGenerator` calls Claude (claude-sonnet-4) with a `search_course_content` tool available +5. If Claude invokes the tool, `CourseSearchTool` runs a semantic search against ChromaDB and returns formatted chunks +6. Claude makes a second API call to synthesize a final answer from the retrieved chunks +7. Sources and response are returned up the chain to the frontend + +**Key design decisions:** +- Claude drives retrieval via tool use — it decides whether to search and what to search for, rather than always retrieving before generating +- Two ChromaDB collections: `course_catalog` (one entry per course, used for fuzzy course-name resolution) and `course_content` (chunked text, used for semantic search) +- Conversation history is stored in-memory in `SessionManager` — it is lost on server restart +- The session ID is minted server-side on first request and returned to the frontend, which holds it in `currentSessionId` for the rest of the browser session + +**Document format** (`docs/*.txt`): +``` +Course Title: ... +Course Link: ... +Course Instructor: ... +Lesson 1: Title +Lesson Link: ... + +``` +`DocumentProcessor` parses this format and chunks each lesson's content into ~800-character overlapping segments. Course documents are loaded at startup via `app.py`'s `startup_event`. + +**Configuration** (`backend/config.py`): +- `ANTHROPIC_MODEL` — Claude model used for generation +- `EMBEDDING_MODEL` — SentenceTransformer model used for ChromaDB embeddings (`all-MiniLM-L6-v2`) +- `CHUNK_SIZE` / `CHUNK_OVERLAP` — control document chunking +- `MAX_HISTORY` — number of conversation exchanges retained per session +- `CHROMA_PATH` — local path for persisted ChromaDB data diff --git a/backend/app.py b/backend/app.py index 5a69d741d..9ef02a943 100644 --- a/backend/app.py +++ b/backend/app.py @@ -6,7 +6,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.middleware.trustedhost import TrustedHostMiddleware from pydantic import BaseModel -from typing import List, Optional +from typing import Any, List, Optional import os from config import config @@ -39,11 +39,12 @@ class QueryRequest(BaseModel): """Request model for course queries""" query: str session_id: Optional[str] = None + model: str = "claude" class QueryResponse(BaseModel): """Response model for course queries""" answer: str - sources: List[str] + sources: List[Any] session_id: str class CourseStats(BaseModel): @@ -63,7 +64,7 @@ async def query_documents(request: QueryRequest): session_id = rag_system.session_manager.create_session() # Process query using RAG system - answer, sources = rag_system.query(request.query, session_id) + answer, sources = rag_system.query(request.query, session_id, request.model) return QueryResponse( answer=answer, @@ -73,6 +74,15 @@ async def query_documents(request: QueryRequest): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) +@app.delete("/api/session/{session_id}") +async def delete_session(session_id: str): + """Clear a conversation session""" + try: + rag_system.session_manager.clear_session(session_id) + return {"status": "cleared"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + @app.get("/api/courses", response_model=CourseStats) async def get_course_stats(): """Get course analytics and statistics""" diff --git a/backend/config.py b/backend/config.py index d9f6392ef..44cd03e99 100644 --- a/backend/config.py +++ b/backend/config.py @@ -11,6 +11,10 @@ class Config: # Anthropic API settings ANTHROPIC_API_KEY: str = os.getenv("ANTHROPIC_API_KEY", "") ANTHROPIC_MODEL: str = "claude-sonnet-4-20250514" + + # Gemini API settings + GEMINI_API_KEY: str = os.getenv("GEMINI_API_KEY", "") + GEMINI_MODEL: str = "gemini-2.5-flash" # Embedding model settings EMBEDDING_MODEL: str = "all-MiniLM-L6-v2" @@ -18,7 +22,7 @@ class Config: # Document processing settings CHUNK_SIZE: int = 800 # Size of text chunks for vector storage CHUNK_OVERLAP: int = 100 # Characters to overlap between chunks - MAX_RESULTS: int = 5 # Maximum search results to return + MAX_RESULTS: int = 10 # Maximum search results to return MAX_HISTORY: int = 2 # Number of conversation messages to remember # Database paths diff --git a/backend/gemini_generator.py b/backend/gemini_generator.py new file mode 100644 index 000000000..26f2672e4 --- /dev/null +++ b/backend/gemini_generator.py @@ -0,0 +1,132 @@ +from google import genai +from google.genai import types +from typing import List, Optional, Dict, Any + +TYPE_MAP = { + "object": types.Type.OBJECT, + "string": types.Type.STRING, + "integer": types.Type.INTEGER, + "number": types.Type.NUMBER, + "boolean": types.Type.BOOLEAN, + "array": types.Type.ARRAY, +} + + +class GeminiGenerator: + 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 = genai.Client(api_key=api_key) + self.model = model + + def generate_response( + self, + query: str, + conversation_history: Optional[str] = None, + tools: Optional[List] = None, + tool_manager=None, + ) -> str: + system = ( + f"{self.SYSTEM_PROMPT}\n\nPrevious conversation:\n{conversation_history}" + if conversation_history + else self.SYSTEM_PROMPT + ) + contents = [{"role": "user", "parts": [{"text": query}]}] + config = types.GenerateContentConfig( + system_instruction=system, + temperature=0, + max_output_tokens=800, + tools=self._convert_tools(tools) if tools else None, + ) + response = self.client.models.generate_content( + model=self.model, contents=contents, config=config + ) + if self._has_function_call(response) and tool_manager: + return self._handle_tool_execution(response, contents, system, tool_manager) + return response.text + + def _handle_tool_execution(self, initial_response, contents, system, tool_manager) -> str: + contents = contents + [ + {"role": "model", "parts": initial_response.candidates[0].content.parts} + ] + result_parts = [] + for part in initial_response.candidates[0].content.parts: + if part.function_call: + result = tool_manager.execute_tool( + part.function_call.name, **dict(part.function_call.args) + ) + result_parts.append( + types.Part.from_function_response( + name=part.function_call.name, + response={"result": result}, + ) + ) + contents = contents + [{"role": "user", "parts": result_parts}] + final = self.client.models.generate_content( + model=self.model, + contents=contents, + config=types.GenerateContentConfig( + system_instruction=system, temperature=0, max_output_tokens=800 + ), + ) + return final.text + + def _has_function_call(self, response) -> bool: + try: + return any( + p.function_call for p in response.candidates[0].content.parts + ) + except (AttributeError, IndexError): + return False + + def _convert_tools(self, anthropic_tools: List[Dict]) -> List: + return [ + types.Tool( + function_declarations=[ + types.FunctionDeclaration( + name=t["name"], + description=t["description"], + parameters=self._convert_schema(t["input_schema"]), + ) + for t in anthropic_tools + ] + ) + ] + + def _convert_schema(self, schema: Dict) -> types.Schema: + kwargs: Dict[str, Any] = { + "type": TYPE_MAP.get(schema.get("type", "").lower(), types.Type.STRING) + } + if "description" in schema: + kwargs["description"] = schema["description"] + if "properties" in schema: + kwargs["properties"] = { + k: self._convert_schema(v) for k, v in schema["properties"].items() + } + if "required" in schema: + kwargs["required"] = schema["required"] + if "items" in schema: + kwargs["items"] = self._convert_schema(schema["items"]) + return types.Schema(**kwargs) diff --git a/backend/rag_system.py b/backend/rag_system.py index 50d848c8e..cd6fac639 100644 --- a/backend/rag_system.py +++ b/backend/rag_system.py @@ -3,8 +3,9 @@ from document_processor import DocumentProcessor from vector_store import VectorStore from ai_generator import AIGenerator +from gemini_generator import GeminiGenerator from session_manager import SessionManager -from search_tools import ToolManager, CourseSearchTool +from search_tools import ToolManager, CourseSearchTool, CoursePageTool from models import Course, Lesson, CourseChunk class RAGSystem: @@ -17,12 +18,15 @@ def __init__(self, config): 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.gemini_generator = GeminiGenerator(config.GEMINI_API_KEY, config.GEMINI_MODEL) self.session_manager = SessionManager(config.MAX_HISTORY) # Initialize search tools self.tool_manager = ToolManager() self.search_tool = CourseSearchTool(self.vector_store) self.tool_manager.register_tool(self.search_tool) + self.course_page_tool = CoursePageTool(self.vector_store) + self.tool_manager.register_tool(self.course_page_tool) def add_course_document(self, file_path: str) -> Tuple[Course, int]: """ @@ -99,7 +103,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, model: str = "claude") -> Tuple[str, List[str]]: """ Process a user query using the RAG system with tool-based search. @@ -111,15 +115,34 @@ def query(self, query: str, session_id: Optional[str] = None) -> Tuple[str, List Tuple of (response, sources list - empty for tool-based approach) """ # Create prompt for the AI with clear instructions - prompt = f"""Answer this question about course materials: {query}""" + courses_meta = self.vector_store.get_all_courses_metadata() + if courses_meta: + courses_context = "\n".join( + f"- {m['title']} ({m.get('lesson_count', '?')} lessons, instructor: {m.get('instructor', 'unknown')})" + for m in courses_meta + ) + else: + courses_context = "- (none loaded)" + prompt = f"""Answer this question about course materials: {query} + +Available courses: +{courses_context}""" # Get conversation history if session exists history = None if session_id: history = self.session_manager.get_conversation_history(session_id) + # Select generator based on model choice + if model == "gemini": + if not self.config.GEMINI_API_KEY: + raise ValueError("Gemini API key is not configured.") + generator = self.gemini_generator + else: + generator = self.ai_generator + # Generate response using AI with tools - response = self.ai_generator.generate_response( + response = generator.generate_response( query=prompt, conversation_history=history, tools=self.tool_manager.get_tool_definitions(), diff --git a/backend/search_tools.py b/backend/search_tools.py index adfe82352..db768f82a 100644 --- a/backend/search_tools.py +++ b/backend/search_tools.py @@ -1,3 +1,6 @@ +import re +import html as html_module +import requests from typing import Dict, Any, Optional, Protocol from abc import ABC, abstractmethod from vector_store import VectorStore, SearchResults @@ -88,31 +91,93 @@ 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 = [] + seen = set() + for doc, meta in zip(results.documents, results.metadata): course_title = meta.get('course_title', 'unknown') lesson_num = meta.get('lesson_number') - - # Build context header + header = f"[{course_title}" if lesson_num is not None: header += f" - Lesson {lesson_num}" header += "]" - - # Track source for the UI - source = course_title + + label = course_title if lesson_num is not None: - source += f" - Lesson {lesson_num}" - sources.append(source) - + label += f" - Lesson {lesson_num}" + + url = None + if lesson_num is not None: + url = self.store.get_lesson_link(course_title, lesson_num) + + key = (label, url) + if key not in seen: + sources.append({"label": label, "url": url}) + seen.add(key) + formatted.append(f"{header}\n{doc}") - - # Store sources for retrieval + self.last_sources = sources - return "\n\n".join(formatted) +class CoursePageTool(Tool): + """Tool that fetches a course's official page to retrieve real metadata like duration""" + + def __init__(self, vector_store: VectorStore): + self.store = vector_store + + def get_tool_definition(self) -> Dict[str, Any]: + return { + "name": "get_course_page_info", + "description": ( + "Fetch the official course page to get metadata not stored in lesson content, " + "such as total duration, difficulty level, and course description. " + "Use this when asked about how long a course takes or its duration in hours." + ), + "input_schema": { + "type": "object", + "properties": { + "course_name": { + "type": "string", + "description": "Course name or partial name (e.g. 'MCP', 'Computer Use')" + } + }, + "required": ["course_name"] + } + } + + def execute(self, course_name: str) -> str: + # Resolve fuzzy name to exact title, then get its URL + course_title = self.store._resolve_course_name(course_name) + if not course_title: + return f"No course found matching '{course_name}'." + + url = self.store.get_course_link(course_title) + if not url: + return f"No page URL stored for course '{course_title}'." + + try: + response = requests.get( + url, + timeout=10, + headers={"User-Agent": "Mozilla/5.0 (compatible; course-info-fetcher/1.0)"} + ) + response.raise_for_status() + except requests.RequestException as e: + return f"Failed to fetch '{url}': {e}" + + # Strip HTML: remove script/style blocks, then all tags + text = response.text + text = re.sub(r'<(script|style)[^>]*>.*?', '', text, flags=re.DOTALL | re.IGNORECASE) + text = re.sub(r'<[^>]+>', ' ', text) + text = html_module.unescape(text) + text = re.sub(r'\s+', ' ', text).strip() + + # Return enough text to cover the course header / metadata section + return f"Page content for '{course_title}' ({url}):\n\n{text[:3000]}" + + class ToolManager: """Manages available tools for the AI""" diff --git a/frontend/index.html b/frontend/index.html index f8e25a62f..072930719 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,18 +7,37 @@ Course Materials Assistant - +
-

Course Materials Assistant

-

Ask questions about courses, instructors, and content

+
+

Course Materials Assistant

+

Ask questions about courses, instructors, and content

+
+
+ +
+
AI Model
+ +
+ + +
+ +
+
@@ -76,6 +95,6 @@

Course Materials Assistant

- + \ No newline at end of file diff --git a/frontend/script.js b/frontend/script.js index 562a8a363..718e16b80 100644 --- a/frontend/script.js +++ b/frontend/script.js @@ -5,7 +5,33 @@ const API_URL = '/api'; let currentSessionId = null; // DOM elements -let chatMessages, chatInput, sendButton, totalCourses, courseTitles; +let chatMessages, chatInput, sendButton, totalCourses, courseTitles, modelSelect, newChatBtn; + +// Theme management +function initModelSelect() { + modelSelect.value = localStorage.getItem('selectedModel') || 'claude'; + modelSelect.addEventListener('change', () => { + localStorage.setItem('selectedModel', modelSelect.value); + }); +} + +function initTheme() { + const saved = localStorage.getItem('theme'); + const preferred = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + setTheme(saved || preferred); +} + +function setTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem('theme', theme); + const icon = document.getElementById('themeToggle')?.querySelector('.theme-icon'); + if (icon) icon.textContent = theme === 'dark' ? '☀️' : '🌙'; +} + +function toggleTheme() { + const current = document.documentElement.getAttribute('data-theme') || 'dark'; + setTheme(current === 'dark' ? 'light' : 'dark'); +} // Initialize document.addEventListener('DOMContentLoaded', () => { @@ -15,7 +41,12 @@ document.addEventListener('DOMContentLoaded', () => { sendButton = document.getElementById('sendButton'); totalCourses = document.getElementById('totalCourses'); courseTitles = document.getElementById('courseTitles'); - + modelSelect = document.getElementById('modelSelect'); + newChatBtn = document.getElementById('newChatBtn'); + + initTheme(); + initModelSelect(); + document.getElementById('themeToggle').addEventListener('click', toggleTheme); setupEventListeners(); createNewSession(); loadCourseStats(); @@ -30,6 +61,9 @@ function setupEventListeners() { }); + // New chat button + newChatBtn.addEventListener('click', createNewSession); + // Suggested questions document.querySelectorAll('.suggested-item').forEach(button => { button.addEventListener('click', (e) => { @@ -67,7 +101,8 @@ async function sendMessage() { }, body: JSON.stringify({ query: query, - session_id: currentSessionId + session_id: currentSessionId, + model: modelSelect ? modelSelect.value : 'claude' }) }); @@ -122,10 +157,18 @@ function addMessage(content, type, sources = null, isWelcome = false) { let html = `
${displayContent}
`; if (sources && sources.length > 0) { + const chipsHtml = sources.map(src => { + if (src && src.url) { + return `${src.label} ↗`; + } + const label = src && src.label ? src.label : String(src); + return `${label}`; + }).join(''); + html += `
Sources -
${sources.join(', ')}
+
${chipsHtml}
`; } @@ -147,6 +190,13 @@ function escapeHtml(text) { // Removed removeMessage function - no longer needed since we handle loading differently async function createNewSession() { + if (currentSessionId) { + try { + await fetch(`${API_URL}/session/${currentSessionId}`, { method: 'DELETE' }); + } catch (e) { + // Non-critical — proceed with local reset regardless + } + } currentSessionId = null; chatMessages.innerHTML = ''; addMessage('Welcome to the Course Materials Assistant! I can help you with questions about courses, lessons and specific content. What would you like to know?', 'assistant', null, true); diff --git a/frontend/style.css b/frontend/style.css index 825d03675..4e71dc099 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -46,25 +46,66 @@ body { padding: 0; } -/* Header - Hidden */ +/* Header */ header { - display: none; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1.5rem; + background: var(--surface); + border-bottom: 1px solid var(--border-color); + flex-shrink: 0; + gap: 1rem; +} + +.header-brand { + display: flex; + align-items: baseline; + gap: 1rem; + min-width: 0; } header h1 { - font-size: 1.75rem; + font-size: 1.1rem; font-weight: 700; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; margin: 0; + white-space: nowrap; } .subtitle { - font-size: 0.95rem; + font-size: 0.8rem; color: var(--text-secondary); - margin-top: 0.5rem; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.theme-toggle { + background: var(--surface-hover); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 0.4rem 0.6rem; + cursor: pointer; + font-size: 1.1rem; + line-height: 1; + flex-shrink: 0; + transition: all 0.2s ease; + color: var(--text-primary); +} + +.theme-toggle:hover { + border-color: var(--primary-color); + box-shadow: 0 0 0 3px var(--focus-ring); +} + +.theme-toggle:focus { + outline: none; + box-shadow: 0 0 0 3px var(--focus-ring); } /* Main Content Area with Sidebar */ @@ -111,6 +152,71 @@ header h1 { margin-bottom: 0; } +/* Model Selector */ +.model-selector-label { + font-size: 0.875rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 0.5rem; +} + +.model-select { + width: 100%; + padding: 0.5rem 0.75rem; + background: var(--background); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.875rem; + cursor: pointer; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2'%3E%3Cpolyline points='6 9 12 15 18 9'%3E%3C/polyline%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + padding-right: 2rem; +} + +.model-select:focus { + outline: none; + border-color: var(--primary-color); + box-shadow: 0 0 0 3px var(--focus-ring); +} + +.model-select:hover { + border-color: var(--primary-color); +} + +.model-select option { + background: var(--surface); + color: var(--text-primary); +} + +/* New Chat Button */ +.new-chat-btn { + font-size: 0.875rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.5px; + padding: 0.5rem 0; + border: none; + background: none; + cursor: pointer; + transition: color 0.2s ease; + text-align: left; + width: 100%; +} + +.new-chat-btn:hover, +.new-chat-btn:focus { + color: var(--primary-color); + outline: none; +} + /* Main Chat Area */ .chat-main { flex: 1; @@ -241,8 +347,40 @@ header h1 { } .sources-content { - padding: 0 0.5rem 0.25rem 1.5rem; + padding: 0.25rem 0.5rem 0.25rem 0.5rem; + display: flex; + flex-wrap: wrap; + gap: 0.4rem; +} + +.source-chip { + display: inline-flex; + align-items: center; + padding: 0.25rem 0.65rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 500; + background: var(--surface); + border: 1px solid var(--border-color); color: var(--text-secondary); + white-space: nowrap; + transition: all 0.2s ease; + text-decoration: none; +} + +.source-chip--link { + color: var(--primary-color); + border-color: rgba(37, 99, 235, 0.3); + background: rgba(37, 99, 235, 0.08); + cursor: pointer; +} + +.source-chip--link:hover { + background: rgba(37, 99, 235, 0.18); + border-color: var(--primary-color); + color: var(--text-primary); + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(37, 99, 235, 0.25); } /* Markdown formatting styles */ @@ -716,3 +854,25 @@ details[open] .suggested-header::before { width: 280px; } } + +/* Light mode */ +[data-theme="light"] { + --background: #f8fafc; + --surface: #ffffff; + --surface-hover: #f1f5f9; + --text-primary: #0f172a; + --text-secondary: #64748b; + --border-color: #e2e8f0; + --assistant-message: #f1f5f9; + --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.08); + --welcome-bg: #eff6ff; +} + +[data-theme="light"] .message-content code, +[data-theme="light"] .message-content pre { + background-color: rgba(0, 0, 0, 0.05); +} + +[data-theme="light"] .message.welcome-message .message-content { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); +} diff --git a/pyproject.toml b/pyproject.toml index 3f05e2de0..3ef89ebe9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,4 +12,5 @@ dependencies = [ "uvicorn==0.35.0", "python-multipart==0.0.20", "python-dotenv==1.1.1", + "google-genai>=1.73.1", ] diff --git a/uv.lock b/uv.lock index 9ae65c557..d7e0ff210 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.13" [[package]] @@ -124,15 +124,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/84/c2/80633736cd183ee4a62107413def345f7e6e3c01563dbca1417363cf957e/build-1.2.2.post1-py3-none-any.whl", hash = "sha256:1d61c0887fa860c01971625baae8bdd338e517b836a2f70dd1f7aa3a6b2fc5b5", size = 22950, upload-time = "2024-10-06T17:22:23.299Z" }, ] -[[package]] -name = "cachetools" -version = "5.5.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380, upload-time = "2025-02-20T21:01:19.524Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, -] - [[package]] name = "certifi" version = "2025.7.14" @@ -142,6 +133,51 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722, upload-time = "2025-07-14T03:29:26.863Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.2" @@ -239,6 +275,59 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, ] +[[package]] +name = "cryptography" +version = "47.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" }, + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" }, + { url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" }, + { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, +] + [[package]] name = "distro" version = "1.9.0" @@ -300,16 +389,41 @@ wheels = [ [[package]] name = "google-auth" -version = "2.40.3" +version = "2.49.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cachetools" }, + { name = "cryptography" }, { name = "pyasn1-modules" }, - { name = "rsa" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9e/9b/e92ef23b84fa10a64ce4831390b7a4c2e53c0132568d99d4ae61d04c8855/google_auth-2.40.3.tar.gz", hash = "sha256:500c3a29adedeb36ea9cf24b8d10858e152f2412e3ca37829b3fa18e33d63b77", size = 281029, upload-time = "2025-06-04T18:04:57.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/fc/e925290a1ad95c975c459e2df070fac2b90954e13a0370ac505dff78cb99/google_auth-2.49.2.tar.gz", hash = "sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409", size = 333958, upload-time = "2026-04-10T00:41:21.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/76/d241a5c927433420507215df6cac1b1fa4ac0ba7a794df42a84326c68da8/google_auth-2.49.2-py3-none-any.whl", hash = "sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5", size = 240638, upload-time = "2026-04-10T00:41:14.501Z" }, +] + +[package.optional-dependencies] +requests = [ + { name = "requests" }, +] + +[[package]] +name = "google-genai" +version = "1.73.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "distro" }, + { name = "google-auth", extra = ["requests"] }, + { name = "httpx" }, + { name = "pydantic" }, + { name = "requests" }, + { name = "sniffio" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/d8/40f5f107e5a2976bbac52d421f04d14fc221b55a8f05e66be44b2f739fe6/google_genai-1.73.1.tar.gz", hash = "sha256:b637e3a3b9e2eccc46f27136d470165803de84eca52abfed2e7352081a4d5a15", size = 530998, upload-time = "2026-04-14T21:06:19.153Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/63/b19553b658a1692443c62bd07e5868adaa0ad746a0751ba62c59568cd45b/google_auth-2.40.3-py2.py3-none-any.whl", hash = "sha256:1370d4593e86213563547f97a92752fc658456fe4514c809544f330fed45a7ca", size = 216137, upload-time = "2025-06-04T18:04:55.573Z" }, + { url = "https://files.pythonhosted.org/packages/65/af/508e0528015240d710c6763f7c89ff44fab9a94a80b4377e265d692cbfd6/google_genai-1.73.1-py3-none-any.whl", hash = "sha256:af2d2287d25e42a187de19811ef33beb2e347c7e2bdb4dc8c467d78254e43a2c", size = 783595, upload-time = "2026-04-14T21:06:17.464Z" }, ] [[package]] @@ -1131,6 +1245,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/6a/8ec0e4461bf89ef0499ef6c746b081f3520a1e710aeb58730bae693e0681/pybase64-1.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b3635e5873707906e72963c447a67969cfc6bac055432a57a91d7a4d5164fdf", size = 29961, upload-time = "2025-03-02T11:12:21.908Z" }, ] +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.11.7" @@ -1393,18 +1516,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/75/04/5302cea1aa26d886d34cadbf2dc77d90d7737e576c0065f357b96dc7a1a6/rpds_py-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f14440b9573a6f76b4ee4770c13f0b5921f71dde3b6fcb8dabbefd13b7fe05d7", size = 232821, upload-time = "2025-07-01T15:55:55.167Z" }, ] -[[package]] -name = "rsa" -version = "4.9.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyasn1" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, -] - [[package]] name = "safetensors" version = "0.5.3" @@ -1555,6 +1666,7 @@ dependencies = [ { name = "anthropic" }, { name = "chromadb" }, { name = "fastapi" }, + { name = "google-genai" }, { name = "python-dotenv" }, { name = "python-multipart" }, { name = "sentence-transformers" }, @@ -1566,6 +1678,7 @@ requires-dist = [ { name = "anthropic", specifier = "==0.58.2" }, { name = "chromadb", specifier = "==1.0.15" }, { name = "fastapi", specifier = "==0.116.1" }, + { name = "google-genai", specifier = ">=1.73.1" }, { name = "python-dotenv", specifier = "==1.1.1" }, { name = "python-multipart", specifier = "==0.0.20" }, { name = "sentence-transformers", specifier = "==5.0.0" },