Commit graph

19 commits

Author SHA1 Message Date
Luis Novo
2499256b4e
refactor(frontend): consume GET /api/providers instead of the manual provider table (#1108)
The settings frontend now fetches the provider list from the backend
registry endpoint (session-cached react-query hook useProviders())
instead of keeping its own hardcoded copies of provider names, display
names, modalities and docs URLs in lib/providers.tsx.

- New api module (lib/api/providers.ts) + hook (lib/hooks/use-providers.ts)
  with staleTime: Infinity — the list only changes on deploy.
- lib/providers.tsx reduced to modality presentation (icon/color/label)
  behind fallback-safe helpers, so an unknown modality from a future
  provider still renders instead of breaking.
- The backend registry declaration order is the display order (verified
  identical to the old curated ALL_PROVIDERS order; endpoint test now
  pins order, not just set-equality).
- api-keys page gains loading/error states for the provider fetch; new
  i18n keys added to all 14 locales.
- Deleted the regex-based frontend/backend sync test; the
  SupportedProvider Literal test remains the backend guarantee.
- Renamed the dead useProviders() in use-models.ts (availability
  endpoint) to useProviderAvailability() to avoid a name collision.
- Updated stale docs/comments (AGENTS.md, credentials.md, cubic.yaml,
  provider_registry.py, api/models.py) that still described the frontend
  table as a manual sync point.

Closes #1082
2026-07-12 12:46:38 -03:00
Luis Novo
c5b2848691
fix(insights): stamp created/updated on source_insight and stop serializing "None" (#1085)
source_insight was the only content table without created/updated field
definitions. Since the table is SCHEMAFULL and insights are created via a
raw CREATE (create_insight command), SurrealDB silently dropped any
timestamps - rows genuinely held NONE - and the API then wrapped them in
str(), so clients received the literal string "None".

- Migration 19 defines created/updated on source_insight with the same
  time::now() defaults used by source, note and notebook, so new insights
  are stamped at creation. Existing rows are left untouched (no backfill).
- SourceInsightResponse.created/updated are now Optional[str]; both
  routers emit an ISO 8601 string when the timestamp is present and null
  when it is absent (legacy rows), never "None".
- Frontend types updated to string | null accordingly.
- New tests cover the migration definition/registration and the API
  serialization (absent -> null, present -> ISO string).

Note: Python-side stamping alone was not viable - the table is SCHEMAFULL,
so undefined fields written by the client are silently dropped on
SurrealDB 2.x (verified against a live instance), hence the schema
migration mirroring the other tables.

Closes #1045
2026-07-12 11:05:51 -03:00
Luis Novo
11ca9c138c
refactor(api): single context-building implementation (#1079)
Consolidate the three copies of context assembly into
open_notebook/utils/context_builder.py:

- POST /api/chat/context now delegates to build_notebook_context()
  (same request/response shapes, same string-matching config semantics)
- The source-chat graph now calls build_source_context() instead of the
  495-line generalized ContextBuilder class, which had exactly one
  caller and whose notebook/notes/priority-config flexibility was dead
- POST /api/notebooks/{notebook_id}/context removed: it duplicated
  /api/chat/context with a slightly different envelope and had zero
  callers (frontend, docs, tests)

Behavior is pinned by new characterization tests written before the
refactor (tests/test_context_endpoint_characterization.py) plus unit
tests for build_source_context.
2026-07-11 19:55:46 -03:00
Luis Novo
ad12e99c99
refactor(ai): single provider registry as the backend source of truth (#1075)
* refactor(ai): single provider registry as the backend source of truth

Provider metadata (env vars, modalities, connection-test models,
OpenAI-compatible discovery URLs, display names, docs links) is now
defined once in open_notebook/ai/provider_registry.py. The existing
surfaces are derived from it, keeping every import and call-site shape
unchanged:

- api/credentials_service.py: PROVIDER_ENV_CONFIG, PROVIDER_MODALITIES
  and the discovery url_map are built from the registry
- open_notebook/ai/connection_tester.py: TEST_MODELS derived
- open_notebook/ai/model_discovery.py: OPENAI_COMPAT_PROVIDERS built
  from registry entries with a discovery URL (quirk hooks stay local)

The SupportedProvider Literal (typing, can't be built at runtime) and
the frontend provider tables remain manual copies; the cross-check
tests now assert registry keys == Literal == frontend list, plus
registry internal consistency and discovery-table coverage.

New GET /api/providers endpoint exposes the registry (name, display
name, modalities, docs_url, env-configured status) so clients can stop
hardcoding provider lists (frontend adoption is a follow-up).

Docs updated: open_notebook/AGENTS.md and docs/7-DEVELOPMENT/credentials.md
now describe the registry instead of the four-place sync rule.

* refactor(ai): address review findings on the provider registry

- Build PROVIDERS via _build_registry(), which raises on a duplicate
  provider name at import time instead of silently dropping the earlier
  spec (dict-comprehension behavior); regression test added
- Pin the exact OpenAI-compatible provider -> discovery URL mapping in
  a test so a registry edit can't silently drop or misassign a URL
- Give TEST_MODELS a real type annotation
  (Dict[str, Tuple[Optional[str], str]]) instead of bare dict
2026-07-11 19:21:36 -03:00
Pico
a5a1f9beb0
fix: restrict CreateCredentialRequest.provider to a known allowlist (#1016)
* fix: restrict CreateCredentialRequest.provider to a known allowlist

provider was a bare `str`, so any string (typo'd or bogus) flowed
through validation to the domain layer and failed later with a less
clear error, instead of a clean 422 at the API boundary.

Add a SupportedProvider Literal covering the 17 providers already
handled elsewhere - kept in sync with the frontend's ALL_PROVIDERS,
connection_tester.py's TEST_MODELS, and credentials_service.py's
PROVIDER_ENV_CONFIG (a test asserts all three agree on the same set).

* test: lock the frontend provider list into the sync test

The frontend's ALL_PROVIDERS copy was only cross-checked in a docstring;
extract its string literals from the source so all four provider lists
(Literal, TEST_MODELS, PROVIDER_ENV_CONFIG, frontend) are enforced by CI.

---------

Co-authored-by: Luis Novo <lfnovo@gmail.com>
2026-07-10 13:34:46 -03:00
Pico
3887346206
fix: harden source upload path handling and cap array inputs (#1015)
Some checks are pending
Development Build / extract-version (push) Waiting to run
Development Build / build-regular (push) Blocked by required conditions
Development Build / build-single (push) Blocked by required conditions
Development Build / summary (push) Blocked by required conditions
Tests / Backend Tests (push) Waiting to run
Tests / Frontend Tests (push) Waiting to run
Three small, independent hardening fixes to source ingestion:

- generate_unique_filename() checked `if not resolved.exists()` then let
  a separate write happen later - two concurrent uploads landing on the
  same candidate name could both pass the check and clobber each other.
  Now atomically claims the name via Path.touch(exist_ok=False) (O_EXCL)
  as part of the search loop itself.

- _resolve_source_file() and _is_source_file_available() compared
  `resolved_path.startswith(safe_root)` without a trailing separator - a
  sibling directory that merely starts with the same string (e.g.
  "uploads_evil/") would incorrectly be treated as contained, unlike this
  file's other two path checks which already guard with `+ os.sep`. Not
  reachable today (source.asset.file_path is only ever set server-side),
  but this closes the gap and matches the existing pattern.

- SourceCreate.notebooks/transformations had no length limit; both are
  iterated with a per-item DB lookup in create_source(), so an unbounded
  array let a single request trigger an unbounded number of sequential DB
  round trips. Capped at 50.

tests/test_upload_type_mitigations.py adds no code change - it documents
why the adjacent "no file type allowlist on uploads" finding was
investigated and judged low-risk without one (downloads are already
served as application/octet-stream regardless of actual file type).
2026-07-10 11:32:51 -03:00
Matt Van Horn
cc8bfb11ad
feat: Recently Viewed section for notebooks and sources (#979)
* feat: Recently Viewed section for notebooks and sources

Fixes #850

* fix: export RecentlyViewedResponse type from api types

The RecentlyViewed component and notebooks API client import
RecentlyViewedResponse from @/lib/types/api but the interface was missing,
failing the type-check build. Add it to match the backend api/models.py model.

* fix: harden recently-viewed read path and index the recency query

Three robustness fixes on the recently-viewed feature:

- Make the last_viewed_at write-on-read stamping best-effort. _stamp_source_view
  and _stamp_notebook_view now swallow and log their own errors, so a failed
  stamp update can no longer turn a successful GET /sources/{id} or
  GET /notebooks/{id} into a 500.
- Stop leaking internal details from GET /recently-viewed: log the full
  exception server-side and return a generic error message to the client.
- Add indexes on last_viewed_at for the notebook and source tables in
  migration 16 so the ORDER BY last_viewed_at DESC LIMIT recently-viewed
  query does not degrade into a full table scan as data grows. The down
  migration removes the indexes before dropping the field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: register migration 18, add recently-viewed i18n keys and CHANGELOG entry

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Luis Novo <lfnovo@gmail.com>
2026-07-03 12:39:14 -03:00
Matt Van Horn
bb48d3d578
feat: per-transformation custom model selection (#978)
* feat: per-transformation custom model selection

Fixes #776

* fix: validate model_id exists when creating or updating a transformation

The create and update endpoints persisted model_id without checking the
referenced model exists, so an invalid reference was stored silently and only
surfaced later as a 404 at execution time. Add the same existence check
execute_transformation already performs (Model.get -> 404 if missing) to both
the create and update paths, so a bad model_id is rejected up front with a
clear 404. Update still allows clearing model_id to None.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: cover model_id existence validation on transformation create/update

The create test now mocks Model.get so the new existence check passes and
asserts it was awaited with the supplied model_id. The update test expects
Model.get to be awaited twice (once validating the update, once at execute)
rather than once.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: add CHANGELOG entry for per-transformation model selection (#776)

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Luis Novo <lfnovo@gmail.com>
2026-07-03 12:30:50 -03:00
Luis Novo
f578b78f6e
fix(search): reject non-positive limit and survive highlight overflow (#898)
* fix(search): reject non-positive limit and survive highlight overflow

- SearchRequest.limit now has ge=1 so 0/negative values are rejected with
  422 instead of flowing into SurrealDB as LIMIT -1/0 (500 or empty set) (#863)
- text_search() catches SurrealDB's "position overflow" highlight error and
  falls back to vector_search() so large/multi-byte chunks no longer 500 (#648)
- add tests covering limit validation and the overflow fallback path

* fix(search): surface error when vector fallback also fails

Address cubic review on #898:
- text_search() no longer returns [] when the vector-search fallback also
  fails; it logs the traceback and raises DatabaseOperationError so a total
  search outage is not masked as a legitimate empty result set
- document the fallback + failure behavior in domain/CLAUDE.md
- update the test to assert the double-failure path raises
2026-06-16 04:51:34 -03:00
Luis Novo
f8625a5811
feat: bump esperanto to 2.22.0 + Ollama num_ctx override (#833)
Upgrade esperanto 2.20.0 -> 2.22.0. The constraint (>=2.20.0,<3) already
allowed it; this relocks and picks up upstream fixes (OpenRouter json body,
clearer null-embedding errors, streaming ToolCall objects, base_url
trailing-slash normalization, Ollama thinking-model content).

Esperanto 2.21.0 lowered the Ollama num_ctx default from 128000 to 8192 to
avoid OOM on consumer GPUs. We keep that safe default and add an optional
per-credential num_ctx override for self-hosters whose hardware can handle a
larger context window:

- Credential gains a num_ctx field, surfaced via to_esperanto_config() so it
  flows into AIFactory automatically (no ModelManager change needed).
- Credential create/update API schemas + router pass num_ctx through.
- Frontend: optional numeric field on the Ollama credential form, with i18n
  labels translated across all 13 locales.
- Docs: document the new default and the override under AI providers.

Closes #825
2026-06-02 06:20:24 -03:00
Luis Novo
ba01f7df4e fix: handle credential decryption errors gracefully (#740)
- Credential.get_all() now uses per-row error handling instead of failing on first bad row
- Broken credentials include decryption_error field with descriptive message
- DELETE endpoint falls back to direct DB delete when credential can't be decrypted
- Frontend shows amber warning alert for broken credentials with disabled test/edit/discover
- Added i18n translation keys for decryption error warning in all 9 locales
2026-04-12 21:22:37 -03:00
Richard Solomou
7efac77503
feat: expose embed command_id in note API responses (#545)
* feat: expose embed command_id in note API responses

Note.save() already returns the command_id from the embed_note
background job, but the API routes discarded it. This surfaces
the command_id in NoteResponse for both POST and PUT endpoints,
enabling callers to poll GET /api/commands/jobs/{command_id} to
know when embedding has completed.

* Add tests for note API command_id response
2026-02-14 18:11:23 -03:00
Luis Novo
3f352cfcce
feat: credential-based API key management (#477) (#540)
* feat: replace provider config with credential-based system (#477)

Introduce a new credential management system replacing the old
ProviderConfig singleton and standalone Models page. Each credential
stores encrypted API keys and provider-specific configuration with
full CRUD support via a unified settings UI.

Backend:
- Add Credential domain model with encrypted API key storage
- Add credentials API router (CRUD, discovery, registration, testing)
- Add encryption utilities for secure key storage
- Add key_provider for DB-first env-var fallback provisioning
- Add connection tester and model discovery services
- Integrate ModelManager with credential-based config
- Add provider name normalization for Esperanto compatibility
- Add database migrations 11-12 for credential schema

Frontend:
- Rewrite settings/api-keys page with credential management UI
- Add model discovery dialog with search and custom model support
- Add compact default model assignments (primary/advanced layout)
- Add inline model testing and credential connection testing
- Add env-var migration banner
- Update navigation to unified settings page
- Remove standalone models page and old settings components

i18n:
- Update all 7 locale files with credential and model management keys

Closes #477

Co-Authored-By: JFMD <git@jfmd.us>
Co-Authored-By: OraCatQAQ <570768706@qq.com>

* fix: address PR #540 review comments

- Fix docs referencing removed Models page
- Fix error-handler returning raw messages instead of i18n keys
- Fix auth.py misleading docstring and missing no-password guard
- Fix connection_tester using wrong env var for openai_compatible
- Add provision_provider_keys before model discovery/sync
- Update CLAUDE.md to reflect credential-based system
- Fix missing closing brace in api-keys page useEffect

* fix: add logging to credential migration and surface errors in UI

- Add comprehensive logging to migrate-from-env and
  migrate-from-provider-config endpoints (start, per-provider
  progress, success/failure with stack traces, final summary)
- Fix frontend migration hooks ignoring errors array from response
- Show error toast when migration fails instead of "nothing to migrate"
- Invalidate status/envStatus queries after migration so banner updates

* docs: update CLAUDE.md files for credential system

Replace stale ProviderConfig and /api-keys/ references across 8 CLAUDE.md
files to reflect the new Credential-based system from PR #540.

* docs: update user documentation for credential-based system

Replace env var API key instructions with Settings UI credential
workflow across all user-facing documentation. The new flow is:
set OPEN_NOTEBOOK_ENCRYPTION_KEY → start services → add credential
in Settings UI → test → discover models → register.

- Rewrite ai-providers.md, api-configuration.md, environment-reference.md
- Update all quick-start guides and installation docs
- Update ollama.md, openai-compatible.md, local-tts/stt networking sections
- Update reverse-proxy.md, development-setup.md, security.md
- Fix broken links to non-existent docs/deployment/ paths
- Add credentials endpoints to api-reference.md
- Move all API key env vars to deprecated/legacy sections

* chore: bump version to 1.7.0-rc1

Release candidate for credential-based provider management system.

* fix: initialize provider before try block in test_credential

Prevents UnboundLocalError when Credential.get() throws (e.g.,
invalid credential_id) before provider is assigned.

* fix: reorder down migration to drop index before table

Removes duplicate REMOVE FIELD statement and reorders so the index
is dropped before the table, preventing rollback failures.

* refactor: simplify encryption key to always derive via SHA-256

Remove the dual code path in _ensure_fernet_key() that detected native
Fernet keys. Since the credential system is new, always deriving via
SHA-256 removes unnecessary complexity. Also removes the generate_key()
function and Fernet.generate_key() references from docs.

* fix: correct mock patch targets in embedding tests and URL validation

Fix embedding tests patching wrong module path for model_manager
(was targeting open_notebook.utils.embedding.model_manager but it's
imported locally from open_notebook.ai.models). Also fix URL validation
to allow unresolvable hostnames since they may be valid in the
deployment environment (e.g., Azure endpoints, internal DNS).

* feat: add global setup banner for encryption and migration status

Show a persistent banner in AppShell when encryption key is missing
(red) or env var API keys can be migrated (amber), so users see
these prompts on every page instead of only on Settings > API Keys.

Includes a docs link for the encryption banner and i18n support
across all 7 locales.

* docs: several improvements to docker-compose e env examples

* Update README.md

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>

* docs: fix env var format in README and update model setup instructions

Align the encryption key snippet in README Step 2 with the list
format used in the compose file. Replace deprecated "Settings →
Models" instructions with credential-based Discover Models flow.

* fix: address credential system review issues

- Fix SSRF bypass via IPv4-mapped IPv6 addresses (::ffff:169.254.x.x)
- Fix TTS connection test missing config parameter
- Add Azure-specific model discovery using api-key auth header
- Add Vertex static model list for credential-based discovery
- Fix PROVIDER_DISCOVERY_FUNCTIONS incorrect azure/vertex mapping
- Extract business logic to api/credentials_service.py (service layer)
- Move credential Pydantic schemas to api/models.py
- Update tests to use new service imports and ValueError assertions

* fix: sanitize error responses and migrate key_provider to Credential

- Replace raw exception messages in all credential router 500 responses
  with generic error strings (internal details logged server-side only)
- Refactor key_provider.py to use Credential.get_by_provider() instead
  of deprecated ProviderConfig.get_instance()
- Remove unused functions (get_provider_configs, get_default_api_key,
  get_provider_config) that were dead code

---------

Co-authored-by: JFMD <git@jfmd.us>
Co-authored-by: OraCatQAQ <570768706@qq.com>
2026-02-10 08:30:22 -03:00
Luis Novo
301dd4e20a
fix: async insight creation to prevent transaction conflicts (#512)
Migrate insight creation to the command system with automatic retry logic
to prevent SurrealDB transaction conflicts during batch imports.

Changes:
- Add create_insight_command with retry logic for transaction conflicts
- Add run_transformation_command for async transformation execution
- Make Source.add_insight() fire-and-forget (returns command_id)
- Update POST /sources/{id}/insights to return 202 Accepted immediately
- Frontend polls command status until complete, then refreshes
- Auto-update notebook page icon when source gains insights
- Add i18n keys for insight generation feedback

Related to #489
2026-01-31 15:51:27 -03:00
Luis Novo
4e411e0488
feat: add cascade deletion for notebooks with delete preview (#471)
* feat: decrease chunking size for maximum ollama compatibility

* docs: improve i18n info on Claude.md

* feat: add cascade deletion for notebooks with delete preview

- Add Notebook.get_delete_preview() to show counts of affected items
- Add Notebook.delete(delete_exclusive_sources) for cascade deletion
- Always delete notes when notebook is deleted
- Allow user to choose: delete or keep exclusive sources
- Shared sources are always unlinked but never deleted
- Add NotebookDeleteDialog component with radio button options
- Add delete-preview API endpoint
- Update delete endpoint with delete_exclusive_sources param
- Add i18n support for all 5 locales

Closes #77

* docs: remove harcoded config settings
2026-01-25 14:56:14 -03:00
Luis Novo
992442150e
feat: add ability to link existing sources to notebooks (OSS-311) (#177)
* fix: small issue where users cant change podcast segments

* chore: remove playwright mcp from gut

* feat: add ability to link existing sources to notebooks (OSS-311)

Implemented bidirectional source-notebook linking functionality:

Backend changes:
- Add POST endpoint to link sources to notebooks
- Include notebook associations in source detail response
- Implement idempotent linking with proper RecordID handling

Frontend changes:
- Add AddExistingSourceDialog with search and multi-select
- Add NotebookAssociations component for source detail view
- Add dropdown menu to "Add Source" button (new/existing)
- Implement useAddSourcesToNotebook hook with graceful error handling
- Fix dialog pointer-events during close animation
- Add loading states and disable checkboxes for linked sources
- Optimize dialog width with proper responsive breakpoints

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: address PR review feedback

- Fix sources.py query to use correct reference direction (OUT where IN)
- Remove debug console.log statements
- Add truncation warning for 100+ source lists

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-19 17:52:14 -03:00
Luis Novo
df0986cee0
Feature/oss 312 notebook item counts (#175)
* fix: small issue where users cant change podcast segments

* feat: display source and note counts on notebook cards (OSS-312)

Add item counters to notebook listing page showing the number of sources
and notes in each notebook. Counts are displayed in a footer section with
FileText and StickyNote icons for visual consistency with ContextIndicator.

Backend changes:
- Add source_count and note_count to NotebookResponse model
- Update /notebooks endpoint to use SurrealDB graph traversal query
- Query: count(<-reference.in) for sources, count(<-artifact.in) for notes
- Update all notebook endpoints to include counts

Frontend changes:
- Add source_count and note_count to TypeScript NotebookResponse interface
- Add footer section to NotebookCard component
- Display counts with FileText and StickyNote icons (h-3 w-3)
- Use border-top separator and muted-foreground styling

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* style: use colorful badges for notebook counts matching ContextIndicator

Update notebook card counts to use Badge components with primary color
styling instead of plain text, matching the visual style of the
ContextIndicator component in the chat window.

Changes:
- Replace plain text divs with Badge components
- Apply text-primary and border-primary/50 styling
- Use same spacing (gap-1.5, px-1.5, py-0.5) as ContextIndicator
- Remove bullet separator (not needed with badge layout)

Visual result matches the colorful badges shown in chat context.

Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-19 16:03:36 -03:00
Luis Novo
b7e656a319
Version 1 (#160)
New front-end
Launch Chat API
Manage Sources
Enable re-embedding of all contents
Sources can be added without a notebook now
Improved settings
Enable model selector on all chats
Background processing for better experience
Dark mode
Improved Notes

Improved Docs: 
- Remove all Streamlit references from documentation
- Update deployment guides with React frontend setup
- Fix Docker environment variables format (SURREAL_URL, SURREAL_PASSWORD)
- Update docker image tag from :latest to :v1-latest
- Change navigation references (Settings → Models to just Models)
- Update development setup to include frontend npm commands
- Add MIGRATION.md guide for users upgrading from Streamlit
- Update quick-start guide with correct environment variables
- Add port 5055 documentation for API access
- Update project structure to reflect frontend/ directory
- Remove outdated source-chat documentation files
2025-10-18 12:46:22 -03:00
Luis Novo
d7b0fff954
Api podcast migration (#93)
Creates the API layer for Open Notebook
Creates a services API gateway for the Streamlit front-end
Migrates the SurrealDB SDK to the official one
Change all database calls to async
New podcast framework supporting multiple speaker configurations
Implement the surreal-commands library for async processing
Improve docker image and docker-compose configurations
2025-07-17 08:36:11 -03:00