mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-04-30 20:39:55 +00:00
Some checks failed
Development Build / extract-version (push) Has been cancelled
Tests / Backend Tests (push) Has been cancelled
Tests / Frontend Tests (push) Has been cancelled
Development Build / build-regular (push) Has been cancelled
Development Build / build-single (push) Has been cancelled
Development Build / summary (push) Has been cancelled
* feat(podcasts): integrate model registry for profiles and credential passthrough Replace loose provider/model string fields with record<model> references in podcast profiles, enabling credential passthrough to podcast-creator. Backend: - EpisodeProfile: outline_llm, transcript_llm (record<model>) replace outline_provider/outline_model strings. New language field (BCP 47). - SpeakerProfile: voice_model (record<model>) replaces tts_provider/ tts_model strings. Per-speaker voice_model override support. - Migration 14: schema changes making legacy fields optional, adding new record<model> fields. - Data migration (migration.py): auto-converts legacy profiles to model registry references on startup. Idempotent. - podcast_commands.py: resolves credentials for ALL profiles before calling podcast-creator. - New /api/languages endpoint (pycountry + babel) with BCP 47 locale codes (pt-BR, en-US, etc.). Frontend: - Episode/speaker profile forms use ModelSelector instead of manual provider/model dropdowns. - Language dropdown with BCP 47 codes in episode profile form. - Per-speaker TTS voice model override in speaker profile form. - "Templates" tab renamed to "Profiles". - Setup required badge on unconfigured profiles. - i18n updated across all 8 locales. Closes #486, closes #552 * fix(i18n): remove unused legacy podcast provider/model keys Remove 10 orphaned i18n keys across all 8 locales that were left behind after replacing manual provider/model dropdowns with ModelSelector. * fix: address review violations in podcast model registry - P1: Remove profiles with failed model resolution from dicts to prevent podcast-creator validation errors on unrelated profiles - P2: Use centralized QUERY_KEYS.languages instead of inline key - P3: Fix ISO 639-1 → BCP 47 in model field description and CLAUDE.md - P3: Update "templates" → "profiles" in locale string values (all 8) * chore: bump version to 1.8.0
260 lines
14 KiB
Markdown
260 lines
14 KiB
Markdown
# API Module
|
|
|
|
FastAPI-based REST backend exposing services for notebooks, sources, notes, chat, podcasts, and AI model management.
|
|
|
|
## Purpose
|
|
|
|
FastAPI application serving three architectural layers: routes (HTTP endpoints), services (business logic), and models (request/response schemas). Integrates LangGraph workflows (chat, ask, source_chat), SurrealDB persistence, and AI providers via Esperanto.
|
|
|
|
## Architecture Overview
|
|
|
|
**Three layers**:
|
|
1. **Routes** (`routers/*`): HTTP endpoints mapping to services
|
|
2. **Services** (`*_service.py`): Business logic orchestrating domain models, database, graphs, AI providers
|
|
3. **Models** (`models.py`): Pydantic request/response schemas with validation
|
|
|
|
**Startup flow**:
|
|
- Load .env environment variables
|
|
- Initialize CORS middleware + password auth middleware
|
|
- Run database migrations via AsyncMigrationManager on lifespan startup
|
|
- Run podcast profile data migration (legacy string to model registry conversion)
|
|
- Register all routers
|
|
|
|
**Key services**:
|
|
- `chat_service.py`: Invokes chat graph with messages, context
|
|
- `podcast_service.py`: Orchestrates outline + transcript generation
|
|
- `sources_service.py`: Content ingestion, vectorization, metadata
|
|
- `notes_service.py`: Note creation, linking to sources/insights
|
|
- `transformations_service.py`: Applies transformations to content
|
|
- `models_service.py`: Manages AI provider/model configuration
|
|
- `episode_profiles_service.py`: Manages podcast speaker/episode profiles
|
|
|
|
## Component Catalog
|
|
|
|
### Main Application
|
|
- **main.py**: FastAPI app initialization, CORS setup, auth middleware, lifespan event, router registration
|
|
- **Lifespan handler**: Runs AsyncMigrationManager on startup (database schema migration)
|
|
- **Auth middleware**: PasswordAuthMiddleware protects endpoints (password-based access control)
|
|
|
|
### Services (Business Logic)
|
|
- **chat_service.py**: Invokes chat.py graph; handles message history via SqliteSaver
|
|
- **podcast_service.py**: Generates outline (outline.jinja), then transcript (transcript.jinja) for episodes
|
|
- **sources_service.py**: Ingests files/URLs (content_core), extracts text, vectorizes, saves to SurrealDB
|
|
- **transformations_service.py**: Applies transformations via transformation.py graph
|
|
- **models_service.py**: Manages ModelManager config (AI provider overrides)
|
|
- **episode_profiles_service.py**: CRUD for EpisodeProfile and SpeakerProfile models
|
|
- **insights_service.py**: Generates and retrieves source insights
|
|
- **notes_service.py**: Creates notes linked to sources/insights
|
|
|
|
### Models (Schemas)
|
|
- **models.py**: Pydantic schemas for request/response validation
|
|
- Request bodies: ChatRequest, CreateNoteRequest, PodcastGenerationRequest, etc.
|
|
- Response bodies: ChatResponse, NoteResponse, PodcastResponse, etc.
|
|
- Custom validators for enum fields, file paths, model references
|
|
|
|
### Routers
|
|
- **routers/chat.py**: POST /chat
|
|
- **routers/source_chat.py**: POST /source/{source_id}/chat
|
|
- **routers/podcasts.py**: POST /podcasts, GET /podcasts/{id}, POST /podcasts/episodes/{id}/retry, etc.
|
|
- **routers/notes.py**: POST /notes, GET /notes/{id}
|
|
- **routers/sources.py**: POST /sources, GET /sources/{id}, DELETE /sources/{id}
|
|
- **routers/models.py**: GET /models, POST /models/config
|
|
- **routers/credentials.py**: CRUD + test + discover + migrate for credential management
|
|
- **routers/transformations.py**: POST /transformations
|
|
- **routers/insights.py**: GET /sources/{source_id}/insights
|
|
- **routers/auth.py**: POST /auth/password (password-based auth)
|
|
- **routers/languages.py**: GET /languages (available podcast languages via pycountry+babel)
|
|
- **routers/commands.py**: GET /commands/{command_id} (job status tracking)
|
|
|
|
## Common Patterns
|
|
|
|
- **Service injection via FastAPI**: Routers import services directly; no DI framework
|
|
- **Async/await throughout**: All DB queries, graph invocations, AI calls are async
|
|
- **SurrealDB transactions**: Services use repo_query, repo_create, repo_upsert from database layer
|
|
- **Config override pattern**: Models/config override via models_service passed to graph.ainvoke(config=...)
|
|
- **Error handling**: Custom exception hierarchy (`open_notebook.exceptions`) with global FastAPI exception handlers mapping to HTTP status codes (see Error Handling section below). LangGraph nodes use `classify_error()` to convert raw LLM provider errors into typed exceptions with user-friendly messages.
|
|
- **Logging**: loguru logger in main.py; services expected to log key operations
|
|
- **Response normalization**: All responses follow standard schema (data + metadata structure)
|
|
|
|
## Key Dependencies
|
|
|
|
- `fastapi`: FastAPI app, routers, HTTPException
|
|
- `pydantic`: Validation models with Field, field_validator
|
|
- `open_notebook.graphs`: chat, ask, source_chat, source, transformation graphs
|
|
- `open_notebook.database`: SurrealDB repository functions (repo_query, repo_create, repo_upsert)
|
|
- `open_notebook.domain`: Notebook, Source, Note, SourceInsight models
|
|
- `open_notebook.ai.provision`: provision_langchain_model() factory
|
|
- `ai_prompter`: Prompter for template rendering
|
|
- `content_core`: extract_content() for file/URL processing
|
|
- `esperanto`: AI provider client library (LLM, embeddings, TTS)
|
|
- `surreal_commands`: Job queue for async operations (podcast generation)
|
|
- `loguru`: Structured logging
|
|
|
|
## Important Quirks & Gotchas
|
|
|
|
- **Migration auto-run**: Database schema migrations run on every API startup (via lifespan); no manual migration steps
|
|
- **PasswordAuthMiddleware is basic**: Uses simple password check; production deployments should replace with OAuth/JWT
|
|
- **No request rate limiting**: No built-in rate limiting; deployment must add via proxy/middleware
|
|
- **Service state is stateless**: Services don't cache results; each request re-queries database/AI models
|
|
- **Graph invocation is blocking**: chat/podcast workflows may take minutes; no timeout handling in services
|
|
- **Command job fire-and-forget**: podcast_service.py submits jobs but doesn't wait (async job queue pattern)
|
|
- **Model override scoping**: Model config override via RunnableConfig is per-request only (not persistent)
|
|
- **CORS open by default**: main.py CORS settings allow all origins (restrict before production)
|
|
- **No OpenAPI security scheme**: API docs available without auth (disable before production)
|
|
- **Services don't validate user permission**: All endpoints trust authentication layer; no per-notebook permission checks
|
|
|
|
## Error Handling
|
|
|
|
### Global Exception Handlers (`main.py`)
|
|
|
|
FastAPI exception handlers map custom exception types from `open_notebook.exceptions` to HTTP status codes. All error responses include CORS headers.
|
|
|
|
| Exception Class | HTTP Status | Use Case |
|
|
|----------------|-------------|----------|
|
|
| `NotFoundError` | 404 | Resource not found |
|
|
| `InvalidInputError` | 400 | Bad request data |
|
|
| `AuthenticationError` | 401 | Invalid/missing API key |
|
|
| `RateLimitError` | 429 | Provider rate limit exceeded |
|
|
| `ConfigurationError` | 422 | Wrong model name, missing config |
|
|
| `NetworkError` | 502 | Cannot reach AI provider |
|
|
| `ExternalServiceError` | 502 | Provider returned error (500/503, context length) |
|
|
| `OpenNotebookError` (base) | 500 | Any other application error |
|
|
|
|
### Error Classification (`open_notebook.utils.error_classifier`)
|
|
|
|
The `classify_error()` function maps raw exceptions from LLM providers/Esperanto/LangChain into the typed exceptions above with user-friendly messages. Used in all LangGraph graph nodes and SSE streaming handlers.
|
|
|
|
**Flow**: Raw exception → keyword matching → `(ExceptionClass, user_message)` → raised → caught by global handler → HTTP response with descriptive message.
|
|
|
|
### Frontend Integration
|
|
|
|
The frontend `getApiErrorMessage()` helper (`lib/utils/error-handler.ts`) tries i18n mapping first, then falls back to displaying the backend's descriptive error message directly.
|
|
|
|
---
|
|
|
|
## How to Add New Endpoint
|
|
|
|
1. Create router file in `routers/` (e.g., `routers/new_feature.py`)
|
|
2. Import router into `main.py` and register: `app.include_router(new_feature.router, tags=["new_feature"])`
|
|
3. Create service in `new_feature_service.py` with business logic
|
|
4. Define request/response schemas in `models.py` (or create `new_feature_models.py`)
|
|
5. Implement router functions calling service methods
|
|
6. Test with `uv run uvicorn api.main:app --host 0.0.0.0 --port 5055`
|
|
|
|
## Testing Patterns
|
|
|
|
- **Interactive docs**: http://localhost:5055/docs (Swagger UI)
|
|
- **Direct service tests**: Import service, call methods directly with test data
|
|
- **Mock graphs**: Replace graph.ainvoke() with mock for testing service logic
|
|
- **Database: Use test database** (separate SurrealDB instance or mock repo_query)
|
|
|
|
---
|
|
|
|
## Credential Management (API Configuration UI)
|
|
|
|
The Credential Management system enables users to configure AI provider credentials through the UI instead of environment variables. Keys are stored securely in SurrealDB (encrypted via Fernet) with database-first fallback to environment variables.
|
|
|
|
### Router: `routers/credentials.py`
|
|
|
|
**Endpoints**:
|
|
|
|
| Method | Endpoint | Description |
|
|
|--------|----------|-------------|
|
|
| GET | `/credentials` | List all credentials (optional `?provider=` filter) |
|
|
| GET | `/credentials/by-provider/{provider}` | List credentials for a provider |
|
|
| POST | `/credentials` | Create a new credential |
|
|
| GET | `/credentials/{credential_id}` | Get a specific credential |
|
|
| PUT | `/credentials/{credential_id}` | Update a credential |
|
|
| DELETE | `/credentials/{credential_id}` | Delete a credential |
|
|
| POST | `/credentials/{credential_id}/test` | Test connection using credential |
|
|
| POST | `/credentials/{credential_id}/discover` | Discover available models |
|
|
| POST | `/credentials/{credential_id}/register-models` | Register discovered models |
|
|
| POST | `/credentials/migrate-from-provider-config` | Migrate from legacy ProviderConfig |
|
|
|
|
**Supported Providers** (13 total):
|
|
- Simple API key: `openai`, `anthropic`, `google`, `groq`, `mistral`, `deepseek`, `xai`, `openrouter`, `voyage`, `elevenlabs`
|
|
- URL-based: `ollama`
|
|
- Multi-field: `azure`, `vertex`, `openai_compatible`
|
|
|
|
**Security Features**:
|
|
- NEVER returns actual API key values (only metadata)
|
|
- URL validation (SSRF protection) on all URL fields via `_validate_url()`
|
|
- Allows private IPs and localhost for self-hosted services (Ollama, LM Studio)
|
|
- Requires `OPEN_NOTEBOOK_ENCRYPTION_KEY` to be set for storing credentials
|
|
|
|
### Domain Model: `Credential` (`open_notebook/domain/credential.py`)
|
|
|
|
Individual credential records replacing the old `ProviderConfig` singleton. Each credential stores:
|
|
- Provider name, display name, modalities
|
|
- Encrypted API key (via Fernet)
|
|
- Provider-specific config (base_url, endpoint, api_version, etc.)
|
|
|
|
### Integration with Key Provider (`open_notebook/ai/key_provider.py`)
|
|
|
|
The `key_provider` module provisions DB-stored credentials into environment variables for Esperanto compatibility:
|
|
|
|
**Database-first Pattern**:
|
|
1. API endpoint saves keys to `Credential` records (encrypted in SurrealDB)
|
|
2. Before model provisioning, `provision_provider_keys(provider)` checks DB, then env vars
|
|
3. Keys from DB are set as environment variables for Esperanto compatibility
|
|
4. Existing env vars remain unchanged if no DB config exists
|
|
|
|
**Key Functions**:
|
|
- `get_api_key(provider)`: Get API key (DB first, env fallback)
|
|
- `provision_provider_keys(provider)`: Set env vars from DB for a provider
|
|
- `provision_all_keys()`: Load all provider keys from DB into env vars
|
|
|
|
### Authentication
|
|
|
|
No changes to authentication. The `credentials` router uses the same `PasswordAuthMiddleware` as all other endpoints. Keys are protected by the same password-based auth.
|
|
|
|
**Auth Flow** (unchanged from `api/auth.py`):
|
|
- `PasswordAuthMiddleware`: Global middleware checking `Authorization: Bearer {password}` header
|
|
- Default password: `open-notebook-change-me` (set `OPEN_NOTEBOOK_PASSWORD` in production)
|
|
- Docker secrets support via `OPEN_NOTEBOOK_PASSWORD_FILE`
|
|
|
|
### Connection Testing (`open_notebook/ai/connection_tester.py`)
|
|
|
|
The `/credentials/{credential_id}/test` endpoint uses minimal API calls to verify credentials:
|
|
- Loads Credential via `Credential.get(config_id)`, uses `credential.to_esperanto_config()`
|
|
- Uses cheapest/smallest models per provider (TEST_MODELS map)
|
|
- Returns success status and descriptive message
|
|
- Special handlers for ollama, openai_compatible, and azure providers
|
|
|
|
### Migration Workflows
|
|
|
|
Two migration endpoints help users transition to the credential system:
|
|
|
|
**From environment variables** (`POST /credentials/migrate-from-env`):
|
|
1. Checks each provider for env var presence
|
|
2. Creates Credential records from env var values
|
|
3. Returns summary: migrated, skipped, errors
|
|
|
|
**From legacy ProviderConfig** (`POST /credentials/migrate-from-provider-config`):
|
|
1. Reads old ProviderConfig records from database
|
|
2. Converts each to individual Credential records
|
|
3. Returns summary: migrated, skipped, errors
|
|
|
|
### Example Usage
|
|
|
|
```python
|
|
# Check status
|
|
GET /credentials/status
|
|
# Response: {"configured": {"openai": true, "anthropic": false}, "source": {"openai": "database", "anthropic": "none"}, "encryption_configured": true}
|
|
|
|
# Create credential
|
|
POST /credentials
|
|
{"name": "My OpenAI Key", "provider": "openai", "modalities": ["language", "embedding"], "api_key": "sk-proj-..."}
|
|
|
|
# Test connection
|
|
POST /credentials/{credential_id}/test
|
|
# Response: {"provider": "openai", "success": true, "message": "Connection successful"}
|
|
|
|
# Discover models
|
|
POST /credentials/{credential_id}/discover
|
|
# Response: {"provider": "openai", "models": [{"model_id": "gpt-4", "name": "gpt-4", ...}], "credential_id": "..."}
|
|
|
|
# Migrate from env
|
|
POST /credentials/migrate-from-env
|
|
# Response: {"message": "Migration complete. Migrated 3 providers.", "migrated": ["openai", "anthropic", "groq"], "skipped": [], "errors": []}
|
|
```
|