mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
merge(F9): E2E + docs + i18n consolidation
This commit is contained in:
@@ -123,6 +123,7 @@ src/
|
||||
| `app/a2a/` | A2A JSON-RPC 2.0 entry point (`POST /a2a`) |
|
||||
| `app/.well-known/agent.json/` | A2A Agent Card (discovery) |
|
||||
| `app/(dashboard)/dashboard/` | Dashboard UI pages (~30 pages: providers, combos, settings, memory, skills, webhooks, evals, audit, batch, cache, costs, health, system, etc.) |
|
||||
| `app/(dashboard)/dashboard/memory/` | Memory Studio (plan 21): `page.tsx` (3-tab shell), `components/` (MemoryConceptCard, MemoryEngineStatus, EmbeddingSourceSelector, EditMemoryModal, RetrievePreview, QdrantConfigCard, RerankConfigCard), `components/tabs/` (MemoriesTab, PlaygroundTab, EngineTab), `hooks/` (useEngineStatus, useMemorySettings) |
|
||||
| `app/docs/` | Embedded documentation viewer (renders `docs/*.md`) |
|
||||
| `app/landing/` | Marketing landing page |
|
||||
| `app/login/`, `forgot-password/`, `forbidden/` | Auth-related pages |
|
||||
@@ -153,7 +154,10 @@ src/
|
||||
| `evals/` | Eval framework (suites, runner, runtime) — see `docs/frameworks/EVALS.md` |
|
||||
| `guardrails/` | PII masker, prompt injection, vision bridge — see `docs/security/GUARDRAILS.md` |
|
||||
| `jobs/` | Background jobs (cron-like) |
|
||||
| `memory/` | Conversational memory (SQLite FTS5 + Qdrant) — see `docs/frameworks/MEMORY.md` |
|
||||
| `memory/` | Conversational memory (SQLite FTS5 + sqlite-vec hybrid RRF + Qdrant tier 2) — see `docs/frameworks/MEMORY.md` |
|
||||
| `memory/embedding/` | Multi-source embedding layer: `index.ts` (resolver), `remote.ts`, `staticPotion.ts`, `transformersLocal.ts`, `cache.ts`, `types.ts` (plan 21) |
|
||||
| `memory/vectorStore.ts` | sqlite-vec v0.1.9 wrapper — KNN brute-force + hybrid RRF (FTS5 + vector, k=60). Lazy-init, degrades gracefully when sqlite-vec unavailable. (plan 21) |
|
||||
| `memory/reindex.ts` | `runReindexBatch()` — processes memories with `needs_reindex=1` in background; called by `POST /api/memory/reindex` and lazy-backfill path. (plan 21) |
|
||||
| `monitoring/` | Health checks, metrics emission |
|
||||
| `oauth/` | OAuth flows for 14 providers (claude, codex, antigravity, cursor, github, gemini, kimi-coding, kilocode, cline, qwen, kiro, qoder, gitlab-duo, windsurf) |
|
||||
| `plugins/` | Plugin registry |
|
||||
@@ -172,7 +176,8 @@ src/
|
||||
| Subdir | Purpose |
|
||||
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `db/core.ts` | `getDbInstance()` singleton with WAL journaling |
|
||||
| `db/migrations/` | 55 versioned SQL files (idempotent, transactional, numbered `001`..`055`) |
|
||||
| `db/migrations/` | Versioned SQL files (idempotent, transactional). `073_memory_vec.sql` adds `memory_vec_meta` + `needs_reindex` column (plan 21). |
|
||||
| `db/memoryVec.ts`| CRUD for `memory_vec_meta` (active_dim, embedding_signature, last_reset_at, vec_loaded) + `markMemoryNeedsReindex`, `getMemoryReindexQueue`, etc. (plan 21) |
|
||||
| `db/<domain>.ts` | One module per domain: providers, combos, apiKeys, users, sessions, usage, audit*log, webhooks, skills, memory_entries, cloud_agent_tasks, evals*\*, reasoning_cache, etc. |
|
||||
|
||||
### `src/domain/`
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Memory System"
|
||||
version: 3.8.2
|
||||
lastUpdated: 2026-05-13
|
||||
version: 3.8.6
|
||||
lastUpdated: 2026-05-28
|
||||
---
|
||||
|
||||
# Memory System
|
||||
|
||||
> **Source of truth:** `src/lib/memory/` and `src/app/api/memory/`
|
||||
> **Last updated:** 2026-05-13 — v3.8.0
|
||||
> **Last updated:** 2026-05-28 — v3.8.6 (plan 21 — Memory Engine Redesign)
|
||||
|
||||
OmniRoute provides persistent conversational memory keyed by API key (and
|
||||
optionally session id). Memories are extracted automatically from LLM responses
|
||||
@@ -27,17 +27,143 @@ Client → /v1/chat/completions (apiKeyInfo resolved upstream)
|
||||
→ resolveMemoryOwnerId(apiKeyInfo) # extracts id
|
||||
→ getMemorySettings() # cached settings
|
||||
→ shouldInjectMemory(body, {enabled}) # gate
|
||||
→ retrieveMemories(apiKeyId, config) # SQL + optional FTS5
|
||||
→ retrieveMemories(apiKeyId, config) # SQL + FTS5 + optional vector
|
||||
→ injectMemory(body, memories, provider) # system or user message
|
||||
→ upstream provider call
|
||||
→ on response: extractFacts(text, apiKeyId, sessionId) # non-blocking
|
||||
→ setImmediate → createMemory(fact) per match
|
||||
→ embed(content) + upsertVector(id, vec)
|
||||
```
|
||||
|
||||
The injection and extraction call-sites are wired in
|
||||
`open-sse/handlers/chatCore.ts` (look for `retrieveMemories`, `injectMemory`,
|
||||
and `extractFacts`).
|
||||
|
||||
## Engine architecture (3-tier resolution)
|
||||
|
||||
The Memory Engine resolves the retrieval path at runtime based on available
|
||||
infrastructure and settings. Three tiers exist, applied in priority order:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TIER 0 — Keyword (FTS5) │
|
||||
│ Always available. SQLite FTS5 full-text search over │
|
||||
│ content + key. Used when strategy = "exact" or as fallback. │
|
||||
└──────────────────────────────────┬──────────────────────────┘
|
||||
│ strategy = semantic|hybrid?
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TIER 1 — Embedded Vector (sqlite-vec) │
|
||||
│ sqlite-vec v0.1.9 loaded via db.loadExtension(). │
|
||||
│ KNN brute-force over Float32 vectors. Active when: │
|
||||
│ • sqlite-vec loadExtension succeeds │
|
||||
│ • An embedding source is available (remote | static | │
|
||||
│ transformers) that can produce a Float32Array │
|
||||
│ • vec_memories table exists (created on first ready()) │
|
||||
└──────────────────────────────────┬──────────────────────────┘
|
||||
│ qdrant.enabled?
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ TIER 2 — Qdrant (opt-in external vector database) │
|
||||
│ When enabled, replaces sqlite-vec for semantic/hybrid. │
|
||||
│ Requires running Qdrant instance + configured host/port. │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Degradation is automatic and transparent:
|
||||
- If sqlite-vec fails to load, tier 1 is unavailable → falls back to tier 0.
|
||||
- If embedding source returns an error, tier 1 falls back to tier 0.
|
||||
- If Qdrant is unhealthy, tier 2 falls back to tier 1 (or tier 0 if tier 1
|
||||
is also unavailable).
|
||||
|
||||
## Embedding sources
|
||||
|
||||
The embedding layer (`src/lib/memory/embedding/`) resolves which source to use
|
||||
based on `MemorySettingsExtended.embeddingSource`:
|
||||
|
||||
| Source | Description | Key required | Cold start |
|
||||
| -------------- | ------------------------------------------------------------------------------- | ------------ | ---------- |
|
||||
| `remote` | Uses a configured provider's embedding API (OpenAI, Cohere, etc.) | Yes | None |
|
||||
| `static` | Local lookup-table embedding via `potion-base-8M` (WordPiece + mean pooling) | No | ~200ms |
|
||||
| `transformers` | Local ONNX inference via `@huggingface/transformers` v4, `all-MiniLM-L6-v2` | No | ~3s + ~400MB RAM |
|
||||
| `auto` | Runtime resolution: remote (if key exists) → static → transformers → null | Depends | Depends |
|
||||
|
||||
**Resolution order for `auto`:**
|
||||
1. Find first provider in `listEmbeddingProviders()` with `hasKey === true` → `remote`.
|
||||
2. If `settings.staticEnabled === true` → `static`.
|
||||
3. If `settings.transformersEnabled === true` → `transformers`.
|
||||
4. Otherwise → `null` (degrades to FTS5 keyword search).
|
||||
|
||||
The embedding cache (`src/lib/memory/embedding/cache.ts`) uses an in-memory
|
||||
LRU map keyed by `${source}:${model}:${dim}:${sha256(text)}`, capped at
|
||||
`MEMORY_EMBEDDING_CACHE_MAX` entries (default 1000) with a TTL of
|
||||
`MEMORY_EMBEDDING_CACHE_TTL_MS` (default 5 min). Shared across all callers
|
||||
per process lifecycle.
|
||||
|
||||
## Hybrid RRF (k=60)
|
||||
|
||||
When `strategy = "hybrid"` and the vector store is available, retrieval uses
|
||||
Reciprocal Rank Fusion to merge FTS5 and vector results:
|
||||
|
||||
```
|
||||
RRF(d) = Σ 1 / (k + rank_i(d)) where k = 60 (configurable via MEMORY_RRF_K)
|
||||
i
|
||||
```
|
||||
|
||||
Concretely:
|
||||
1. Run FTS5 search → ranked list `R_fts` (position 1..N).
|
||||
2. Run KNN vector search → ranked list `R_vec` (position 1..M).
|
||||
3. For each unique `memoryId`:
|
||||
`rrf_score = 1/(60 + fts_rank)` + `1/(60 + vec_rank)` (0 if not in list).
|
||||
4. Sort by `rrf_score` DESC, apply token budget walk.
|
||||
|
||||
RRF is well-known to be effective without needing score normalization across
|
||||
heterogeneous retrieval systems. The default `k=60` is from the original
|
||||
Cormack et al. paper and works well for small corpora (<10k memories).
|
||||
|
||||
## Backfill (lazy + reindex)
|
||||
|
||||
When the embedding model changes (detected via `embedding_signature`), the
|
||||
vector store is rebuilt and all existing memories are marked
|
||||
`needs_reindex = 1` in the `memories` table.
|
||||
|
||||
**Lazy backfill**: On the next retrieval, any memory missing a vector entry is
|
||||
embedded and inserted into `vec_memories` before the search runs. This
|
||||
amortizes the backfill cost across real requests without blocking startup.
|
||||
|
||||
**Explicit reindex**: The Engine tab in `/dashboard/memory` provides a
|
||||
"Reindex Now" button that calls `POST /api/memory/reindex`. The handler calls
|
||||
`runReindexBatch()` from `src/lib/memory/reindex.ts`, which processes up to
|
||||
`limit` pending entries per request. Progress can be polled via
|
||||
`GET /api/memory/engine-status` (`vectorStore.needsReindex`).
|
||||
|
||||
The `memory_vec_meta` table (migration `073_memory_vec.sql`) stores:
|
||||
- `active_dim` — current vector dimension (null = not yet calibrated).
|
||||
- `embedding_signature` — `${source}:${model}:${dim}` used to detect changes.
|
||||
- `last_reset_at` — timestamp of last full reset.
|
||||
- `vec_loaded` — 0/1 flag whether sqlite-vec loaded successfully.
|
||||
|
||||
## Settings extension
|
||||
|
||||
Seven new fields were added to `MemorySettingsExtended` (plan 21, D9) in
|
||||
`src/shared/schemas/memory.ts`, persisted via `src/lib/db/settings.ts`:
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
| ---------------------- | --------------------------------------------- | ------------ | --------------------------------------------- |
|
||||
| `embeddingSource` | `"remote" \| "static" \| "transformers" \| "auto"` | `"auto"` | Which embedding source to use |
|
||||
| `embeddingProviderModel` | `string \| null` | `null` | Provider/model in `provider/model` format |
|
||||
| `transformersEnabled` | `boolean` | `false` | Opt-in for Transformers.js (MiniLM, ~400MB) |
|
||||
| `staticEnabled` | `boolean` | `false` | Opt-in for static potion-base-8M local model |
|
||||
| `rerankEnabled` | `boolean` | `false` | Enable reranking step (adds +200-500ms/req) |
|
||||
| `rerankProviderModel` | `string \| null` | `null` | Rerank provider/model in `provider/model` format |
|
||||
| `vectorStore` | `"sqlite-vec" \| "qdrant" \| "auto"` | `"auto"` | Which vector backend to use |
|
||||
|
||||
These are exposed via `GET /PUT /api/settings/memory` (schema `MemorySettingsExtendedSchema`).
|
||||
|
||||
> **TODO (D20):** Scope `global` (sharing memories across all API keys) is not
|
||||
> implemented in this release. It requires schema changes and a global retrieval
|
||||
> path. Track separately.
|
||||
|
||||
## Storage Layers
|
||||
|
||||
### Primary: SQLite (`memories` table)
|
||||
@@ -78,10 +204,10 @@ Used by `retrieval.ts` for the `semantic` and `hybrid` strategies (see below).
|
||||
The retrieval code guards with `hasTable("memory_fts")` and falls back to
|
||||
chronological order if the FTS table is missing or the FTS query throws.
|
||||
|
||||
### Optional: Qdrant (vector store)
|
||||
### Optional: Qdrant (vector store tier 2)
|
||||
|
||||
`src/lib/memory/qdrant.ts` implements an optional Qdrant integration for true
|
||||
semantic memory:
|
||||
`src/lib/memory/qdrant.ts` implements an optional Qdrant integration as tier 2
|
||||
vector store. Enabled via `qdrantEnabled` in settings / toggle in Engine tab.
|
||||
|
||||
- `upsertSemanticMemoryPoint()` — embed `key + content` with the configured
|
||||
embedding model, ensure the collection exists (creates cosine-distance
|
||||
@@ -90,24 +216,24 @@ apiKeyId, sessionId, key, content, metadata, createdAtUnix, expiresAtUnix}`.
|
||||
- `searchSemanticMemory(query, topK, scope)` — embed the query, search the
|
||||
collection filtered by `kind = "omniroute_memory"` and optionally by
|
||||
`apiKeyId` / `sessionId`. Caps `topK` to `[1, 20]`.
|
||||
- `deleteSemanticMemoryPoint(id)` — single point delete.
|
||||
- `deleteSemanticMemoryPoint(id)` — single point delete. Called by
|
||||
`deleteMemory()` after the SQLite row is removed (D15).
|
||||
- `cleanupSemanticMemoryPoints({retentionDays})` — bulk delete points whose
|
||||
`expiresAtUnix` is in the past or whose `createdAtUnix` is older than the
|
||||
retention cutoff. Counts first so the dashboard can show actual numbers.
|
||||
- `checkQdrantHealth()` — `GET /readyz` health probe with latency.
|
||||
|
||||
> **TODO**: The chat pipeline (`chatCore.ts`) and the in-tree `retrieveMemories()`
|
||||
> implementation do not currently call `upsertSemanticMemoryPoint` or
|
||||
> `searchSemanticMemory`. The Qdrant integration is feature-flagged via
|
||||
> `qdrantEnabled` in settings, but at the time of writing the
|
||||
> `searchSemanticMemory` results are not fused into retrieval — the
|
||||
> `semantic`/`hybrid` retrieval strategies use SQLite FTS5 only. The settings UI
|
||||
> in `dashboard/settings → MemorySkillsTab` exposes Qdrant config, health,
|
||||
> search test, and cleanup, but the corresponding `/api/settings/qdrant`,
|
||||
> `/api/settings/qdrant/health`, `/api/settings/qdrant/search`, and
|
||||
> `/api/settings/qdrant/cleanup` routes are referenced from the UI but **not
|
||||
> present** under `src/app/api/settings/qdrant/` (only `embedding-models/` is
|
||||
> wired). Treat Qdrant as preview/optional plumbing.
|
||||
The settings UI exposes Qdrant config, health check, semantic search test,
|
||||
and cleanup in the **Engine tab** of `/dashboard/memory`. The corresponding
|
||||
routes under `src/app/api/settings/qdrant/` are all wired as of v3.8.6:
|
||||
|
||||
| Route | Method | Description |
|
||||
| ----- | ------ | ----------- |
|
||||
| `/api/settings/qdrant` | `GET` / `PUT` | Read / update Qdrant settings |
|
||||
| `/api/settings/qdrant/health` | `GET` | Liveness probe + latency |
|
||||
| `/api/settings/qdrant/search` | `POST` | Semantic search test |
|
||||
| `/api/settings/qdrant/cleanup` | `POST` | Remove expired / old points |
|
||||
| `/api/settings/qdrant/embedding-models` | `GET` | List available embedding models |
|
||||
|
||||
## Memory Types
|
||||
|
||||
@@ -202,6 +328,8 @@ Memory configuration is **stored in the DB settings table**, not in env vars.
|
||||
in-process; `invalidateMemorySettingsCache()` is called by the settings PUT
|
||||
route after writes.
|
||||
|
||||
### Legacy fields (all versions)
|
||||
|
||||
| DB key | Type | Default | UI control |
|
||||
| --------------------- | ------- | -------------------------------------------------- | ----------------------------------------------- |
|
||||
| `memoryEnabled` | boolean | `true` | Memory on/off |
|
||||
@@ -213,14 +341,38 @@ route after writes.
|
||||
Note: the UI strategy `"recent"` maps to the internal `"exact"` retrieval
|
||||
strategy via `toMemoryRetrievalConfig()` (chronological order).
|
||||
|
||||
### New fields (v3.8.6, plan 21 D9)
|
||||
|
||||
See also the "Settings extension" section above for field descriptions.
|
||||
|
||||
| DB key | API field | Default |
|
||||
| ------------------------- | ---------------------- | ------------- |
|
||||
| `memoryEmbeddingSource` | `embeddingSource` | `"auto"` |
|
||||
| `memoryEmbeddingModel` | `embeddingProviderModel` | `null` |
|
||||
| `memoryTransformersEnabled` | `transformersEnabled` | `false` |
|
||||
| `memoryStaticEnabled` | `staticEnabled` | `false` |
|
||||
| `memoryRerankEnabled` | `rerankEnabled` | `false` |
|
||||
| `memoryRerankModel` | `rerankProviderModel` | `null` |
|
||||
| `memoryVectorStore` | `vectorStore` | `"auto"` |
|
||||
|
||||
Qdrant-related DB keys (`qdrantEnabled`, `qdrantHost`, `qdrantPort`,
|
||||
`qdrantApiKey`, `qdrantCollection` default `"omniroute_memory"`,
|
||||
`qdrantEmbeddingModel` default `"openai/text-embedding-3-small"`) are read by
|
||||
`normalizeQdrantConfig()` in `qdrant.ts`.
|
||||
|
||||
No `MEMORY_*` or `QDRANT_*` env vars exist today — everything is per-instance
|
||||
DB settings. `OMNIROUTE_MEMORY_MB` (commented out in `.env.example`) is
|
||||
unrelated and refers to Node heap sizing.
|
||||
### Environment variables (v3.8.6)
|
||||
|
||||
Six optional env vars tune the engine's runtime behaviour (documented in `.env.example`):
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------- | ------- | -------------------------------------------------- |
|
||||
| `MEMORY_EMBEDDING_CACHE_TTL_MS` | `300000` | Embedding cache TTL (5 min) |
|
||||
| `MEMORY_EMBEDDING_CACHE_MAX` | `1000` | Max entries in embedding LRU cache |
|
||||
| `MEMORY_TRANSFORMERS_MODEL` | `Xenova/all-MiniLM-L6-v2` | HF repo for Transformers.js model |
|
||||
| `MEMORY_STATIC_MODEL` | `minishlab/potion-base-8M` | HF repo for static potion model |
|
||||
| `MEMORY_STATIC_CACHE_DIR` | `<DATA_DIR>/embeddings` | Where to store downloaded models |
|
||||
| `MEMORY_VEC_TOP_K` | `20` | Default top-K for vector search |
|
||||
| `MEMORY_RRF_K` | `60` | RRF k constant for hybrid search |
|
||||
|
||||
## Summarisation (`summarization.ts`)
|
||||
|
||||
@@ -240,15 +392,39 @@ loss is one-way: original text is overwritten.
|
||||
|
||||
All endpoints require management auth (`requireManagementAuth`).
|
||||
|
||||
| Method | Path | Description |
|
||||
| -------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/api/memory` | Paginated list with filters: `apiKeyId`, `type`, `sessionId`, `q`, `limit`, `page`, `offset`. Response includes `stats.total` and `stats.byType` |
|
||||
| `POST` | `/api/memory` | Create entry (Zod-validated: `content`, `key`, optional `type`, `sessionId`, `apiKeyId`, `metadata`, `expiresAt`). Calls `createMemory()` which upserts on `(apiKeyId, key)` |
|
||||
| `GET` | `/api/memory/[id]` | Fetch a single entry by UUID |
|
||||
| `DELETE` | `/api/memory/[id]` | Delete an entry; returns 404 when missing |
|
||||
| `GET` | `/api/memory/health` | Runs `verifyExtractionPipeline("health-check")` — round-trip create→list→delete to confirm the store is alive. Returns `{working, latencyMs, error?}` |
|
||||
| `GET` | `/api/settings/memory` | Current normalised `MemorySettings` |
|
||||
| `PUT` | `/api/settings/memory` | Update one or more of `enabled`, `maxTokens`, `retentionDays`, `strategy`, `skillsEnabled` |
|
||||
### Core memory endpoints (existing + updated)
|
||||
|
||||
| Method | Path | Description |
|
||||
| ---------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GET` | `/api/memory` | Paginated list with filters: `apiKeyId`, `type`, `sessionId`, `q`, `limit`, `page`, `offset`. Response includes `stats.total`, `stats.tokensUsed`, `stats.hitRate`, `cacheStats` |
|
||||
| `POST` | `/api/memory` | Create entry (Zod-validated: `content`, `key`, optional `type`, `sessionId`, `apiKeyId`, `metadata`, `expiresAt`). Calls `createMemory()` which upserts on `(apiKeyId, key)` |
|
||||
| `GET` | `/api/memory/[id]` | Fetch a single entry by UUID |
|
||||
| `PUT` | `/api/memory/[id]` | Update entry fields (`type`, `key`, `content`, `metadata`). Body: `MemoryUpdatePutSchema`. Also syncs vector if embedding source available. |
|
||||
| `DELETE` | `/api/memory/[id]` | Delete an entry; also deletes from `vec_memories` (D15) and Qdrant best-effort. Returns 404 when missing. |
|
||||
| `GET` | `/api/memory/health` | Runs `verifyExtractionPipeline("health-check")` — round-trip create→list→delete. Returns `{working, latencyMs, error?}` |
|
||||
|
||||
### New memory engine endpoints (plan 21)
|
||||
|
||||
| Method | Path | Description |
|
||||
| -------- | ---------------------------------- | --------------------------------------------------------------------------------------- |
|
||||
| `POST` | `/api/memory/retrieve-preview` | Dry-run of `retrieveMemories` — returns ranked results with score, tier, tokens. Body: `RetrievePreviewSchema`. Does NOT inject or modify memories. |
|
||||
| `GET` | `/api/memory/embedding-providers` | Lists providers with embedding models, indicating which have a configured API key. |
|
||||
| `GET` | `/api/memory/engine-status` | Returns full engine status: keyword tier, embedding resolution, vector store stats, Qdrant health, rerank config. Shape: `MemoryEngineStatusSchema`. |
|
||||
| `POST` | `/api/memory/summarize` | Manually trigger memory compaction. Body: `MemorySummarizeSchema` (`olderThanDays`, `apiKeyId?`, `dryRun`). Returns `{candidates, tokensSaved}`. |
|
||||
| `POST` | `/api/memory/reindex` | Trigger vector reindex for memories with `needs_reindex=1`. Body: `MemoryReindexSchema` (`force`). Returns `{started, pending}`. |
|
||||
|
||||
### Settings endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
| -------- | ---------------------------------- | ------------------------------------------------------------------------ |
|
||||
| `GET` | `/api/settings/memory` | Current normalised `MemorySettingsExtended` (7 new fields + legacy) |
|
||||
| `PUT` | `/api/settings/memory` | Update any field from `MemorySettingsExtendedSchema` (12 total fields) |
|
||||
| `GET` | `/api/settings/qdrant` | Current Qdrant settings (`QdrantSettingsSchema`) |
|
||||
| `PUT` | `/api/settings/qdrant` | Update Qdrant settings. Body: `QdrantSettingsUpdateSchema`. `apiKey` = empty string removes key. |
|
||||
| `GET` | `/api/settings/qdrant/health` | Liveness probe against configured Qdrant instance. Returns `QdrantHealthResultSchema`. |
|
||||
| `POST` | `/api/settings/qdrant/search` | Semantic search test against Qdrant. Body: `QdrantSearchSchema` (`query`, `topK`). |
|
||||
| `POST` | `/api/settings/qdrant/cleanup` | Remove Qdrant points for expired / old memories. |
|
||||
| `GET` | `/api/settings/qdrant/embedding-models` | List embedding models available for Qdrant. |
|
||||
|
||||
The `/api/memory` list query supports either `page`-based pagination
|
||||
(`parsePaginationParams`) **or** raw `offset` — when `offset` is present it
|
||||
@@ -259,31 +435,55 @@ takes precedence and a derived `page` is computed for the response shape.
|
||||
When the MCP server is enabled, three memory tools are registered:
|
||||
|
||||
- `omniroute_memory_search` — `{apiKeyId, query?, type?, maxTokens?, limit?}`
|
||||
→ wraps `retrieveMemories()` with `retrievalStrategy: "exact"`, optionally
|
||||
filters by `type`, and reports `totalTokens`.
|
||||
→ wraps `retrieveMemories()`. As of v3.8.6 (D16), the `strategy` is read
|
||||
from `getMemorySettings()` instead of being hardcoded to `"exact"`. If
|
||||
`query` is provided and `strategy` is `semantic` or `hybrid`, the vector
|
||||
store is used when available.
|
||||
- `omniroute_memory_add` — `{apiKeyId, sessionId?, type, key, content,
|
||||
metadata?}` → wraps `createMemory()`.
|
||||
metadata?}` → wraps `createMemory()`. Accepts only the 4 canonical types:
|
||||
`factual`, `episodic`, `procedural`, `semantic` (D17).
|
||||
- `omniroute_memory_clear` — `{apiKeyId, type?, olderThan?}` → lists matching
|
||||
entries, optionally filters by created-before timestamp, then deletes each
|
||||
via `deleteMemory()`.
|
||||
via `deleteMemory()` (which also removes vectors from sqlite-vec + Qdrant).
|
||||
|
||||
See [MCP-SERVER.md](./MCP-SERVER.md) for transport and scope details.
|
||||
|
||||
## Dashboard
|
||||
## Dashboard (Memory Studio)
|
||||
|
||||
`src/app/(dashboard)/dashboard/memory/page.tsx` provides:
|
||||
`src/app/(dashboard)/dashboard/memory/page.tsx` is now a **3-tab Studio**:
|
||||
|
||||
### Tab: Memórias / Memories
|
||||
- Concept card (collapsible "How it works" explainer).
|
||||
- Real-time list, search, and pagination (debounced 300 ms).
|
||||
- Type filter (`factual` / `episodic` / `procedural` / `semantic` / all).
|
||||
- Add-memory modal (key, content, type).
|
||||
- Delete per row.
|
||||
- Inline edit (pencil button → `PUT /api/memory/[id]`).
|
||||
- Delete per row (with confirmation dialog).
|
||||
- JSON export of the current page; JSON import via file picker.
|
||||
- Stat cards: `totalEntries`, `tokensUsed`, `hitRate`.
|
||||
- "Compact old" button → `POST /api/memory/summarize` (dry-run first shows
|
||||
candidate count, then confirms).
|
||||
- A green/red health dot driven by `GET /api/memory/health`.
|
||||
- Stat cards: `totalEntries`, `tokensUsed`, `hitRate` (the latter two come
|
||||
from the API stats payload).
|
||||
|
||||
Memory and Qdrant settings live under
|
||||
`/dashboard/settings → Memory & Skills` (`MemorySkillsTab.tsx`).
|
||||
### Tab: Playground
|
||||
- Query input + strategy selector (Exact / Semantic / Hybrid) + token budget.
|
||||
- "Simulate" → `POST /api/memory/retrieve-preview` — shows ranked results with
|
||||
`score`, `tier`, `tokens`, `vecScore`, `ftsScore`.
|
||||
- Resolution panel showing which embedding source / vector store was used and
|
||||
whether a fallback occurred.
|
||||
|
||||
### Tab: Engine
|
||||
- Engine status panel (keyword FTS5 chip, embedding chip, vector store chip,
|
||||
Qdrant health chip, rerank chip).
|
||||
- "Reindex Now" button → `POST /api/memory/reindex`.
|
||||
- Embedding source selector (auto / remote / static / transformers + toggles).
|
||||
- Qdrant config card (enable toggle, host/port/collection/key, test connection,
|
||||
semantic search test, cleanup).
|
||||
- Rerank config card (enable toggle, provider/model selector).
|
||||
|
||||
Memory and Qdrant settings also live under
|
||||
`/dashboard/settings → Memory & Skills` (`MemorySkillsTab.tsx`) for
|
||||
the legacy/global settings surface.
|
||||
|
||||
## Caching
|
||||
|
||||
@@ -316,12 +516,28 @@ default TTL 5 min).
|
||||
- [API_REFERENCE.md](../reference/API_REFERENCE.md) — broader API surface.
|
||||
- Source modules:
|
||||
- `src/lib/memory/types.ts`, `schemas.ts`
|
||||
- `src/lib/memory/store.ts`, `retrieval.ts`, `injection.ts`
|
||||
- `src/lib/memory/store.ts`, `retrieval.ts`, `injection.ts`, `reindex.ts`
|
||||
- `src/lib/memory/extraction.ts`, `summarization.ts`, `verify.ts`
|
||||
- `src/lib/memory/settings.ts`, `qdrant.ts`, `cache.ts`
|
||||
- `src/lib/memory/vectorStore.ts` — sqlite-vec + hybrid RRF
|
||||
- `src/lib/memory/embedding/index.ts` — multi-source embedding layer
|
||||
- `src/lib/memory/embedding/types.ts`, `remote.ts`, `staticPotion.ts`,
|
||||
`transformersLocal.ts`, `cache.ts`
|
||||
- `src/shared/schemas/memory.ts` — Zod schemas for all memory API bodies
|
||||
- `src/shared/schemas/qdrant.ts` — Zod schemas for Qdrant settings/ops
|
||||
- `src/lib/db/memoryVec.ts` — CRUD for `memory_vec_meta`
|
||||
- `src/lib/db/migrations/015_create_memories.sql`,
|
||||
`022_add_memory_fts5.sql`, `023_fix_memory_fts_uuid.sql`
|
||||
`022_add_memory_fts5.sql`, `023_fix_memory_fts_uuid.sql`,
|
||||
`073_memory_vec.sql`
|
||||
- `src/app/api/memory/route.ts`, `[id]/route.ts`, `health/route.ts`
|
||||
- `src/app/api/memory/retrieve-preview/route.ts`
|
||||
- `src/app/api/memory/engine-status/route.ts`
|
||||
- `src/app/api/memory/embedding-providers/route.ts`
|
||||
- `src/app/api/memory/summarize/route.ts`
|
||||
- `src/app/api/memory/reindex/route.ts`
|
||||
- `src/app/api/settings/memory/route.ts`
|
||||
- `src/app/api/settings/qdrant/route.ts` + sub-routes
|
||||
- `src/app/(dashboard)/dashboard/memory/` — Studio UI (page + components +
|
||||
tabs + hooks)
|
||||
- `open-sse/handlers/chatCore.ts` (injection / extraction wiring)
|
||||
- `open-sse/mcp-server/tools/memoryTools.ts`
|
||||
|
||||
@@ -75,6 +75,11 @@ tags:
|
||||
description: Fallback chain management
|
||||
- name: Telemetry
|
||||
description: Telemetry and token health monitoring
|
||||
- name: Memory
|
||||
description: >-
|
||||
Conversational memory management — CRUD, engine status, playground preview,
|
||||
summarization, reindex, and Qdrant settings (plan 21 — v3.8.6).
|
||||
All routes require management auth.
|
||||
|
||||
paths:
|
||||
# ─── Proxy Endpoints ──────────────────────────────────────────
|
||||
@@ -2730,6 +2735,801 @@ paths:
|
||||
"500":
|
||||
description: Failed to parse OpenAPI spec
|
||||
|
||||
# ─── Memory Engine (plan 21 — v3.8.6) ─────────────────────────
|
||||
|
||||
/api/memory:
|
||||
get:
|
||||
tags: [Memory]
|
||||
summary: List memory entries
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
parameters:
|
||||
- name: apiKeyId
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: type
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum: [factual, episodic, procedural, semantic]
|
||||
- name: sessionId
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: q
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 200
|
||||
default: 50
|
||||
- name: page
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- name: offset
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
responses:
|
||||
"200":
|
||||
description: Paginated list of memories with stats
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/MemoryEntry"
|
||||
total:
|
||||
type: integer
|
||||
totalPages:
|
||||
type: integer
|
||||
stats:
|
||||
type: object
|
||||
properties:
|
||||
total:
|
||||
type: integer
|
||||
tokensUsed:
|
||||
type: integer
|
||||
hitRate:
|
||||
type: number
|
||||
cacheStats:
|
||||
type: object
|
||||
properties:
|
||||
hits:
|
||||
type: integer
|
||||
misses:
|
||||
type: integer
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
post:
|
||||
tags: [Memory]
|
||||
summary: Create a memory entry
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [content, key]
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
minLength: 1
|
||||
key:
|
||||
type: string
|
||||
minLength: 1
|
||||
type:
|
||||
type: string
|
||||
enum: [factual, episodic, procedural, semantic]
|
||||
default: factual
|
||||
sessionId:
|
||||
type: string
|
||||
nullable: true
|
||||
apiKeyId:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
expiresAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
responses:
|
||||
"201":
|
||||
description: Created memory entry
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MemoryEntry"
|
||||
"400":
|
||||
description: Validation error
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
/api/memory/{id}:
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
description: Memory UUID
|
||||
get:
|
||||
tags: [Memory]
|
||||
summary: Get a single memory entry
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Memory entry
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MemoryEntry"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
description: Memory not found
|
||||
put:
|
||||
tags: [Memory]
|
||||
summary: Update a memory entry
|
||||
description: >-
|
||||
Update `type`, `key`, `content`, and/or `metadata` of an existing memory.
|
||||
If an embedding source is available, the vector in `vec_memories` is also
|
||||
regenerated. Corresponds to `MemoryUpdatePutSchema`.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
enum: [factual, episodic, procedural, semantic]
|
||||
key:
|
||||
type: string
|
||||
minLength: 1
|
||||
content:
|
||||
type: string
|
||||
minLength: 1
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
additionalProperties: false
|
||||
responses:
|
||||
"200":
|
||||
description: Updated memory entry
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MemoryEntry"
|
||||
"400":
|
||||
description: Validation error
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
description: Memory not found
|
||||
delete:
|
||||
tags: [Memory]
|
||||
summary: Delete a memory entry
|
||||
description: >-
|
||||
Deletes the SQLite row, removes the vector from `vec_memories`, and
|
||||
best-effort deletes the point from Qdrant.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Deleted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
success:
|
||||
type: boolean
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
description: Memory not found
|
||||
|
||||
/api/memory/health:
|
||||
get:
|
||||
tags: [Memory]
|
||||
summary: Memory store health check
|
||||
description: >-
|
||||
Round-trip create→list→delete to verify the store is alive.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Health result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
working:
|
||||
type: boolean
|
||||
latencyMs:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
/api/memory/retrieve-preview:
|
||||
post:
|
||||
tags: [Memory]
|
||||
summary: Dry-run memory retrieval (Playground)
|
||||
description: >-
|
||||
Simulates `retrieveMemories()` for a given query and returns the ranked
|
||||
results with score, tier, and token count. Does NOT modify any memory.
|
||||
Corresponds to `RetrievePreviewSchema`.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [query]
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
minLength: 1
|
||||
strategy:
|
||||
type: string
|
||||
enum: [exact, semantic, hybrid]
|
||||
default: hybrid
|
||||
maxTokens:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 16000
|
||||
default: 2000
|
||||
apiKeyId:
|
||||
type: string
|
||||
description: Optional — tests global pool when omitted
|
||||
limit:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
additionalProperties: false
|
||||
responses:
|
||||
"200":
|
||||
description: Preview results
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
memories:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
type:
|
||||
type: string
|
||||
enum: [factual, episodic, procedural, semantic]
|
||||
key:
|
||||
type: string
|
||||
content:
|
||||
type: string
|
||||
score:
|
||||
type: number
|
||||
tokens:
|
||||
type: integer
|
||||
tier:
|
||||
type: string
|
||||
enum: [fts5, vector, hybrid-rrf, qdrant]
|
||||
vecScore:
|
||||
type: number
|
||||
nullable: true
|
||||
ftsScore:
|
||||
type: number
|
||||
nullable: true
|
||||
resolution:
|
||||
type: object
|
||||
properties:
|
||||
embeddingSource:
|
||||
type: string
|
||||
enum: [remote, static, transformers]
|
||||
nullable: true
|
||||
embeddingModel:
|
||||
type: string
|
||||
nullable: true
|
||||
vectorStore:
|
||||
type: string
|
||||
enum: [sqlite-vec, qdrant, none]
|
||||
strategyUsed:
|
||||
type: string
|
||||
enum: [exact, semantic, hybrid]
|
||||
rerankApplied:
|
||||
type: boolean
|
||||
fallbackReason:
|
||||
type: string
|
||||
nullable: true
|
||||
totalTokensUsed:
|
||||
type: integer
|
||||
budgetMaxTokens:
|
||||
type: integer
|
||||
"400":
|
||||
description: Validation error
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
/api/memory/embedding-providers:
|
||||
get:
|
||||
tags: [Memory]
|
||||
summary: List embedding providers
|
||||
description: >-
|
||||
Returns all providers that have embedding-capable models, indicating
|
||||
which have an active API key configured.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Provider list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
providers:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
provider:
|
||||
type: string
|
||||
hasKey:
|
||||
type: boolean
|
||||
models:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: "Format: provider/model"
|
||||
name:
|
||||
type: string
|
||||
dimensions:
|
||||
type: integer
|
||||
nullable: true
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
/api/memory/engine-status:
|
||||
get:
|
||||
tags: [Memory]
|
||||
summary: Memory engine status
|
||||
description: >-
|
||||
Returns the full engine status including keyword tier availability,
|
||||
embedding resolution, vector store statistics (sqlite-vec), Qdrant
|
||||
health, and rerank configuration. Corresponds to `MemoryEngineStatusSchema`.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Engine status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
keyword:
|
||||
type: object
|
||||
properties:
|
||||
available:
|
||||
type: boolean
|
||||
backend:
|
||||
type: string
|
||||
enum: [FTS5]
|
||||
embedding:
|
||||
type: object
|
||||
properties:
|
||||
source:
|
||||
type: string
|
||||
enum: [remote, static, transformers]
|
||||
nullable: true
|
||||
model:
|
||||
type: string
|
||||
nullable: true
|
||||
dimensions:
|
||||
type: integer
|
||||
nullable: true
|
||||
available:
|
||||
type: boolean
|
||||
reason:
|
||||
type: string
|
||||
cacheStats:
|
||||
type: object
|
||||
properties:
|
||||
hits:
|
||||
type: integer
|
||||
misses:
|
||||
type: integer
|
||||
size:
|
||||
type: integer
|
||||
vectorStore:
|
||||
type: object
|
||||
properties:
|
||||
backend:
|
||||
type: string
|
||||
enum: [sqlite-vec, qdrant, none]
|
||||
available:
|
||||
type: boolean
|
||||
rowCount:
|
||||
type: integer
|
||||
needsReindex:
|
||||
type: integer
|
||||
reason:
|
||||
type: string
|
||||
qdrant:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
healthy:
|
||||
type: boolean
|
||||
nullable: true
|
||||
latencyMs:
|
||||
type: number
|
||||
nullable: true
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
rerank:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
provider:
|
||||
type: string
|
||||
nullable: true
|
||||
model:
|
||||
type: string
|
||||
nullable: true
|
||||
available:
|
||||
type: boolean
|
||||
reason:
|
||||
type: string
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
/api/memory/summarize:
|
||||
post:
|
||||
tags: [Memory]
|
||||
summary: Compact old memories
|
||||
description: >-
|
||||
Manually triggers memory compaction for memories older than
|
||||
`olderThanDays`. Use `dryRun: true` to preview candidates.
|
||||
Corresponds to `MemorySummarizeSchema`.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
olderThanDays:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 365
|
||||
default: 30
|
||||
apiKeyId:
|
||||
type: string
|
||||
description: Optional — compacts all keys when omitted
|
||||
dryRun:
|
||||
type: boolean
|
||||
default: false
|
||||
additionalProperties: false
|
||||
responses:
|
||||
"200":
|
||||
description: Summarization result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
candidates:
|
||||
type: integer
|
||||
tokensSaved:
|
||||
type: integer
|
||||
dryRun:
|
||||
type: boolean
|
||||
"400":
|
||||
description: Validation error
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
/api/memory/reindex:
|
||||
post:
|
||||
tags: [Memory]
|
||||
summary: Trigger vector reindex
|
||||
description: >-
|
||||
Starts background reindexing of memories with `needs_reindex = 1`.
|
||||
Use `force: true` to regenerate ALL vectors regardless of index status.
|
||||
Corresponds to `MemoryReindexSchema`.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
force:
|
||||
type: boolean
|
||||
default: false
|
||||
description: >-
|
||||
When true, marks all memories needs_reindex=1 before running.
|
||||
additionalProperties: false
|
||||
responses:
|
||||
"200":
|
||||
description: Reindex started
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
started:
|
||||
type: boolean
|
||||
pending:
|
||||
type: integer
|
||||
description: Memories still pending after this batch
|
||||
"400":
|
||||
description: Validation error
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
/api/settings/memory:
|
||||
get:
|
||||
tags: [Memory, Settings]
|
||||
summary: Get memory settings
|
||||
description: >-
|
||||
Returns the extended memory settings including 7 new fields added in
|
||||
plan 21 (embeddingSource, embeddingProviderModel, transformersEnabled,
|
||||
staticEnabled, rerankEnabled, rerankProviderModel, vectorStore).
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Extended memory settings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MemorySettingsExtended"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
put:
|
||||
tags: [Memory, Settings]
|
||||
summary: Update memory settings
|
||||
description: >-
|
||||
Update any subset of the extended memory settings.
|
||||
All fields are optional; only provided fields are updated.
|
||||
Schema: `MemorySettingsExtendedSchema`.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MemorySettingsExtended"
|
||||
responses:
|
||||
"200":
|
||||
description: Updated memory settings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/MemorySettingsExtended"
|
||||
"400":
|
||||
description: Validation error
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
/api/settings/qdrant:
|
||||
get:
|
||||
tags: [Memory, Settings]
|
||||
summary: Get Qdrant settings
|
||||
description: >-
|
||||
Returns current Qdrant configuration. The `apiKey` field is never
|
||||
returned raw — use `hasApiKey` / `apiKeyMasked` instead.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Qdrant settings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/QdrantSettings"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
put:
|
||||
tags: [Memory, Settings]
|
||||
summary: Update Qdrant settings
|
||||
description: >-
|
||||
Update Qdrant configuration. Pass `apiKey: ""` to remove the stored
|
||||
key. Schema: `QdrantSettingsUpdateSchema`.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
host:
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 65535
|
||||
collection:
|
||||
type: string
|
||||
minLength: 1
|
||||
embeddingModel:
|
||||
type: string
|
||||
minLength: 1
|
||||
apiKey:
|
||||
type: string
|
||||
description: Empty string removes the key
|
||||
additionalProperties: false
|
||||
responses:
|
||||
"200":
|
||||
description: Updated Qdrant settings
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/QdrantSettings"
|
||||
"400":
|
||||
description: Validation error
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
/api/settings/qdrant/health:
|
||||
get:
|
||||
tags: [Memory]
|
||||
summary: Qdrant health probe
|
||||
description: >-
|
||||
Performs a liveness check against the configured Qdrant instance.
|
||||
Returns latency and any connection error (sanitized — no stack traces).
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Health result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/QdrantHealthResult"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
/api/settings/qdrant/search:
|
||||
post:
|
||||
tags: [Memory]
|
||||
summary: Qdrant semantic search test
|
||||
description: >-
|
||||
Performs a test semantic search against the Qdrant collection.
|
||||
Useful for validating that the integration works end-to-end.
|
||||
Schema: `QdrantSearchSchema`.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [query]
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
minLength: 1
|
||||
topK:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 50
|
||||
default: 5
|
||||
additionalProperties: false
|
||||
responses:
|
||||
"200":
|
||||
description: Search results
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
"400":
|
||||
description: Validation error
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"503":
|
||||
description: Qdrant unavailable (structured error, no stack trace)
|
||||
|
||||
/api/settings/qdrant/cleanup:
|
||||
post:
|
||||
tags: [Memory]
|
||||
summary: Clean up expired Qdrant points
|
||||
description: >-
|
||||
Removes Qdrant points for memories that have expired or exceeded
|
||||
the configured retention window.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Cleanup result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
deleted:
|
||||
type: integer
|
||||
checked:
|
||||
type: integer
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"503":
|
||||
description: Qdrant unavailable (structured error, no stack trace)
|
||||
|
||||
/api/settings/qdrant/embedding-models:
|
||||
get:
|
||||
tags: [Memory]
|
||||
summary: List Qdrant embedding models
|
||||
description: Returns the list of embedding models available for use with Qdrant.
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Embedding models list
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
models:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
@@ -3262,3 +4062,127 @@ components:
|
||||
type: integer
|
||||
priority:
|
||||
type: integer
|
||||
|
||||
# ─── Memory Engine schemas (plan 21 — v3.8.6) ─────────────────
|
||||
|
||||
MemoryEntry:
|
||||
type: object
|
||||
description: A single persisted memory entry
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
description: UUID
|
||||
apiKeyId:
|
||||
type: string
|
||||
sessionId:
|
||||
type: string
|
||||
nullable: true
|
||||
type:
|
||||
type: string
|
||||
enum: [factual, episodic, procedural, semantic]
|
||||
key:
|
||||
type: string
|
||||
description: Stable upsert key (e.g. preference:i_prefer_python)
|
||||
content:
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
createdAt:
|
||||
type: string
|
||||
format: date-time
|
||||
updatedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
expiresAt:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
needsReindex:
|
||||
type: integer
|
||||
description: 1 if the vector for this memory is stale or missing
|
||||
|
||||
MemorySettingsExtended:
|
||||
type: object
|
||||
description: >-
|
||||
Extended memory settings including 7 new fields from plan 21.
|
||||
All fields are optional for PUT (patch semantics).
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
maxTokens:
|
||||
type: integer
|
||||
minimum: 0
|
||||
maximum: 16000
|
||||
retentionDays:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 365
|
||||
strategy:
|
||||
type: string
|
||||
enum: [recent, semantic, hybrid]
|
||||
skillsEnabled:
|
||||
type: boolean
|
||||
embeddingSource:
|
||||
type: string
|
||||
enum: [remote, static, transformers, auto]
|
||||
description: >-
|
||||
Which embedding source to use. "auto" = remote > static > transformers.
|
||||
embeddingProviderModel:
|
||||
type: string
|
||||
nullable: true
|
||||
description: >-
|
||||
Embedding provider/model in "provider/model" format (e.g. openai/text-embedding-3-small).
|
||||
transformersEnabled:
|
||||
type: boolean
|
||||
description: Opt-in for Transformers.js local MiniLM model (~400MB RAM)
|
||||
staticEnabled:
|
||||
type: boolean
|
||||
description: Opt-in for static potion-base-8M local model
|
||||
rerankEnabled:
|
||||
type: boolean
|
||||
description: Enable reranking step (+200-500ms/req)
|
||||
rerankProviderModel:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Rerank provider/model in "provider/model" format
|
||||
vectorStore:
|
||||
type: string
|
||||
enum: [sqlite-vec, qdrant, auto]
|
||||
description: Which vector backend to use
|
||||
|
||||
QdrantSettings:
|
||||
type: object
|
||||
description: Qdrant vector database configuration (read shape — no raw apiKey)
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
host:
|
||||
type: string
|
||||
port:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 65535
|
||||
collection:
|
||||
type: string
|
||||
embeddingModel:
|
||||
type: string
|
||||
hasApiKey:
|
||||
type: boolean
|
||||
apiKeyMasked:
|
||||
type: string
|
||||
nullable: true
|
||||
description: First 4 chars of the configured API key, or null
|
||||
|
||||
QdrantHealthResult:
|
||||
type: object
|
||||
description: Result of a Qdrant liveness probe
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
latencyMs:
|
||||
type: number
|
||||
error:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Sanitized error message (no stack traces)
|
||||
|
||||
@@ -2978,84 +2978,91 @@
|
||||
"a2aStep3": "Track and cancel tasks with {code1} and {code2}."
|
||||
},
|
||||
"memory": {
|
||||
"title": "Memory Management",
|
||||
"description": "View and manage stored memory entries",
|
||||
"memories": "Memories",
|
||||
"totalEntries": "Total Entries",
|
||||
"tokensUsed": "Tokens Used",
|
||||
"hitRate": "Hit Rate",
|
||||
"loading": "Loading...",
|
||||
"noMemories": "No memories found",
|
||||
"search": "Search memories...",
|
||||
"allTypes": "All Types",
|
||||
"export": "Export",
|
||||
"import": "Import",
|
||||
"addMemory": "Add Memory",
|
||||
"type": "Type",
|
||||
"key": "Key",
|
||||
"content": "Content",
|
||||
"created": "Created",
|
||||
"actions": "Actions",
|
||||
"delete": "Delete",
|
||||
"factual": "Factual",
|
||||
"episodic": "Episodic",
|
||||
"procedural": "Procedural",
|
||||
"semantic": "Semantic",
|
||||
"a": "A",
|
||||
"pipelineOk": "Pipeline OK ({latencyMs}ms)",
|
||||
"pipelineError": "Pipeline error",
|
||||
"healthUnknown": "Health unknown",
|
||||
"checkingHealth": "Checking…",
|
||||
"checkHealth": "Check health",
|
||||
"pageInfo": "Page {page} of {totalPages} ({total} total)",
|
||||
"previous": "Previous",
|
||||
"next": "Next",
|
||||
"actions": "Actions",
|
||||
"addMemory": "Add Memory",
|
||||
"allTypes": "All Types",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"keyPlaceholder": "e.g. user.preferences.theme",
|
||||
"contentPlaceholder": "Value or JSON content to remember",
|
||||
"tabs": {
|
||||
"memories": "Memories",
|
||||
"playground": "Playground",
|
||||
"engine": "Engine"
|
||||
},
|
||||
"checkHealth": "Check health",
|
||||
"checkingHealth": "Checking…",
|
||||
"compactOld": "Compact old",
|
||||
"concept": {
|
||||
"title": "Conversational Memory",
|
||||
"description": "OmniRoute learns from every conversation, remembering facts, episodes, procedures, and semantic concepts that make responses more accurate and context-aware.",
|
||||
"howWorksToggle": "How it works",
|
||||
"howWorksContent": "1. Automatic extraction: at the end of each response, facts and episodes are detected and saved automatically.\n2. Retrieval: before each response, the most relevant memories are searched via FTS5 (exact), vector (semantic), or hybrid RRF.\n3. Injection: relevant memories are injected into the assistant context to improve response quality.\n4. Management: use this page to view, edit, export, and compact old memories."
|
||||
},
|
||||
"tooltip": {
|
||||
"totalEntries": "Total memories stored for this API key",
|
||||
"tokensUsed": "Estimated total tokens occupied by active memories",
|
||||
"hitRate": "Read-by-ID cache hit rate (not semantic recall accuracy)",
|
||||
"factual": "Objective, permanent facts from user context",
|
||||
"episodic": "Events and experiences from conversation history",
|
||||
"procedural": "Procedures, workflows, and instructions the assistant should follow",
|
||||
"semantic": "Concepts, preferences, and domain knowledge"
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "No memories yet",
|
||||
"description": "Memories are created automatically from conversations. You can also add them manually using the button above."
|
||||
},
|
||||
"content": "Content",
|
||||
"contentPlaceholder": "Value or JSON content to remember",
|
||||
"created": "Created",
|
||||
"delete": "Delete",
|
||||
"deleteConfirmDesc": "This action cannot be undone. The memory will be permanently removed.",
|
||||
"deleteConfirmTitle": "Delete memory?",
|
||||
"description": "View and manage stored memory entries",
|
||||
"editMemory": "Edit memory",
|
||||
"editModal": {
|
||||
"title": "Edit Memory",
|
||||
"metadataLabel": "Metadata (JSON)",
|
||||
"metadataInvalid": "Invalid JSON",
|
||||
"saveFailed": "Failed to save memory"
|
||||
},
|
||||
"editMemory": "Edit memory",
|
||||
"deleteConfirmTitle": "Delete memory?",
|
||||
"deleteConfirmDesc": "This action cannot be undone. The memory will be permanently removed.",
|
||||
"importResult": "{imported} imported, {skipped} skipped",
|
||||
"importError": "Failed to import file",
|
||||
"compactOld": "Compact old",
|
||||
"summarize": {
|
||||
"title": "Compact old memories",
|
||||
"noCandidates": "No memories eligible for compaction (criteria: >30 days old).",
|
||||
"candidatesDesc": "{count} memories will be compacted into summaries. This action cannot be undone.",
|
||||
"confirm": "Compact now"
|
||||
"embedding": {
|
||||
"autoLabel": "Automatic",
|
||||
"autoDesc": "Uses best available: remote provider > static > transformers",
|
||||
"remoteLabel": "Remote provider",
|
||||
"remoteDesc": "Uses embedding via provider API (requires API key)",
|
||||
"staticLabel": "Static local (potion)",
|
||||
"staticDesc": "Local embedding without WASM or external dependencies",
|
||||
"transformersLabel": "Transformers.js (MiniLM)",
|
||||
"transformersDesc": "Local embedding via @huggingface/transformers (~400MB RAM)",
|
||||
"providerModelLabel": "Provider / Model",
|
||||
"noRemoteProviders": "No providers with configured API key",
|
||||
"selectProviderModel": "Select a model",
|
||||
"staticEnabledLabel": "Enable Static Potion",
|
||||
"staticEnabledDesc": "Download and use potion-base-8M model locally",
|
||||
"transformersEnabledLabel": "Enable Transformers.js",
|
||||
"transformersEnabledDesc": "Opt-in for local MiniLM (~400MB RAM, ~3s cold start)",
|
||||
"transformersWarning": "Requires ~400MB RAM and ~3s cold start on the first semantic query."
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "No memories yet",
|
||||
"description": "Memories are created automatically from conversations. You can also add them manually using the button above."
|
||||
},
|
||||
"engine": {
|
||||
"statusTitle": "Engine Status",
|
||||
"embeddingTitle": "Embedding Source",
|
||||
"rerankTitle": "Rerank (optional)",
|
||||
"reindexNow": "Reindex now",
|
||||
"reindexStarted": "Reindexing started ({pending} pending)",
|
||||
"reindexFailed": "Failed to start reindexing",
|
||||
"keywordLabel": "Keyword (FTS5)",
|
||||
"keywordReason": "Keyword search always available",
|
||||
"embeddingLabel": "Embedding",
|
||||
"vectorStoreLabel": "Vector Store",
|
||||
"qdrantLabel": "Qdrant",
|
||||
"rerankLabel": "Rerank",
|
||||
"qdrantDisabled": "Disabled",
|
||||
"qdrantOk": "Healthy ({latencyMs}ms)",
|
||||
"qdrantError": "Connection error",
|
||||
"needsReindex": "{count} memory(ies) need reindexing"
|
||||
},
|
||||
"episodic": "Episodic",
|
||||
"export": "Export",
|
||||
"factual": "Factual",
|
||||
"healthUnknown": "Health unknown",
|
||||
"hitRate": "Hit Rate",
|
||||
"import": "Import",
|
||||
"importError": "Failed to import file",
|
||||
"importResult": "{imported} imported, {skipped} skipped",
|
||||
"key": "Key",
|
||||
"keyPlaceholder": "e.g. user.preferences.theme",
|
||||
"loading": "Loading...",
|
||||
"memories": "Memories",
|
||||
"next": "Next",
|
||||
"noMemories": "No memories found",
|
||||
"pageInfo": "Page {page} of {totalPages} ({total} total)",
|
||||
"pipelineError": "Pipeline error",
|
||||
"pipelineOk": "Pipeline OK ({latencyMs}ms)",
|
||||
"playground": {
|
||||
"infoTitle": "Memory Playground",
|
||||
"infoDesc": "Simulate what would be retrieved for a given query. No memories are modified — read-only preview.",
|
||||
@@ -3079,42 +3086,8 @@
|
||||
"none": "none",
|
||||
"errorFetch": "Failed to fetch preview"
|
||||
},
|
||||
"engine": {
|
||||
"statusTitle": "Engine Status",
|
||||
"embeddingTitle": "Embedding Source",
|
||||
"rerankTitle": "Rerank (optional)",
|
||||
"reindexNow": "Reindex now",
|
||||
"reindexStarted": "Reindexing started ({pending} pending)",
|
||||
"reindexFailed": "Failed to start reindexing",
|
||||
"keywordLabel": "Keyword (FTS5)",
|
||||
"keywordReason": "Keyword search always available",
|
||||
"embeddingLabel": "Embedding",
|
||||
"vectorStoreLabel": "Vector Store",
|
||||
"qdrantLabel": "Qdrant",
|
||||
"rerankLabel": "Rerank",
|
||||
"qdrantDisabled": "Disabled",
|
||||
"qdrantOk": "Healthy ({latencyMs}ms)",
|
||||
"qdrantError": "Connection error",
|
||||
"needsReindex": "{count} memory(ies) need reindexing"
|
||||
},
|
||||
"embedding": {
|
||||
"autoLabel": "Automatic",
|
||||
"autoDesc": "Uses best available: remote provider > static > transformers",
|
||||
"remoteLabel": "Remote provider",
|
||||
"remoteDesc": "Uses embedding via provider API (requires API key)",
|
||||
"staticLabel": "Static local (potion)",
|
||||
"staticDesc": "Local embedding without WASM or external dependencies",
|
||||
"transformersLabel": "Transformers.js (MiniLM)",
|
||||
"transformersDesc": "Local embedding via @huggingface/transformers (~400MB RAM)",
|
||||
"providerModelLabel": "Provider / Model",
|
||||
"noRemoteProviders": "No providers with configured API key",
|
||||
"selectProviderModel": "Select a model",
|
||||
"staticEnabledLabel": "Enable Static Potion",
|
||||
"staticEnabledDesc": "Download and use potion-base-8M model locally",
|
||||
"transformersEnabledLabel": "Enable Transformers.js",
|
||||
"transformersEnabledDesc": "Opt-in for local MiniLM (~400MB RAM, ~3s cold start)",
|
||||
"transformersWarning": "Requires ~400MB RAM and ~3s cold start on the first semantic query."
|
||||
},
|
||||
"previous": "Previous",
|
||||
"procedural": "Procedural",
|
||||
"qdrant": {
|
||||
"title": "Qdrant (Vector Store Tier 2)",
|
||||
"description": "Optional Qdrant integration for scalable semantic search",
|
||||
@@ -3155,7 +3128,34 @@
|
||||
"noProviderWithKey": "No provider with configured API key. Configure a provider to use rerank.",
|
||||
"selectProviderModel": "Select a provider/model"
|
||||
},
|
||||
"saving": "Saving..."
|
||||
"save": "Save",
|
||||
"saving": "Saving...",
|
||||
"search": "Search memories...",
|
||||
"semantic": "Semantic",
|
||||
"summarize": {
|
||||
"title": "Compact old memories",
|
||||
"noCandidates": "No memories eligible for compaction (criteria: >30 days old).",
|
||||
"candidatesDesc": "{count} memories will be compacted into summaries. This action cannot be undone.",
|
||||
"confirm": "Compact now"
|
||||
},
|
||||
"tabs": {
|
||||
"memories": "Memories",
|
||||
"playground": "Playground",
|
||||
"engine": "Engine"
|
||||
},
|
||||
"title": "Memory Management",
|
||||
"tokensUsed": "Tokens Used",
|
||||
"tooltip": {
|
||||
"totalEntries": "Total memories stored for this API key",
|
||||
"tokensUsed": "Estimated total tokens occupied by active memories",
|
||||
"hitRate": "Read-by-ID cache hit rate (not semantic recall accuracy)",
|
||||
"factual": "Objective, permanent facts from user context",
|
||||
"episodic": "Events and experiences from conversation history",
|
||||
"procedural": "Procedures, workflows, and instructions the assistant should follow",
|
||||
"semantic": "Concepts, preferences, and domain knowledge"
|
||||
},
|
||||
"totalEntries": "Total Entries",
|
||||
"type": "Type"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
@@ -7432,4 +7432,4 @@
|
||||
"resetIn": "reset in",
|
||||
"quotaTotal": "total"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2975,84 +2975,91 @@
|
||||
"a2aStep3": "Rastreie e cancele tarefas com {code1} and {code2}."
|
||||
},
|
||||
"memory": {
|
||||
"title": "Gerenciamento de Memória",
|
||||
"description": "Visualize e gerencie entradas de memória armazenadas",
|
||||
"memories": "Memórias",
|
||||
"totalEntries": "Total de Entradas",
|
||||
"tokensUsed": "Tokens Usados",
|
||||
"hitRate": "Taxa de Acerto",
|
||||
"loading": "Carregando...",
|
||||
"noMemories": "Nenhuma memória encontrada",
|
||||
"search": "Buscar memórias...",
|
||||
"allTypes": "Todos os Tipos",
|
||||
"export": "Exportar",
|
||||
"import": "Importar",
|
||||
"addMemory": "Adicionar Memória",
|
||||
"type": "Tipo",
|
||||
"key": "Chave",
|
||||
"content": "Conteúdo",
|
||||
"created": "Criado",
|
||||
"actions": "Ações",
|
||||
"delete": "Delete",
|
||||
"factual": "Factual",
|
||||
"episodic": "Episódica",
|
||||
"procedural": "Procedural",
|
||||
"semantic": "Semântica",
|
||||
"a": "A",
|
||||
"pipelineOk": "Pipeline OK ({latencyMs}ms)",
|
||||
"pipelineError": "Erro de pipeline",
|
||||
"healthUnknown": "Saúde desconhecida",
|
||||
"checkingHealth": "Verificando…",
|
||||
"checkHealth": "Verifique a saúde",
|
||||
"pageInfo": "Página {page} de {totalPages} ({total} total)",
|
||||
"previous": "Anterior",
|
||||
"next": "Próximo",
|
||||
"actions": "Ações",
|
||||
"addMemory": "Adicionar Memória",
|
||||
"allTypes": "Todos os Tipos",
|
||||
"cancel": "Cancelar",
|
||||
"save": "Salvar",
|
||||
"keyPlaceholder": "por exemplo usuário.preferências.tema",
|
||||
"contentPlaceholder": "Valor ou conteúdo JSON para lembrar",
|
||||
"tabs": {
|
||||
"memories": "Memórias",
|
||||
"playground": "Playground",
|
||||
"engine": "Engine"
|
||||
},
|
||||
"checkHealth": "Verifique a saúde",
|
||||
"checkingHealth": "Verificando…",
|
||||
"compactOld": "Compactar antigas",
|
||||
"concept": {
|
||||
"title": "Memória Conversacional",
|
||||
"description": "O OmniRoute aprende com cada conversa, lembrando de fatos, episódios, procedimentos e conceitos semânticos que tornam as respostas mais precisas e contextualizadas.",
|
||||
"howWorksToggle": "Como funciona",
|
||||
"howWorksContent": "1. Extração automática: ao final de cada resposta, fatos e episódios são detectados e salvos automaticamente.\n2. Recuperação: antes de cada resposta, as memórias mais relevantes são buscadas por FTS5 (exato), vetor (semântico) ou RRF híbrido.\n3. Injeção: as memórias relevantes são injetadas no contexto do assistente para melhorar a qualidade da resposta.\n4. Gestão: use esta página para visualizar, editar, exportar e compactar memórias antigas."
|
||||
},
|
||||
"tooltip": {
|
||||
"totalEntries": "Total de memórias armazenadas para esta chave de API",
|
||||
"tokensUsed": "Total de tokens estimados ocupados pelas memórias ativas",
|
||||
"hitRate": "Taxa de cache de leitura por ID (não é precisão semântica)",
|
||||
"factual": "Fatos objetivos e permanentes do contexto do usuário",
|
||||
"episodic": "Eventos e experiências do histórico da conversa",
|
||||
"procedural": "Procedimentos, fluxos e instruções que o assistente deve seguir",
|
||||
"semantic": "Conceitos, preferências e conhecimento de domínio"
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "Nenhuma memória ainda",
|
||||
"description": "As memórias são criadas automaticamente das conversas. Você também pode adicionar manualmente usando o botão acima."
|
||||
},
|
||||
"content": "Conteúdo",
|
||||
"contentPlaceholder": "Valor ou conteúdo JSON para lembrar",
|
||||
"created": "Criado",
|
||||
"delete": "Delete",
|
||||
"deleteConfirmDesc": "Esta ação não pode ser desfeita. A memória será removida permanentemente.",
|
||||
"deleteConfirmTitle": "Excluir memória?",
|
||||
"description": "Visualize e gerencie entradas de memória armazenadas",
|
||||
"editMemory": "Editar memória",
|
||||
"editModal": {
|
||||
"title": "Editar Memória",
|
||||
"metadataLabel": "Metadados (JSON)",
|
||||
"metadataInvalid": "JSON inválido",
|
||||
"saveFailed": "Falha ao salvar memória"
|
||||
},
|
||||
"editMemory": "Editar memória",
|
||||
"deleteConfirmTitle": "Excluir memória?",
|
||||
"deleteConfirmDesc": "Esta ação não pode ser desfeita. A memória será removida permanentemente.",
|
||||
"importResult": "{imported} importadas, {skipped} ignoradas",
|
||||
"importError": "Erro ao importar arquivo",
|
||||
"compactOld": "Compactar antigas",
|
||||
"summarize": {
|
||||
"title": "Compactar memórias antigas",
|
||||
"noCandidates": "Nenhuma memória elegível para compactação (critério: >30 dias).",
|
||||
"candidatesDesc": "{count} memórias serão compactadas em summaries. Esta ação não pode ser desfeita.",
|
||||
"confirm": "Compactar agora"
|
||||
"embedding": {
|
||||
"autoLabel": "Automático",
|
||||
"autoDesc": "Usa o melhor disponível: provider remoto > static > transformers",
|
||||
"remoteLabel": "Provider remoto",
|
||||
"remoteDesc": "Usa embedding via API de um provider configurado (requer chave)",
|
||||
"staticLabel": "Static local (potion)",
|
||||
"staticDesc": "Embedding local sem WASM, sem dependências externas",
|
||||
"transformersLabel": "Transformers.js (MiniLM)",
|
||||
"transformersDesc": "Embedding local via @huggingface/transformers (~400MB RAM)",
|
||||
"providerModelLabel": "Provider / Modelo",
|
||||
"noRemoteProviders": "Nenhum provider com chave configurada",
|
||||
"selectProviderModel": "Selecione um modelo",
|
||||
"staticEnabledLabel": "Habilitar Static Potion",
|
||||
"staticEnabledDesc": "Baixa e usa o modelo potion-base-8M localmente",
|
||||
"transformersEnabledLabel": "Habilitar Transformers.js",
|
||||
"transformersEnabledDesc": "Opt-in para MiniLM local (~400MB RAM, cold start ~3s)",
|
||||
"transformersWarning": "Requer ~400MB de RAM e ~3s de cold start na primeira query semântica."
|
||||
},
|
||||
"emptyState": {
|
||||
"title": "Nenhuma memória ainda",
|
||||
"description": "As memórias são criadas automaticamente das conversas. Você também pode adicionar manualmente usando o botão acima."
|
||||
},
|
||||
"engine": {
|
||||
"statusTitle": "Status do Engine",
|
||||
"embeddingTitle": "Fonte de Embedding",
|
||||
"rerankTitle": "Rerank (opcional)",
|
||||
"reindexNow": "Reindexar agora",
|
||||
"reindexStarted": "Reindexação iniciada ({pending} pendentes)",
|
||||
"reindexFailed": "Falha ao iniciar reindexação",
|
||||
"keywordLabel": "Keyword (FTS5)",
|
||||
"keywordReason": "Busca por palavras-chave sempre disponível",
|
||||
"embeddingLabel": "Embedding",
|
||||
"vectorStoreLabel": "Vector Store",
|
||||
"qdrantLabel": "Qdrant",
|
||||
"rerankLabel": "Rerank",
|
||||
"qdrantDisabled": "Desabilitado",
|
||||
"qdrantOk": "Saudável ({latencyMs}ms)",
|
||||
"qdrantError": "Erro de conexão",
|
||||
"needsReindex": "{count} memória(s) precisam de reindexação"
|
||||
},
|
||||
"episodic": "Episódica",
|
||||
"export": "Exportar",
|
||||
"factual": "Factual",
|
||||
"healthUnknown": "Saúde desconhecida",
|
||||
"hitRate": "Taxa de Acerto",
|
||||
"import": "Importar",
|
||||
"importError": "Erro ao importar arquivo",
|
||||
"importResult": "{imported} importadas, {skipped} ignoradas",
|
||||
"key": "Chave",
|
||||
"keyPlaceholder": "por exemplo usuário.preferências.tema",
|
||||
"loading": "Carregando...",
|
||||
"memories": "Memórias",
|
||||
"next": "Próximo",
|
||||
"noMemories": "Nenhuma memória encontrada",
|
||||
"pageInfo": "Página {page} de {totalPages} ({total} total)",
|
||||
"pipelineError": "Erro de pipeline",
|
||||
"pipelineOk": "Pipeline OK ({latencyMs}ms)",
|
||||
"playground": {
|
||||
"infoTitle": "Memory Playground",
|
||||
"infoDesc": "Simule o que seria recuperado para uma determinada query. Nenhuma memória é modificada — apenas visualização.",
|
||||
@@ -3076,42 +3083,8 @@
|
||||
"none": "nenhum",
|
||||
"errorFetch": "Falha ao buscar preview"
|
||||
},
|
||||
"engine": {
|
||||
"statusTitle": "Status do Engine",
|
||||
"embeddingTitle": "Fonte de Embedding",
|
||||
"rerankTitle": "Rerank (opcional)",
|
||||
"reindexNow": "Reindexar agora",
|
||||
"reindexStarted": "Reindexação iniciada ({pending} pendentes)",
|
||||
"reindexFailed": "Falha ao iniciar reindexação",
|
||||
"keywordLabel": "Keyword (FTS5)",
|
||||
"keywordReason": "Busca por palavras-chave sempre disponível",
|
||||
"embeddingLabel": "Embedding",
|
||||
"vectorStoreLabel": "Vector Store",
|
||||
"qdrantLabel": "Qdrant",
|
||||
"rerankLabel": "Rerank",
|
||||
"qdrantDisabled": "Desabilitado",
|
||||
"qdrantOk": "Saudável ({latencyMs}ms)",
|
||||
"qdrantError": "Erro de conexão",
|
||||
"needsReindex": "{count} memória(s) precisam de reindexação"
|
||||
},
|
||||
"embedding": {
|
||||
"autoLabel": "Automático",
|
||||
"autoDesc": "Usa o melhor disponível: provider remoto > static > transformers",
|
||||
"remoteLabel": "Provider remoto",
|
||||
"remoteDesc": "Usa embedding via API de um provider configurado (requer chave)",
|
||||
"staticLabel": "Static local (potion)",
|
||||
"staticDesc": "Embedding local sem WASM, sem dependências externas",
|
||||
"transformersLabel": "Transformers.js (MiniLM)",
|
||||
"transformersDesc": "Embedding local via @huggingface/transformers (~400MB RAM)",
|
||||
"providerModelLabel": "Provider / Modelo",
|
||||
"noRemoteProviders": "Nenhum provider com chave configurada",
|
||||
"selectProviderModel": "Selecione um modelo",
|
||||
"staticEnabledLabel": "Habilitar Static Potion",
|
||||
"staticEnabledDesc": "Baixa e usa o modelo potion-base-8M localmente",
|
||||
"transformersEnabledLabel": "Habilitar Transformers.js",
|
||||
"transformersEnabledDesc": "Opt-in para MiniLM local (~400MB RAM, cold start ~3s)",
|
||||
"transformersWarning": "Requer ~400MB de RAM e ~3s de cold start na primeira query semântica."
|
||||
},
|
||||
"previous": "Anterior",
|
||||
"procedural": "Procedural",
|
||||
"qdrant": {
|
||||
"title": "Qdrant (Vector Store Tier 2)",
|
||||
"description": "Integração opcional com Qdrant para busca semântica escalável",
|
||||
@@ -3152,7 +3125,34 @@
|
||||
"noProviderWithKey": "Nenhum provider com chave configurada. Configure um provider para usar rerank.",
|
||||
"selectProviderModel": "Selecione um provider/modelo"
|
||||
},
|
||||
"saving": "Salvando..."
|
||||
"save": "Salvar",
|
||||
"saving": "Salvando...",
|
||||
"search": "Buscar memórias...",
|
||||
"semantic": "Semântica",
|
||||
"summarize": {
|
||||
"title": "Compactar memórias antigas",
|
||||
"noCandidates": "Nenhuma memória elegível para compactação (critério: >30 dias).",
|
||||
"candidatesDesc": "{count} memórias serão compactadas em summaries. Esta ação não pode ser desfeita.",
|
||||
"confirm": "Compactar agora"
|
||||
},
|
||||
"tabs": {
|
||||
"memories": "Memórias",
|
||||
"playground": "Playground",
|
||||
"engine": "Engine"
|
||||
},
|
||||
"title": "Gerenciamento de Memória",
|
||||
"tokensUsed": "Tokens Usados",
|
||||
"tooltip": {
|
||||
"totalEntries": "Total de memórias armazenadas para esta chave de API",
|
||||
"tokensUsed": "Total de tokens estimados ocupados pelas memórias ativas",
|
||||
"hitRate": "Taxa de cache de leitura por ID (não é precisão semântica)",
|
||||
"factual": "Fatos objetivos e permanentes do contexto do usuário",
|
||||
"episodic": "Eventos e experiências do histórico da conversa",
|
||||
"procedural": "Procedimentos, fluxos e instruções que o assistente deve seguir",
|
||||
"semantic": "Conceitos, preferências e conhecimento de domínio"
|
||||
},
|
||||
"totalEntries": "Total de Entradas",
|
||||
"type": "Tipo"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills",
|
||||
@@ -7422,4 +7422,4 @@
|
||||
"resetIn": "redefinir em",
|
||||
"quotaTotal": "total"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
607
tests/e2e/memory-engine.spec.ts
Normal file
607
tests/e2e/memory-engine.spec.ts
Normal file
@@ -0,0 +1,607 @@
|
||||
import { expect, test, type Page, type Route } from "@playwright/test";
|
||||
import { gotoDashboardRoute } from "./helpers/dashboardAuth";
|
||||
|
||||
const NAVIGATION_TIMEOUT_MS = 300_000;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type MemoryEntry = {
|
||||
id: string;
|
||||
apiKeyId: string;
|
||||
sessionId: string | null;
|
||||
type: "factual" | "episodic" | "procedural" | "semantic";
|
||||
key: string;
|
||||
content: string;
|
||||
metadata: Record<string, unknown>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
expiresAt: string | null;
|
||||
};
|
||||
|
||||
type MemoryStats = {
|
||||
totalEntries: number;
|
||||
tokensUsed: number;
|
||||
hitRate: number;
|
||||
cacheStats: { hits: number; misses: number };
|
||||
};
|
||||
|
||||
type MemorySettings = {
|
||||
enabled: boolean;
|
||||
maxTokens: number;
|
||||
retentionDays: number;
|
||||
strategy: "recent" | "semantic" | "hybrid";
|
||||
skillsEnabled: boolean;
|
||||
embeddingSource: "remote" | "static" | "transformers" | "auto";
|
||||
embeddingProviderModel: string | null;
|
||||
transformersEnabled: boolean;
|
||||
staticEnabled: boolean;
|
||||
rerankEnabled: boolean;
|
||||
rerankProviderModel: string | null;
|
||||
vectorStore: "sqlite-vec" | "qdrant" | "auto";
|
||||
};
|
||||
|
||||
type EngineStatus = {
|
||||
keyword: { available: true; backend: "FTS5" };
|
||||
embedding: {
|
||||
source: "remote" | "static" | "transformers" | null;
|
||||
model: string | null;
|
||||
dimensions: number | null;
|
||||
available: boolean;
|
||||
reason: string;
|
||||
cacheStats: { hits: number; misses: number; size: number };
|
||||
};
|
||||
vectorStore: {
|
||||
backend: "sqlite-vec" | "qdrant" | "none";
|
||||
available: boolean;
|
||||
rowCount: number;
|
||||
needsReindex: number;
|
||||
reason: string;
|
||||
};
|
||||
qdrant: {
|
||||
enabled: boolean;
|
||||
healthy: boolean | null;
|
||||
latencyMs: number | null;
|
||||
error: string | null;
|
||||
};
|
||||
rerank: {
|
||||
enabled: boolean;
|
||||
provider: string | null;
|
||||
model: string | null;
|
||||
available: boolean;
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixtures helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
function makeMemory(overrides: Partial<MemoryEntry> = {}): MemoryEntry {
|
||||
return {
|
||||
id: "mem-test-1",
|
||||
apiKeyId: "key-1",
|
||||
sessionId: null,
|
||||
type: "factual",
|
||||
key: "test.preference.language",
|
||||
content: "The user prefers English responses.",
|
||||
metadata: {},
|
||||
createdAt: new Date("2026-05-01T10:00:00.000Z").toISOString(),
|
||||
updatedAt: new Date("2026-05-01T10:00:00.000Z").toISOString(),
|
||||
expiresAt: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function defaultSettings(): MemorySettings {
|
||||
return {
|
||||
enabled: true,
|
||||
maxTokens: 2000,
|
||||
retentionDays: 30,
|
||||
strategy: "hybrid",
|
||||
skillsEnabled: false,
|
||||
embeddingSource: "auto",
|
||||
embeddingProviderModel: null,
|
||||
transformersEnabled: false,
|
||||
staticEnabled: false,
|
||||
rerankEnabled: false,
|
||||
rerankProviderModel: null,
|
||||
vectorStore: "auto",
|
||||
};
|
||||
}
|
||||
|
||||
function defaultEngineStatus(): EngineStatus {
|
||||
return {
|
||||
keyword: { available: true, backend: "FTS5" },
|
||||
embedding: {
|
||||
source: null,
|
||||
model: null,
|
||||
dimensions: null,
|
||||
available: false,
|
||||
reason: "No embedding source configured",
|
||||
cacheStats: { hits: 0, misses: 0, size: 0 },
|
||||
},
|
||||
vectorStore: {
|
||||
backend: "none",
|
||||
available: false,
|
||||
rowCount: 0,
|
||||
needsReindex: 0,
|
||||
reason: "sqlite-vec unavailable in this environment",
|
||||
},
|
||||
qdrant: { enabled: false, healthy: null, latencyMs: null, error: null },
|
||||
rerank: {
|
||||
enabled: false,
|
||||
provider: null,
|
||||
model: null,
|
||||
available: false,
|
||||
reason: "Rerank disabled",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Route interceptors
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function setupMemoryRoutes(
|
||||
page: Page,
|
||||
state: {
|
||||
memories: MemoryEntry[];
|
||||
stats: MemoryStats;
|
||||
settings: MemorySettings;
|
||||
engineStatus: EngineStatus;
|
||||
createCalls: number;
|
||||
updateCalls: number;
|
||||
deleteCalls: number;
|
||||
settingsCalls: number;
|
||||
reindexCalls: number;
|
||||
previewCalls: number;
|
||||
},
|
||||
) {
|
||||
// GET/POST /api/memory
|
||||
await page.route(/\/api\/memory(\?.*)?$/, async (route) => {
|
||||
const method = route.request().method();
|
||||
|
||||
if (method === "GET") {
|
||||
await fulfillJson(route, {
|
||||
data: state.memories,
|
||||
total: state.memories.length,
|
||||
totalPages: 1,
|
||||
stats: {
|
||||
total: state.memories.length,
|
||||
tokensUsed: state.stats.tokensUsed,
|
||||
hitRate: state.stats.hitRate,
|
||||
cacheStats: state.stats.cacheStats,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "POST") {
|
||||
state.createCalls += 1;
|
||||
const body = route.request().postDataJSON() as Partial<MemoryEntry>;
|
||||
const newMemory = makeMemory({
|
||||
id: `mem-new-${state.createCalls}`,
|
||||
key: body.key ?? "new.key",
|
||||
content: body.content ?? "New memory content.",
|
||||
type: (body.type as MemoryEntry["type"]) ?? "factual",
|
||||
sessionId: body.sessionId ?? null,
|
||||
apiKeyId: body.apiKeyId ?? "key-1",
|
||||
metadata: body.metadata ?? {},
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
state.memories = [...state.memories, newMemory];
|
||||
state.stats.totalEntries = state.memories.length;
|
||||
await fulfillJson(route, newMemory, 201);
|
||||
return;
|
||||
}
|
||||
|
||||
await fulfillJson(route, { error: { message: "Method not allowed" } }, 405);
|
||||
});
|
||||
|
||||
// GET/PUT /api/memory/[id] (must be after /api/memory$ route)
|
||||
await page.route(/\/api\/memory\/[^/]+$/, async (route) => {
|
||||
const method = route.request().method();
|
||||
const memoryId = route.request().url().split("/").pop()?.split("?")[0] ?? "";
|
||||
|
||||
if (method === "GET") {
|
||||
const mem = state.memories.find((m) => m.id === memoryId);
|
||||
if (!mem) {
|
||||
await fulfillJson(route, { error: { message: "Not found" } }, 404);
|
||||
return;
|
||||
}
|
||||
await fulfillJson(route, mem);
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "PUT") {
|
||||
state.updateCalls += 1;
|
||||
const body = route.request().postDataJSON() as Partial<MemoryEntry>;
|
||||
state.memories = state.memories.map((m) => {
|
||||
if (m.id !== memoryId) return m;
|
||||
return {
|
||||
...m,
|
||||
...body,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
const updated = state.memories.find((m) => m.id === memoryId);
|
||||
await fulfillJson(route, updated ?? { error: { message: "Not found" } }, updated ? 200 : 404);
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === "DELETE") {
|
||||
state.deleteCalls += 1;
|
||||
state.memories = state.memories.filter((m) => m.id !== memoryId);
|
||||
state.stats.totalEntries = state.memories.length;
|
||||
await fulfillJson(route, { success: true });
|
||||
return;
|
||||
}
|
||||
|
||||
await fulfillJson(route, { error: { message: "Method not allowed" } }, 405);
|
||||
});
|
||||
|
||||
// GET /api/memory/engine-status
|
||||
await page.route(/\/api\/memory\/engine-status$/, async (route) => {
|
||||
await fulfillJson(route, state.engineStatus);
|
||||
});
|
||||
|
||||
// GET /api/memory/embedding-providers
|
||||
await page.route(/\/api\/memory\/embedding-providers$/, async (route) => {
|
||||
await fulfillJson(route, { providers: [] });
|
||||
});
|
||||
|
||||
// POST /api/memory/retrieve-preview
|
||||
await page.route(/\/api\/memory\/retrieve-preview$/, async (route) => {
|
||||
state.previewCalls += 1;
|
||||
await fulfillJson(route, {
|
||||
memories: state.memories.slice(0, 3).map((m) => ({
|
||||
id: m.id,
|
||||
type: m.type,
|
||||
key: m.key,
|
||||
content: m.content,
|
||||
score: 0.9,
|
||||
tokens: 24,
|
||||
tier: "fts5",
|
||||
vecScore: null,
|
||||
ftsScore: 0.9,
|
||||
})),
|
||||
resolution: {
|
||||
embeddingSource: null,
|
||||
embeddingModel: null,
|
||||
vectorStore: "none",
|
||||
strategyUsed: "exact",
|
||||
rerankApplied: false,
|
||||
fallbackReason: "No embedding source available, fell back to FTS5.",
|
||||
},
|
||||
totalTokensUsed: state.memories.length * 24,
|
||||
budgetMaxTokens: 2000,
|
||||
});
|
||||
});
|
||||
|
||||
// POST /api/memory/reindex
|
||||
await page.route(/\/api\/memory\/reindex$/, async (route) => {
|
||||
state.reindexCalls += 1;
|
||||
await fulfillJson(route, { started: true, pending: 0, queued: 0 });
|
||||
});
|
||||
|
||||
// GET/PUT /api/settings/memory
|
||||
await page.route(/\/api\/settings\/memory$/, async (route) => {
|
||||
const method = route.request().method();
|
||||
if (method === "GET") {
|
||||
await fulfillJson(route, state.settings);
|
||||
return;
|
||||
}
|
||||
if (method === "PUT") {
|
||||
state.settingsCalls += 1;
|
||||
const body = route.request().postDataJSON() as Partial<MemorySettings>;
|
||||
state.settings = { ...state.settings, ...body };
|
||||
await fulfillJson(route, state.settings);
|
||||
return;
|
||||
}
|
||||
await fulfillJson(route, { error: { message: "Method not allowed" } }, 405);
|
||||
});
|
||||
|
||||
// GET/PUT /api/settings/qdrant (Engine tab, QdrantConfigCard)
|
||||
await page.route(/\/api\/settings\/qdrant$/, async (route) => {
|
||||
const method = route.request().method();
|
||||
if (method === "GET") {
|
||||
await fulfillJson(route, {
|
||||
enabled: false,
|
||||
host: "",
|
||||
port: 6333,
|
||||
collection: "omniroute_memory",
|
||||
embeddingModel: "openai/text-embedding-3-small",
|
||||
hasApiKey: false,
|
||||
apiKeyMasked: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (method === "PUT") {
|
||||
await fulfillJson(route, { enabled: false, host: "", port: 6333 });
|
||||
return;
|
||||
}
|
||||
await fulfillJson(route, { error: { message: "Method not allowed" } }, 405);
|
||||
});
|
||||
|
||||
// GET /api/settings/qdrant/health
|
||||
await page.route(/\/api\/settings\/qdrant\/health$/, async (route) => {
|
||||
await fulfillJson(route, { ok: false, latencyMs: 0, error: "Connection refused" });
|
||||
});
|
||||
|
||||
// GET /api/settings/qdrant/embedding-models
|
||||
await page.route(/\/api\/settings\/qdrant\/embedding-models$/, async (route) => {
|
||||
await fulfillJson(route, { models: [] });
|
||||
});
|
||||
|
||||
// GET /api/memory/health
|
||||
await page.route(/\/api\/memory\/health$/, async (route) => {
|
||||
await fulfillJson(route, { working: true, latencyMs: 5 });
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test suite
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe("Memory Engine Studio — /dashboard/memory", () => {
|
||||
test.setTimeout(600_000);
|
||||
|
||||
test("1. /dashboard/memory renders 3 tabs and concept card", async ({ page }) => {
|
||||
const state = {
|
||||
memories: [makeMemory()],
|
||||
stats: { totalEntries: 1, tokensUsed: 24, hitRate: 0.75, cacheStats: { hits: 3, misses: 1 } },
|
||||
settings: defaultSettings(),
|
||||
engineStatus: defaultEngineStatus(),
|
||||
createCalls: 0,
|
||||
updateCalls: 0,
|
||||
deleteCalls: 0,
|
||||
settingsCalls: 0,
|
||||
reindexCalls: 0,
|
||||
previewCalls: 0,
|
||||
};
|
||||
|
||||
await setupMemoryRoutes(page, state);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/memory", { timeoutMs: NAVIGATION_TIMEOUT_MS });
|
||||
|
||||
// All 3 tabs should be visible
|
||||
await expect(page.getByTestId("tab-memories")).toBeVisible({ timeout: 30_000 });
|
||||
await expect(page.getByTestId("tab-playground")).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByTestId("tab-engine")).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test("2. Memories tab renders table and Total card", async ({ page }) => {
|
||||
const state = {
|
||||
memories: [makeMemory()],
|
||||
stats: { totalEntries: 1, tokensUsed: 48, hitRate: 0.5, cacheStats: { hits: 1, misses: 1 } },
|
||||
settings: defaultSettings(),
|
||||
engineStatus: defaultEngineStatus(),
|
||||
createCalls: 0,
|
||||
updateCalls: 0,
|
||||
deleteCalls: 0,
|
||||
settingsCalls: 0,
|
||||
reindexCalls: 0,
|
||||
previewCalls: 0,
|
||||
};
|
||||
|
||||
await setupMemoryRoutes(page, state);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/memory", { timeoutMs: NAVIGATION_TIMEOUT_MS });
|
||||
|
||||
// Wait for memories tab (active by default)
|
||||
await expect(page.getByTestId("tab-memories")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// The memory key should appear in the table
|
||||
await expect(async () => {
|
||||
await expect(page.getByText("test.preference.language")).toBeVisible({ timeout: 10_000 });
|
||||
}).toPass({ timeout: 30_000, intervals: [1000, 2000] });
|
||||
});
|
||||
|
||||
test("3. Add Memory modal → entry appears in table", async ({ page }) => {
|
||||
const state = {
|
||||
memories: [] as MemoryEntry[],
|
||||
stats: { totalEntries: 0, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } },
|
||||
settings: defaultSettings(),
|
||||
engineStatus: defaultEngineStatus(),
|
||||
createCalls: 0,
|
||||
updateCalls: 0,
|
||||
deleteCalls: 0,
|
||||
settingsCalls: 0,
|
||||
reindexCalls: 0,
|
||||
previewCalls: 0,
|
||||
};
|
||||
|
||||
await setupMemoryRoutes(page, state);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/memory", { timeoutMs: NAVIGATION_TIMEOUT_MS });
|
||||
|
||||
// Wait for the page to be ready (empty state is shown initially)
|
||||
await expect(page.getByTestId("tab-memories")).toBeVisible({ timeout: 30_000 });
|
||||
|
||||
// Click Add Memory
|
||||
await expect(async () => {
|
||||
const addBtn = page.getByRole("button", { name: /add memory/i }).first();
|
||||
await expect(addBtn).toBeVisible({ timeout: 10_000 });
|
||||
await addBtn.click();
|
||||
}).toPass({ timeout: 30_000, intervals: [1000, 2000] });
|
||||
|
||||
// Fill in the add-memory form fields
|
||||
const keyInput = page.getByPlaceholder(/e\.g.*preferences/i).or(page.getByLabel(/key/i)).first();
|
||||
await expect(keyInput).toBeVisible({ timeout: 10_000 });
|
||||
await keyInput.fill("test.new.memory");
|
||||
|
||||
const contentInput = page
|
||||
.getByPlaceholder(/content|value/i)
|
||||
.or(page.getByLabel(/content/i))
|
||||
.first();
|
||||
await expect(contentInput).toBeVisible({ timeout: 5_000 });
|
||||
await contentInput.fill("New memory added via modal.");
|
||||
|
||||
// Submit the form
|
||||
const saveBtn = page.getByRole("button", { name: /save|add|create/i }).last();
|
||||
await expect(saveBtn).toBeVisible({ timeout: 5_000 });
|
||||
await saveBtn.click();
|
||||
|
||||
// createCalls should increment
|
||||
await expect.poll(() => state.createCalls).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("4. Edit memory — pencil → modal → save → change reflected", async ({ page }) => {
|
||||
const mem = makeMemory({ id: "mem-edit-1", key: "edit.test.key", content: "Original content." });
|
||||
const state = {
|
||||
memories: [mem],
|
||||
stats: { totalEntries: 1, tokensUsed: 24, hitRate: 0.8, cacheStats: { hits: 4, misses: 1 } },
|
||||
settings: defaultSettings(),
|
||||
engineStatus: defaultEngineStatus(),
|
||||
createCalls: 0,
|
||||
updateCalls: 0,
|
||||
deleteCalls: 0,
|
||||
settingsCalls: 0,
|
||||
reindexCalls: 0,
|
||||
previewCalls: 0,
|
||||
};
|
||||
|
||||
await setupMemoryRoutes(page, state);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/memory", { timeoutMs: NAVIGATION_TIMEOUT_MS });
|
||||
|
||||
// Wait for the memory to appear
|
||||
await expect(async () => {
|
||||
await expect(page.getByText("edit.test.key")).toBeVisible({ timeout: 10_000 });
|
||||
}).toPass({ timeout: 30_000, intervals: [1000, 2000] });
|
||||
|
||||
// Click the edit (pencil) button for our memory
|
||||
const editBtn = page.getByTestId(`edit-memory-${mem.id}`);
|
||||
await expect(editBtn).toBeVisible({ timeout: 10_000 });
|
||||
await editBtn.click();
|
||||
|
||||
// The edit modal should appear
|
||||
const contentInput = page.getByLabel(/content/i).or(page.locator("textarea")).first();
|
||||
await expect(contentInput).toBeVisible({ timeout: 10_000 });
|
||||
await contentInput.fill("Updated content via modal.");
|
||||
|
||||
// Save the changes
|
||||
const saveBtn = page.getByRole("button", { name: /save/i }).last();
|
||||
await expect(saveBtn).toBeVisible({ timeout: 5_000 });
|
||||
await saveBtn.click();
|
||||
|
||||
// updateCalls should increment
|
||||
await expect.poll(() => state.updateCalls).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("5. Playground tab — query and Simulate renders results", async ({ page }) => {
|
||||
const state = {
|
||||
memories: [makeMemory(), makeMemory({ id: "mem-2", key: "test.key.2", content: "Second fact." })],
|
||||
stats: { totalEntries: 2, tokensUsed: 48, hitRate: 0.6, cacheStats: { hits: 3, misses: 2 } },
|
||||
settings: defaultSettings(),
|
||||
engineStatus: defaultEngineStatus(),
|
||||
createCalls: 0,
|
||||
updateCalls: 0,
|
||||
deleteCalls: 0,
|
||||
settingsCalls: 0,
|
||||
reindexCalls: 0,
|
||||
previewCalls: 0,
|
||||
};
|
||||
|
||||
await setupMemoryRoutes(page, state);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/memory", { timeoutMs: NAVIGATION_TIMEOUT_MS });
|
||||
|
||||
// Navigate to Playground tab
|
||||
await expect(page.getByTestId("tab-playground")).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId("tab-playground").click();
|
||||
|
||||
// Fill in query
|
||||
const queryInput = page.getByTestId("playground-query-input");
|
||||
await expect(queryInput).toBeVisible({ timeout: 10_000 });
|
||||
await queryInput.fill("test");
|
||||
|
||||
// Click Simulate
|
||||
const submitBtn = page.getByTestId("playground-submit");
|
||||
await expect(submitBtn).toBeVisible({ timeout: 5_000 });
|
||||
await expect(submitBtn).toBeEnabled({ timeout: 5_000 });
|
||||
await submitBtn.click();
|
||||
|
||||
// Wait for previewCalls to increment
|
||||
await expect.poll(() => state.previewCalls).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// Results section should appear (result count heading)
|
||||
await expect(
|
||||
page.getByText(/result\(s\)|resultado\(s\)/, { exact: false }),
|
||||
).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test("6. Engine tab — status chips render and toggle transformers", async ({ page }) => {
|
||||
const state = {
|
||||
memories: [],
|
||||
stats: { totalEntries: 0, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } },
|
||||
settings: defaultSettings(),
|
||||
engineStatus: defaultEngineStatus(),
|
||||
createCalls: 0,
|
||||
updateCalls: 0,
|
||||
deleteCalls: 0,
|
||||
settingsCalls: 0,
|
||||
reindexCalls: 0,
|
||||
previewCalls: 0,
|
||||
};
|
||||
|
||||
await setupMemoryRoutes(page, state);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/memory", { timeoutMs: NAVIGATION_TIMEOUT_MS });
|
||||
|
||||
// Navigate to Engine tab
|
||||
await expect(page.getByTestId("tab-engine")).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId("tab-engine").click();
|
||||
|
||||
// Status section should be visible (Reindex Now button is a proxy for the engine panel)
|
||||
const reindexBtn = page.getByTestId("reindex-now-button");
|
||||
await expect(reindexBtn).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
// Engine status heading
|
||||
await expect(
|
||||
page.getByText(/engine status|status do engine/i, { exact: false }),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
});
|
||||
|
||||
test("7. Reindex Now button triggers POST /api/memory/reindex", async ({ page }) => {
|
||||
const state = {
|
||||
memories: [],
|
||||
stats: { totalEntries: 0, tokensUsed: 0, hitRate: 0, cacheStats: { hits: 0, misses: 0 } },
|
||||
settings: defaultSettings(),
|
||||
engineStatus: { ...defaultEngineStatus(), vectorStore: { ...defaultEngineStatus().vectorStore, needsReindex: 5 } },
|
||||
createCalls: 0,
|
||||
updateCalls: 0,
|
||||
deleteCalls: 0,
|
||||
settingsCalls: 0,
|
||||
reindexCalls: 0,
|
||||
previewCalls: 0,
|
||||
};
|
||||
|
||||
await setupMemoryRoutes(page, state);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/memory", { timeoutMs: NAVIGATION_TIMEOUT_MS });
|
||||
|
||||
// Navigate to Engine tab
|
||||
await expect(page.getByTestId("tab-engine")).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId("tab-engine").click();
|
||||
|
||||
// Click Reindex Now
|
||||
const reindexBtn = page.getByTestId("reindex-now-button");
|
||||
await expect(reindexBtn).toBeVisible({ timeout: 20_000 });
|
||||
await reindexBtn.click();
|
||||
|
||||
// reindexCalls should increment (request was made)
|
||||
await expect.poll(() => state.reindexCalls).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
358
tests/e2e/memory-qdrant-routes.spec.ts
Normal file
358
tests/e2e/memory-qdrant-routes.spec.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import { expect, test, type Page, type Route } from "@playwright/test";
|
||||
import { gotoDashboardRoute } from "./helpers/dashboardAuth";
|
||||
|
||||
const NAVIGATION_TIMEOUT_MS = 300_000;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function fulfillJson(route: Route, body: unknown, status = 200) {
|
||||
await route.fulfill({
|
||||
status,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
type QdrantSettings = {
|
||||
enabled: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
collection: string;
|
||||
embeddingModel: string;
|
||||
hasApiKey: boolean;
|
||||
apiKeyMasked: string | null;
|
||||
};
|
||||
|
||||
type MemorySettings = {
|
||||
enabled: boolean;
|
||||
maxTokens: number;
|
||||
retentionDays: number;
|
||||
strategy: "recent" | "semantic" | "hybrid";
|
||||
skillsEnabled: boolean;
|
||||
embeddingSource: "remote" | "static" | "transformers" | "auto";
|
||||
embeddingProviderModel: string | null;
|
||||
transformersEnabled: boolean;
|
||||
staticEnabled: boolean;
|
||||
rerankEnabled: boolean;
|
||||
rerankProviderModel: string | null;
|
||||
vectorStore: "sqlite-vec" | "qdrant" | "auto";
|
||||
};
|
||||
|
||||
function defaultQdrantSettings(): QdrantSettings {
|
||||
return {
|
||||
enabled: false,
|
||||
host: "",
|
||||
port: 6333,
|
||||
collection: "omniroute_memory",
|
||||
embeddingModel: "openai/text-embedding-3-small",
|
||||
hasApiKey: false,
|
||||
apiKeyMasked: null,
|
||||
};
|
||||
}
|
||||
|
||||
function defaultMemorySettings(): MemorySettings {
|
||||
return {
|
||||
enabled: true,
|
||||
maxTokens: 2000,
|
||||
retentionDays: 30,
|
||||
strategy: "hybrid",
|
||||
skillsEnabled: false,
|
||||
embeddingSource: "auto",
|
||||
embeddingProviderModel: null,
|
||||
transformersEnabled: false,
|
||||
staticEnabled: false,
|
||||
rerankEnabled: false,
|
||||
rerankProviderModel: null,
|
||||
vectorStore: "auto",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up all route mocks needed for the Engine tab + QdrantConfigCard.
|
||||
*
|
||||
* Key security assertion: health/search/cleanup endpoints return error payloads
|
||||
* WITHOUT a stack trace (no "at /…" lines) — validates Hard Rule #12 compliance.
|
||||
*/
|
||||
async function setupQdrantRoutes(
|
||||
page: Page,
|
||||
state: {
|
||||
qdrantSettings: QdrantSettings;
|
||||
memorySettings: MemorySettings;
|
||||
healthCalls: number;
|
||||
settingsPutCalls: number;
|
||||
searchCalls: number;
|
||||
cleanupCalls: number;
|
||||
},
|
||||
) {
|
||||
// /api/memory (GET) — empty list, needed by MemoriesTab which is the default
|
||||
await page.route(/\/api\/memory(\?.*)?$/, async (route) => {
|
||||
if (route.request().method() === "GET") {
|
||||
await fulfillJson(route, {
|
||||
data: [],
|
||||
total: 0,
|
||||
totalPages: 1,
|
||||
stats: { total: 0, tokensUsed: 0, hitRate: 0 },
|
||||
});
|
||||
return;
|
||||
}
|
||||
await fulfillJson(route, { error: { message: "Not allowed" } }, 405);
|
||||
});
|
||||
|
||||
// /api/memory/engine-status
|
||||
await page.route(/\/api\/memory\/engine-status$/, async (route) => {
|
||||
await fulfillJson(route, {
|
||||
keyword: { available: true, backend: "FTS5" },
|
||||
embedding: {
|
||||
source: null,
|
||||
model: null,
|
||||
dimensions: null,
|
||||
available: false,
|
||||
reason: "No embedding source",
|
||||
cacheStats: { hits: 0, misses: 0, size: 0 },
|
||||
},
|
||||
vectorStore: {
|
||||
backend: "none",
|
||||
available: false,
|
||||
rowCount: 0,
|
||||
needsReindex: 0,
|
||||
reason: "sqlite-vec unavailable",
|
||||
},
|
||||
qdrant: { enabled: false, healthy: null, latencyMs: null, error: null },
|
||||
rerank: {
|
||||
enabled: false,
|
||||
provider: null,
|
||||
model: null,
|
||||
available: false,
|
||||
reason: "Rerank disabled",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// /api/memory/embedding-providers
|
||||
await page.route(/\/api\/memory\/embedding-providers$/, async (route) => {
|
||||
await fulfillJson(route, { providers: [] });
|
||||
});
|
||||
|
||||
// /api/memory/health
|
||||
await page.route(/\/api\/memory\/health$/, async (route) => {
|
||||
await fulfillJson(route, { working: true, latencyMs: 5 });
|
||||
});
|
||||
|
||||
// /api/memory/reindex
|
||||
await page.route(/\/api\/memory\/reindex$/, async (route) => {
|
||||
await fulfillJson(route, { started: true, pending: 0 });
|
||||
});
|
||||
|
||||
// GET/PUT /api/settings/memory
|
||||
await page.route(/\/api\/settings\/memory$/, async (route) => {
|
||||
const method = route.request().method();
|
||||
if (method === "GET") {
|
||||
await fulfillJson(route, state.memorySettings);
|
||||
return;
|
||||
}
|
||||
if (method === "PUT") {
|
||||
const body = route.request().postDataJSON() as Partial<MemorySettings>;
|
||||
state.memorySettings = { ...state.memorySettings, ...body };
|
||||
await fulfillJson(route, state.memorySettings);
|
||||
return;
|
||||
}
|
||||
await fulfillJson(route, { error: { message: "Method not allowed" } }, 405);
|
||||
});
|
||||
|
||||
// GET/PUT /api/settings/qdrant
|
||||
await page.route(/\/api\/settings\/qdrant$/, async (route) => {
|
||||
const method = route.request().method();
|
||||
if (method === "GET") {
|
||||
await fulfillJson(route, state.qdrantSettings);
|
||||
return;
|
||||
}
|
||||
if (method === "PUT") {
|
||||
state.settingsPutCalls += 1;
|
||||
const body = route.request().postDataJSON() as Partial<QdrantSettings>;
|
||||
state.qdrantSettings = {
|
||||
...state.qdrantSettings,
|
||||
...body,
|
||||
// PUT returns the sanitized version (no raw apiKey field)
|
||||
};
|
||||
await fulfillJson(route, {
|
||||
...state.qdrantSettings,
|
||||
hasApiKey: false,
|
||||
apiKeyMasked: null,
|
||||
});
|
||||
return;
|
||||
}
|
||||
await fulfillJson(route, { error: { message: "Method not allowed" } }, 405);
|
||||
});
|
||||
|
||||
// GET /api/settings/qdrant/health — simulates a refused connection.
|
||||
// The error message MUST NOT contain a stack trace (Hard Rule #12).
|
||||
await page.route(/\/api\/settings\/qdrant\/health$/, async (route) => {
|
||||
state.healthCalls += 1;
|
||||
// Return a structured error that is safe (no stack trace)
|
||||
await fulfillJson(route, {
|
||||
ok: false,
|
||||
latencyMs: 0,
|
||||
error: "connect ECONNREFUSED 127.0.0.1:6333",
|
||||
});
|
||||
});
|
||||
|
||||
// GET /api/settings/qdrant/embedding-models
|
||||
await page.route(/\/api\/settings\/qdrant\/embedding-models$/, async (route) => {
|
||||
await fulfillJson(route, { models: ["openai/text-embedding-3-small"] });
|
||||
});
|
||||
|
||||
// POST /api/settings/qdrant/search — simulates failed search (Qdrant not running)
|
||||
await page.route(/\/api\/settings\/qdrant\/search$/, async (route) => {
|
||||
state.searchCalls += 1;
|
||||
await fulfillJson(
|
||||
route,
|
||||
{
|
||||
error: {
|
||||
message: "Qdrant connection failed: ECONNREFUSED",
|
||||
code: "QDRANT_UNAVAILABLE",
|
||||
},
|
||||
},
|
||||
503,
|
||||
);
|
||||
});
|
||||
|
||||
// POST /api/settings/qdrant/cleanup — simulates failed cleanup (Qdrant not running)
|
||||
await page.route(/\/api\/settings\/qdrant\/cleanup$/, async (route) => {
|
||||
state.cleanupCalls += 1;
|
||||
await fulfillJson(
|
||||
route,
|
||||
{
|
||||
error: {
|
||||
message: "Qdrant cleanup failed: ECONNREFUSED",
|
||||
code: "QDRANT_UNAVAILABLE",
|
||||
},
|
||||
},
|
||||
503,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test.describe("Memory Qdrant routes — Engine tab integration", () => {
|
||||
test.setTimeout(600_000);
|
||||
|
||||
test("Engine tab renders Qdrant config card", async ({ page }) => {
|
||||
const state = {
|
||||
qdrantSettings: defaultQdrantSettings(),
|
||||
memorySettings: defaultMemorySettings(),
|
||||
healthCalls: 0,
|
||||
settingsPutCalls: 0,
|
||||
searchCalls: 0,
|
||||
cleanupCalls: 0,
|
||||
};
|
||||
|
||||
await setupQdrantRoutes(page, state);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/memory", { timeoutMs: NAVIGATION_TIMEOUT_MS });
|
||||
|
||||
// Navigate to Engine tab
|
||||
await expect(page.getByTestId("tab-engine")).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId("tab-engine").click();
|
||||
|
||||
// Qdrant section should be visible
|
||||
await expect(
|
||||
page.getByText(/qdrant/i, { exact: false }),
|
||||
).toBeVisible({ timeout: 20_000 });
|
||||
|
||||
// Qdrant enabled switch should be visible
|
||||
const qdrantSwitch = page.getByTestId("qdrant-enabled-switch");
|
||||
await expect(qdrantSwitch).toBeVisible({ timeout: 15_000 });
|
||||
});
|
||||
|
||||
test("Test Connection button triggers GET /api/settings/qdrant/health with sanitized error", async ({
|
||||
page,
|
||||
}) => {
|
||||
const state = {
|
||||
qdrantSettings: { ...defaultQdrantSettings(), host: "localhost" },
|
||||
memorySettings: defaultMemorySettings(),
|
||||
healthCalls: 0,
|
||||
settingsPutCalls: 0,
|
||||
searchCalls: 0,
|
||||
cleanupCalls: 0,
|
||||
};
|
||||
|
||||
await setupQdrantRoutes(page, state);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/memory", { timeoutMs: NAVIGATION_TIMEOUT_MS });
|
||||
|
||||
// Navigate to Engine tab
|
||||
await expect(page.getByTestId("tab-engine")).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId("tab-engine").click();
|
||||
|
||||
// Find and click the Test Connection button
|
||||
const testConnBtn = page.getByTestId("qdrant-test-connection");
|
||||
await expect(testConnBtn).toBeVisible({ timeout: 20_000 });
|
||||
await testConnBtn.click();
|
||||
|
||||
// healthCalls should increment
|
||||
await expect.poll(() => state.healthCalls).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// The error should surface in the UI — but without a stack trace
|
||||
// "ECONNREFUSED" is acceptable; "at /" (stack trace marker) is not
|
||||
await expect(async () => {
|
||||
const bodyText = await page.locator("body").innerText();
|
||||
// Error is shown (connection refused, not just silent failure)
|
||||
expect(
|
||||
bodyText.toLowerCase().includes("error") ||
|
||||
bodyText.toLowerCase().includes("refused") ||
|
||||
bodyText.toLowerCase().includes("failed") ||
|
||||
bodyText.toLowerCase().includes("erro"),
|
||||
).toBe(true);
|
||||
// Must NOT contain a stack trace
|
||||
expect(bodyText).not.toMatch(/\sat\s\//);
|
||||
}).toPass({ timeout: 15_000, intervals: [1000, 2000] });
|
||||
});
|
||||
|
||||
test("Cleanup button triggers POST /api/settings/qdrant/cleanup with sanitized error", async ({
|
||||
page,
|
||||
}) => {
|
||||
const state = {
|
||||
qdrantSettings: { ...defaultQdrantSettings(), host: "localhost" },
|
||||
memorySettings: defaultMemorySettings(),
|
||||
healthCalls: 0,
|
||||
settingsPutCalls: 0,
|
||||
searchCalls: 0,
|
||||
cleanupCalls: 0,
|
||||
};
|
||||
|
||||
await setupQdrantRoutes(page, state);
|
||||
|
||||
await gotoDashboardRoute(page, "/dashboard/memory", { timeoutMs: NAVIGATION_TIMEOUT_MS });
|
||||
|
||||
// Navigate to Engine tab
|
||||
await expect(page.getByTestId("tab-engine")).toBeVisible({ timeout: 30_000 });
|
||||
await page.getByTestId("tab-engine").click();
|
||||
|
||||
// Find and click the Cleanup button
|
||||
const cleanupBtn = page.getByTestId("qdrant-cleanup");
|
||||
await expect(cleanupBtn).toBeVisible({ timeout: 20_000 });
|
||||
await cleanupBtn.click();
|
||||
|
||||
// cleanupCalls should increment
|
||||
await expect.poll(() => state.cleanupCalls).toBeGreaterThanOrEqual(1);
|
||||
|
||||
// The error should surface in the UI but without a stack trace
|
||||
await expect(async () => {
|
||||
const bodyText = await page.locator("body").innerText();
|
||||
// Error surfaces
|
||||
expect(
|
||||
bodyText.toLowerCase().includes("error") ||
|
||||
bodyText.toLowerCase().includes("failed") ||
|
||||
bodyText.toLowerCase().includes("falh") ||
|
||||
bodyText.toLowerCase().includes("cleanup"),
|
||||
).toBe(true);
|
||||
// Must NOT contain a stack trace
|
||||
expect(bodyText).not.toMatch(/\sat\s\//);
|
||||
}).toPass({ timeout: 15_000, intervals: [1000, 2000] });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user