* 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
5.7 KiB
5.7 KiB
Commands Module
Purpose: Defines async command handlers for long-running operations via surreal-commands job queue system.
Key Components
Embedding Commands
embed_note_command: Embeds a single note using unified embedding pipeline with content-type aware processing. Uses MARKDOWN content type detection. Retry: 5 attempts, exponential jitter 1-60s.embed_insight_command: Embeds a single source insight. Uses MARKDOWN content type. Retry: 5 attempts, exponential jitter 1-60s.embed_source_command: Embeds a source by chunking full_text with content-type aware splitters (HTML, Markdown, plain), then batch embedding all chunks (batches of 50 with per-batch retry). Retry: 5 attempts, exponential jitter 1-60s.create_insight_command: Creates a source insight with automatic retry on transaction conflicts. Creates the DB record, then submitsembed_insightcommand (fire-and-forget). Retry: 5 attempts, exponential jitter 1-60s. Used bySource.add_insight().rebuild_embeddings_command: Submits individual embed_* commands for all sources/notes/insights. Returns immediately; actual embedding happens async. No retry (coordinator only).
Other Commands
process_source_command: Ingests content throughsource_graph, creates embeddings (optional), and generates insights. Retries on transaction conflicts (exp. jitter, max 15×, 1-120s).run_transformation_command: Runs a transformation on an existing source to generate an insight. Executes the transformation graph (LLM call) then creates insight viacreate_insight_command. Used byPOST /sources/{id}/insightsAPI endpoint. Retry: 5 attempts, exponential jitter 1-60s.generate_podcast_command: Creates podcasts via podcast-creator library. Resolves model registry references and credentials for all profiles before invoking podcast-creator. Validates that outline_llm, transcript_llm, and voice_model are configured.process_text_command(example): Test fixture for text operations (uppercase, lowercase, reverse, word_count).analyze_data_command(example): Test fixture for numeric aggregations.
Important Patterns
- Pydantic I/O: All commands use
CommandInput/CommandOutputsubclasses for type safety and serialization. - Error handling: Permanent errors (ValueError) return failure output; all other exceptions auto-retry via surreal-commands.
- Retry configuration: Uses
stop_on: [ValueError](blocklist approach) - retries all exceptions EXCEPT ValueError. This is more resilient than allowlist as new exception types auto-retry. - Fire-and-forget embedding: Domain models submit embed_* commands via
submit_command()without waiting. Commands process asynchronously. - Content-type aware chunking:
embed_source_commanduseschunk_text()with automatic content type detection (HTML, Markdown, plain text) for optimal text splitting. Default: 1500 char chunks with 225 char overlap. - Batch embedding:
embed_source_commandusesgenerate_embeddings()which automatically batches texts (default 50) with per-batch retry to avoid exceeding provider payload limits. - Mean pooling for large content:
embed_note_commandandembed_insight_commandusegenerate_embedding()which handles content larger than chunk size via mean pooling. - Model dumping: Recursive
full_model_dump()utility converts Pydantic models → dicts for DB/API responses. - Logging: Uses
loguru.loggerthroughout; logs execution start/end and key metrics (processing time, counts). - Time tracking: All commands measure
start_time→processing_timefor monitoring.
Dependencies
External: surreal_commands (command decorator, job queue, submit_command), loguru, pydantic, podcast_creator
Internal: open_notebook.domain.notebook (Source, Note, SourceInsight), open_notebook.utils.chunking (chunk_text, detect_content_type), open_notebook.utils.embedding (generate_embedding, generate_embeddings), open_notebook.database.repository (repo_query, repo_insert)
Quirks & Edge Cases
- source_commands:
ensure_record_id()wraps command IDs for DB storage; transaction conflicts trigger exponential backoff retry. ValueError exceptions are permanent (not retried). - embedding_commands: Content type detection uses file extension as primary source, heuristics as fallback. Chunks >1800 chars trigger secondary splitting. Empty/whitespace-only content returns ValueError (not retried).
- rebuild_embeddings_command: Returns "jobs_submitted" not "processed_items" - embedding is async. Individual commands handle failures with their own retries.
- podcast_commands: Profiles loaded from SurrealDB by name; model configs (credentials) resolved for ALL profiles before podcast-creator validation. Validates outline_llm/transcript_llm/voice_model are set. Episode records created mid-execution.
- Example commands: Accept optional
delay_secondsfor testing async behavior; not for production.
Code Example
@command("process_source", app="open_notebook", retry={
"max_attempts": 5,
"wait_strategy": "exponential_jitter",
"stop_on": [ValueError], # Don't retry validation errors
})
async def process_source_command(input_data: SourceProcessingInput) -> SourceProcessingOutput:
start_time = time.time()
try:
transformations = [await Transformation.get(id) for id in input_data.transformations]
source = await Source.get(input_data.source_id)
result = await source_graph.ainvoke({...})
return SourceProcessingOutput(success=True, ...)
except ValueError as e:
return SourceProcessingOutput(success=False, error_message=str(e)) # No retry
except Exception as e:
raise # Retry all other exceptions