mirror of
https://github.com/lfnovo/open-notebook.git
synced 2026-10-19 01:08:07 +00:00
* docs: development docs match the codebase; add-a-provider playbook - architecture.md: rewritten as a ~110-line map verified against the code (processes, layout, data model from the migrations, graphs, model calls, background jobs); links the accurate topic pages - testing.md, api-reference.md, code-standards.md: rewritten from the real suite, the /api prefix and Bearer auth, and the actual error-handling pattern (no longer contradicts AGENTS.md) - change-playbooks.md: new "Add an AI provider" playbook (registry + the four hand-maintained copies + tests + docs, checked against #1443/#1444); fixes async nodes, command registration, polling paths, locale steps (LanguageToggle, satisfies TranslationShape), colocated frontend tests - development-setup.md is the single setup page (deps, .env with localhost SURREAL_URL, DB -> API -> worker -> frontend, real log lines, /health); quick-start.md and README.dev.md point to it - contributing.md: Conventional Commits, CHANGELOG rule, pre-PR checks that match CI; PR template gains ruff format and frontend checks - AGENTS files agree with the docs (provider edits, routers vs services, async nodes, checkpoint path, ADR-013, stop_on); api/AGENTS.md is a plain link instead of a Claude-only @ import - ADR index explains ADR-010 (reserved for #1332) and number-at-merge; GitHub-compatible encryption anchor; small credentials/security fixes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: address review (CI commands, async source creation, paths, ADR-010, language toggle) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
8.1 KiB
8.1 KiB
Backend Rules (api/ + open_notebook/ + commands/ + prompts/)
Normative rules for working on the Python backend. Architecture and design rationale live in docs/7-DEVELOPMENT/ — this file is only what you must know before changing code. Project-wide rules are in the root AGENTS.md.
Commands
- Run API:
make api(runsrun_api.py: uvicorn with reload on 127.0.0.1:5055; Swagger at http://localhost:5055/docs) - Background jobs need the worker:
make worker-start(surreal-commands-worker --import-modules commands) - Tests:
uv run pytest tests/ - Lint/format/typecheck:
uv run ruff check . --fix,uv run ruff format .anduv run python -m mypy .(CI runsruff format --check)
API layer (api/)
- Routers (
api/routers/) call domain models andrepo_*functions directly. Logic goes in anapi/*_service.pymodule only when it's shared or orchestrates jobs (command_service,credentials_service,podcast_service). New routers are registered inapi/main.pywithprefix="/api". - Provider metadata (env vars, modalities, test models, discovery URLs, docs links) lives in the registry:
open_notebook/ai/provider_registry.pyPROVIDERS.TEST_MODELS,PROVIDER_ENV_CONFIG,PROVIDER_MODALITIESandOPENAI_COMPAT_PROVIDERSare derived from it, andGET /api/providersexposes it. Adding a provider takes the registry entry plus four hand-maintained copies: theSupportedProviderLiteral (api/models.py),PROVIDER_CONFIG(open_notebook/ai/key_provider.py),env_var_mapinget_provider_availability()(api/routers/models.py) andPROVIDER_DISCOVERY_FUNCTIONS(open_notebook/ai/model_discovery.py). Tests catch a missing Literal or discovery entry, not the other two. Follow the Add an AI provider playbook. The frontend consumesGET /api/providersat runtime (useProviders()), so a simple API-key provider needs no frontend edit; the registry declaration order is the display order. - NEVER return API key values from any endpoint — metadata only.
- Every user-supplied URL field must go through
validate_url()(open_notebook/utils/url_validation.py, async) for SSRF protection. Private IPs/localhost are intentionally allowed (self-hosted Ollama, LM Studio). - Errors: raise typed exceptions from
open_notebook.exceptions— global handlers map them to HTTP status codes (NotFoundError→404,InvalidInputError→400,AuthenticationError→401,UnsupportedTypeException→415,RateLimitError→429,ConfigurationError→422,NetworkError/ExternalServiceError→502, otherOpenNotebookError→500). Don't raise bareHTTPExceptionfor domain errors. A router's catch-allexcept Exception(sanitized 500) must come afterexcept OpenNotebookError: raiseso typed errors reach the handlers (tests/test_typed_exceptions_reach_handlers.py). - Requests over
OPEN_NOTEBOOK_MAX_UPLOAD_SIZE_MB(default 100) are rejected byMaxBodySizeMiddlewarebefore auth/routing. - CORS is open by default (
CORS_ORIGINS);allow_credentialsflips toTrueonly when origins are explicit. No rate limiting built in.
AI / model provisioning (open_notebook/ai/)
- All LLM calls in graph nodes go through
provision_langchain_model()— never instantiate provider clients directly. It auto-upgrades tolarge_context_modelabove 105,000 tokens (hard-coded threshold). - Missing/unconfigured model → raise
ConfigurationError(notValueError) so the API returns 422. - Credential-linked models are preferred;
provision_provider_keys()is the env-var fallback and mutatesos.environ— be aware in tests. DefaultModels.get_instance()intentionally bypasses the singleton cache (fresh DB fetch each call).
Graphs (open_notebook/graphs/)
- Nodes are
async def(ask.py,source.py,transformation.py,prompt.py). Only the SqliteSaver-checkpointed graphs (chat.py,source_chat.py) use sync nodes with theasyncio.new_event_loop()/ ThreadPool workaround, because the checkpointer is sync — fragile; don't copy it into new graphs. - Every node wraps LLM calls with
classify_error():except Exception as e: exc_class, message = classify_error(e) raise exc_class(message) from e - Strip extended-thinking output with
clean_thinking_content()before using model responses. - Chat checkpoints (SqliteSaver) live at
./data/sqlite-db/checkpoints.sqlite—LANGGRAPH_CHECKPOINT_FILEinopen_notebook/config.pyis a constant, not an env var.
Domain (open_notebook/domain/)
Source.save()does NOT auto-embed — callsource.vectorize()explicitly (fire-and-forget, returns a command id).Note.save()DOES auto-submitembed_note.ObjectModel.get()is polymorphic via ID prefix — the subclass must be imported first or resolution fails. It raisesNotFoundErroronly for a missing record andDatabaseOperationErrorfor any other DB failure (ADR-013) — so jobs can treat not-found as permanent and DB errors as retryable.RecordModelsubclasses are singletons — callclear_instance()in tests.- Relationship strings passed to
relate()must match the schema (reference,artifact,refers_to).
Database (open_notebook/database/)
- New migration = new file
open_notebook/database/migrations/N.surrealql(+N_down.surrealql) and an edit toAsyncMigrationManager— migrations are hard-coded, not auto-discovered. They run automatically on API startup. - No connection pooling — each
repo_*call opens/closes a connection. - Transaction-conflict
RuntimeErrors are retriable and logged at DEBUG (don't "fix" the missing stack trace). - Bind values as
$params, never f-string user input into SurrealQL (security.md); see the SurrealQL docs for syntax.
Background commands (commands/)
- Commands are
@command("<name>", app="open_notebook", retry={...})functions and must be imported fromcommands/__init__.py(the worker loads--import-modules commands). - Retry config uses a blocklist: exceptions in
stop_on(ValueError,ConfigurationError,NotFoundError, … — check the command's list) fail the job permanently (no retry, job markedfailed); any other exception auto-retries. Note the embed commands catchValueErrorinternally and returnsuccess=False(see the comment incommands/embedding_commands.py). - Submission is fire-and-forget via
submit_command(); commands must be idempotent-ish under retry. - Podcast generation uses
max_attempts: 1on purpose (prevents duplicate episodes); retry is the explicitPOST /api/podcasts/episodes/{id}/retryendpoint.
Prompts (prompts/)
- Template path syntax:
Prompter(prompt_template="ask/entry")→prompts/ask/entry.jinja(forward slashes, no extension). - Data is passed as
data=dict; dict keys must match template variable names exactly. - With a
PydanticOutputParser, Prompter auto-injectsformat_instructions— the template must contain{{ format_instructions }}or the parser is silently ignored. - No template inheritance/composition; templates are flat by design.
- Templates are cached — restart the app after editing.
Environment knobs to know
| Variable | Meaning |
|---|---|
OPEN_NOTEBOOK_ENCRYPTION_KEY (or _FILE) |
Required for credential storage; any string, no default |
OPEN_NOTEBOOK_CHUNK_SIZE / _CHUNK_OVERLAP |
Token-based (default 400 / 15%); restart required |
OPEN_NOTEBOOK_MAX_UPLOAD_SIZE_MB |
Upload cap (default 100) |
CORS_ORIGINS |
Restrict before production |
Deep dives
architecture · code standards · credentials · content processing · podcasts · prompts · change playbooks · testing