Merge pull request #29 from salestech-group/fix/18-graphiti-loud-embedding-failures

fix(graphiti): surface embedding failures and document ollama embedder
This commit is contained in:
Dominik Seemann 2026-05-08 11:08:22 +02:00 committed by GitHub
commit 729a51ea82
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 796 additions and 8 deletions

View File

@ -18,6 +18,14 @@ LLM_MODEL_NAME=qwen-plus
# EMBEDDING_BASE_URL=
EMBEDDING_MODEL=text-embedding-3-small
# Local embeddings via Ollama (run: ollama pull mxbai-embed-large).
# mxbai-embed-large is 1024-dim, matching Graphiti's default EMBEDDING_DIM.
# 768-dim models (e.g. nomic-embed-text) are NOT supported until EMBEDDING_DIM
# becomes configurable. Use host.docker.internal in Docker, localhost in host mode.
# EMBEDDING_BASE_URL=http://host.docker.internal:11434/v1
# EMBEDDING_API_KEY=ollama
# EMBEDDING_MODEL=mxbai-embed-large
# Knowledge graph — Neo4j (default works for both Docker and host modes).
# Docker compose overrides NEO4J_URI to bolt://neo4j:7687 inside the stack.
NEO4J_URI=bolt://localhost:7687

View File

@ -0,0 +1,296 @@
# Design Document — graphiti-ollama-embedder
## Overview
**Purpose**: Add first-class documentation for using a local Ollama embedder (`mxbai-embed-large`) with the Graphiti adapter, and remove the silent placeholder-UUID fallback in `_GraphNamespace.add_batch` so embedding failures terminate the surrounding graph-build `Task` with the underlying error visible.
**Users**: Self-hosting MiroFish operators who run the LLM/embedder stack locally on Ollama, and any operator hitting a misconfigured embedder (which currently produces an empty graph that *looks* successfully built).
**Impact**: The graph-build pipeline becomes correctly observable: invalid `EMBEDDING_*` configuration produces a `Task.status = FAILED` with the underlying error, instead of `COMPLETED` with no nodes. The change is invisible on the OpenAI/Gemini happy path.
### Goals
- R1 — `.env.example`, `CLAUDE.md`, `README.md`, `docker-compose.yml` document Ollama as a supported embedder configuration with `mxbai-embed-large` and a `curl` smoke test.
- R2 — embedding failures in `_GraphNamespace.add_batch` propagate to the calling background task, which terminates with `status=FAILED` and a non-empty `error`. ERROR-level logging instead of `WARNING`.
- R3 — OpenAI- and Gemini-based deployments are unchanged; no new env var; the 1024-dim constraint is documented.
### Non-Goals
- Adding a startup-time embedder health probe.
- Making `EMBEDDING_DIM` env-configurable to support 768-dim models.
- Adding an `ollama` provider literal in `_build_llm_and_embedder` (Ollama uses the existing `openai` branch with a different `EMBEDDING_BASE_URL`).
- Generic retry/backoff for transient embedder errors. Tracked as an explicit follow-up.
## Boundary Commitments
### This Spec Owns
- The documentation surface for Ollama embedder configuration in `.env.example`, `CLAUDE.md`, `README.md`, and `docker-compose.yml` comments.
- The error-propagation contract of `_GraphNamespace.add_batch` in `backend/app/services/graphiti_adapter.py`.
- Adapter-level ERROR-log emission for failed `add_episode` calls.
### Out of Boundary
- Behavior of `_GraphNamespace.add(...)` (single-episode path; already correct).
- Behavior of `_GraphNamespace.search(...)` (still allowed to log-and-return-empty per steering).
- The `_build_graph_worker` outer `try/except` and `fail_task` plumbing — already implements the contract this spec depends on.
- Any change to `_build_llm_and_embedder` (no provider literal added; existing `openai` branch is sufficient).
- Generic retry policy.
### Allowed Dependencies
- `app.utils.logger.get_logger(...)` for ERROR-level emission.
- The existing `_run` helper that drives async Graphiti calls on the persistent loop.
- The existing `Task` lifecycle methods (`fail_task`) called from `_build_graph_worker` — relied on, not modified.
- `graphiti_core.embedder.openai.OpenAIEmbedder` configured with arbitrary `base_url`.
### Revalidation Triggers
- Any future provider literal added to `_build_llm_and_embedder` (would change which env vars feed which embedder).
- Any change to the contract that `_GraphNamespace.add_batch` returns one `_EpisodeResult` per input episode in input order.
- Any change to how `_build_graph_worker` translates exceptions into `Task` failures (would invalidate the assumption that propagating from the adapter is sufficient).
## Architecture
### Existing Architecture Analysis
- The Graphiti adapter (`backend/app/services/graphiti_adapter.py`) is the **single** read/write surface for Neo4j (`tech.md`: "All graph reads/writes go through the `graphiti_adapter`").
- Graph build runs as a background `Task` (`models/task.py`), tracked through the `Task` model with `status`, `progress`, `error`, polled by the frontend.
- `error-handling.md` mandates that long-running tasks always reach `COMPLETED` or `FAILED`. The current silent-swallow path violates this by producing `COMPLETED` with no nodes.
- The `OpenAIEmbedder` from `graphiti_core` accepts an arbitrary `base_url` / `api_key` / `embedding_model`. Ollama's `/v1/embeddings` is OpenAI-compatible. No new client class is needed.
### Architecture Pattern & Boundary Map
```mermaid
flowchart TD
UI[Frontend Step 1<br/>Graph Build] -->|POST /api/graph/build| API[graph_bp handler]
API --> SVC[GraphBuilderService.build_graph_async]
SVC -->|spawn thread| W[_build_graph_worker]
W --> ADD[GraphBuilderService.add_text_batches]
ADD --> NS["_GraphNamespace.add_batch<br/>(this spec)"]
NS -->|_run| GR[graphiti_core.add_episode]
GR -->|/v1/embeddings| EMB[OpenAI-SDK embedder<br/>OpenAI / Gemini / Ollama]
NS -. raise on failure .-> ADD
ADD -. raise .-> W
W -. fail_task(error) .-> TM[TaskManager]
TM -. status=FAILED .-> UI
classDef changed fill:#fef3c7,stroke:#92400e,stroke-width:2px;
class NS changed;
```
**Architecture Integration**:
- **Selected pattern**: minimal extension of the existing adapter pattern — fix one method's failure semantics, add no new layer.
- **Domain/feature boundaries**: error propagation stays at the adapter; task-state translation stays in the worker; UI rendering of failed tasks is unchanged.
- **Existing patterns preserved**: single-surface graph adapter; background-task `Task` lifecycle; `_run` async-loop helper; `OpenAIEmbedder` reuse for any OpenAI-SDK target.
- **New components rationale**: none — no new module is introduced.
- **Steering compliance**:
- `error-handling.md` § Background Task Errors — failure now terminates the task with a real error.
- `error-handling.md` § Logging — ERROR level for unrecoverable; WARNING reserved for retry/recovered.
- `tech.md` § Key Libraries — adapter remains the single graph read/write surface.
### Technology Stack & Alignment
| Layer | Choice / Version | Role in Feature | Notes |
|-------|------------------|-----------------|-------|
| Frontend / CLI | Vue 3.5 (unchanged) | Polls `Task` status; renders failure | No code change. |
| Backend / Services | Python ≥3.11, Flask 3.0, `graphiti-core ≥ 0.3` | `_GraphNamespace.add_batch` failure propagation | One method edited. |
| Data / Storage | Neo4j 5.x via `bolt://` (unchanged) | Same writes attempted; failed writes never partially commit because the adapter is the only path. | — |
| Messaging / Events | None | — | — |
| Infrastructure / Runtime | Optional Ollama daemon at `http://host.docker.internal:11434/v1` | Source of `mxbai-embed-large` embeddings (1024-dim). | Documented, not enforced. |
## File Structure Plan
### Modified Files
- `backend/app/services/graphiti_adapter.py` — replace the broad `except Exception` in `_GraphNamespace.add_batch` (lines ~471473) with `logger.exception(...)` + `raise`. Remove the placeholder-UUID fallback. ~5 LOC delta.
- `.env.example` — add a commented Ollama embedder block (3 commented env-var lines + a 1-line comment about `ollama pull`).
- `CLAUDE.md` — extend the "Required Environment Variables" section to list three supported embedder providers (OpenAI, Gemini, Ollama) and the 1024-dim constraint.
- `README.md` — replace the single Gemini hint comment in the Required Environment Variables block with a short three-option block (OpenAI, Gemini, Ollama) and append a one-line `curl` smoke-test snippet inside the same setup section.
- `docker-compose.yml` — one comment line above the `mirofish` service noting that Ollama on the host is reached via `host.docker.internal:11434`.
### New Files
- None.
> No code is moved or split. All edits are local and additive except the 5-line deletion in `_GraphNamespace.add_batch`.
## System Flows
### Failure flow (the change)
```mermaid
sequenceDiagram
autonumber
participant W as _build_graph_worker
participant A as add_text_batches
participant NS as _GraphNamespace.add_batch
participant G as graphiti_core.add_episode
participant E as Embedder (Ollama / OpenAI)
participant TM as TaskManager
W->>A: chunks, batch_size
loop per batch
A->>NS: add_batch(group_id, episodes)
loop per episode
NS->>G: _run(add_episode(...))
G->>E: POST /v1/embeddings
alt embedder OK
E-->>G: 200, vector(1024)
G-->>NS: EpisodeResult
else embedder error (404 / 401 / connection)
E-->>G: 4xx/5xx
G-->>NS: raise exception
Note right of NS: logger.exception(...); raise
end
end
end
Note over A: try/except wraps add_batch and re-raises
NS-->>A: raise
A-->>W: raise
W->>TM: fail_task(task_id, str(e) + traceback)
TM-->>W: Task.status = FAILED
```
Decisions reflected in the diagram:
- The adapter raises immediately on any exception from `_g.add_episode`.
- The single-episode `add()` path (not shown) is unchanged because it already raises naturally.
- `add_text_batches` already re-raises after a localized progress message — no edit needed there.
## Requirements Traceability
| Requirement | Summary | Components | Interfaces | Flows |
|-------------|---------|------------|------------|-------|
| 1.1 | `.env.example` Ollama block | `.env.example` (modified file) | n/a | n/a |
| 1.2 | `CLAUDE.md` lists three providers + 1024-dim constraint | `CLAUDE.md` (modified file) | n/a | n/a |
| 1.3 | docker-compose / README note about `host.docker.internal:11434` | `docker-compose.yml`, `README.md` (modified files) | n/a | n/a |
| 1.4 | `curl` smoke-test snippet | `README.md` (modified file) | n/a | n/a |
| 1.5 | End-to-end build with `mxbai-embed-large` | `graphiti_adapter._build_llm_and_embedder` (unchanged) | `OpenAIEmbedderConfig` | Failure flow (happy path is identical to today) |
| 2.1 | No placeholder UUID on failure | `_GraphNamespace.add_batch` | `_EpisodeResult` (only emitted on success) | Failure flow |
| 2.2 | Propagate exception | `_GraphNamespace.add_batch` | n/a | Failure flow |
| 2.3 | `Task.FAILED` with non-empty error | `_build_graph_worker` (unchanged) | `TaskManager.fail_task` | Failure flow |
| 2.4 | Log at ERROR level | `_GraphNamespace.add_batch` | `logger.exception(...)` | Failure flow |
| 2.5 | UI shows error, no fake-success placeholder | Frontend Step 1 (unchanged) | Task polling | Failure flow |
| 2.6 | Preserve happy-path UUID contract | `_GraphNamespace.add_batch` | `_EpisodeResult.uuid_` | n/a |
| 3.1 | OpenAI/Gemini behavior unchanged | `_build_llm_and_embedder` (unchanged) | n/a | n/a |
| 3.2 | No new env var | scope rule | n/a | n/a |
| 3.3 | 1024-dim constraint documented | `CLAUDE.md` (modified file) | n/a | n/a |
## Components and Interfaces
| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
|-----------|--------------|--------|--------------|--------------------------|-----------|
| `_GraphNamespace.add_batch` | services / graph-adapter | Ingest a batch of text episodes; raise on first failure; preserve UUIDs on success | 2.1, 2.2, 2.4, 2.6 | `graphiti_core.add_episode` (P0), `app.utils.logger` (P0) | Service |
| Documentation set (`.env.example`, `CLAUDE.md`, `README.md`, `docker-compose.yml`) | docs | Describe Ollama embedder configuration and constraints | 1.1, 1.2, 1.3, 1.4, 3.3 | none | Doc |
### graph-adapter / `_GraphNamespace.add_batch`
| Field | Detail |
|-------|--------|
| Intent | Ingest each episode through `graphiti_core.add_episode`; propagate the first failure to the caller; never substitute a placeholder UUID. |
| Requirements | 2.1, 2.2, 2.4, 2.6 |
**Responsibilities & Constraints**
- Iterate `episodes` in input order.
- For each episode, call `_run(self._g.add_episode(...))` and append a `_EpisodeResult` whose `uuid_` matches the Graphiti-assigned episode UUID.
- On any exception from `_run(...)`, emit `logger.exception(...)` (ERROR level with traceback) including the `graph_id` and the index of the failing episode for diagnosability, then `raise`.
- Do **not** swallow the exception. Do **not** return a `_EpisodeResult` for the failed episode. Do **not** continue the loop after a failure.
- Domain boundary: the method speaks Graphiti and Python exceptions; it does not know about `Task` lifecycles.
- Data ownership: emits `_EpisodeResult` instances only for successfully ingested episodes.
**Dependencies**
- Inbound: `GraphBuilderService.add_text_batches` (P0, sole production caller for this method).
- Outbound: `graphiti_core.add_episode` via `_run(...)` (P0).
- External: `app.utils.logger.get_logger("mirofish.graph_builder")` (P0).
**Contracts**: Service [x] / API [ ] / Event [ ] / Batch [ ] / State [ ]
##### Service Interface
```python
class _GraphNamespace:
def add_batch(self, graph_id: str, episodes: List[Any]) -> List[_EpisodeResult]:
"""Add a batch of episodes.
Returns a list of _EpisodeResult, one per successfully ingested
episode, in input order. Raises the underlying exception on the
first failure; partial results are not returned.
Preconditions:
- graph_id is a non-empty per-project group_id.
- Each item in `episodes` exposes a `data` attribute (str)
or stringifies to a meaningful body.
Postconditions:
- On success: len(returned list) == len(episodes), each
`_EpisodeResult.uuid_` is the Graphiti-assigned UUID.
- On failure: an exception is raised; no `_EpisodeResult`
is returned for the failing episode and no further episodes
are attempted; partial successes prior to the failure are
committed in Neo4j (this matches today's behavior because
`add_episode` is invoked synchronously per episode).
Invariants:
- Never returns a `_EpisodeResult` whose UUID was generated
locally as a placeholder.
"""
```
- Preconditions: as above.
- Postconditions: as above.
- Invariants: never emit a placeholder UUID.
**Implementation Notes**
- Integration: the method is called from `GraphBuilderService.add_text_batches` (graph_builder.py:289308), which already wraps the call in `try/except Exception: progress_callback(...); raise`. No caller-side change.
- Validation: input shape unchanged.
- Risks: an environment that was producing "successful" empty graphs because of the silent fallback will now produce a failed `Task`. This is the intended correction; PR description must call it out.
### Documentation set
**Edits (verbatim intent)**:
- `.env.example` — add an opt-in commented block, e.g.:
```env
# Local embeddings via Ollama (run: ollama pull mxbai-embed-large).
# mxbai-embed-large is 1024-dim, matching Graphiti's default EMBEDDING_DIM.
# EMBEDDING_BASE_URL=http://host.docker.internal:11434/v1
# EMBEDDING_API_KEY=ollama
# EMBEDDING_MODEL=mxbai-embed-large
```
- `CLAUDE.md` — extend the embedder note to enumerate OpenAI / Gemini / Ollama and call out the 1024-dim constraint.
- `README.md` — keep the existing Gemini comment, add the Ollama three-line example, append the `curl` smoke-test below the env block.
- `docker-compose.yml` — one comment above the `mirofish` service: `# Note: Ollama on the host is reachable from this container via host.docker.internal:11434`.
These edits are doc-only; they do not affect the runtime contract.
## Data Models
No new data models. The `_EpisodeResult` dataclass shape is unchanged. The `Task` model is unchanged. The `Project.status` lifecycle is unchanged.
## Error Handling
### Error Strategy
- The adapter raises on first failure; the worker catches and routes to `Task.fail_task`. This is the existing project pattern (`error-handling.md` § Background Task Errors), and this spec aligns the adapter with it.
- No retries inside `add_batch`. Transient resilience, if added later, belongs at a layer that owns idempotency considerations (out of scope).
### Error Categories and Responses
- **Embedder configuration errors** (404 unknown model, 401 unauthorized, connection refused) → adapter raises → worker fails the task with the exception's `str()` plus traceback → frontend renders `Task.error`. Operator action: fix `EMBEDDING_*` env vars per the new docs and re-run the build.
- **Embedder transient errors** (timeouts, intermittent 5xx) → today, treated identically to configuration errors (task fails). Future follow-up may narrow this with `retry_with_backoff`.
- **Graphiti-internal errors** unrelated to embeddings (e.g., Neo4j unavailable) → already raised by `_run(...)` and currently swallowed; this fix surfaces them too. Treated as a positive side effect.
### Monitoring
- `logger.exception(...)` in `_GraphNamespace.add_batch` adds a full traceback at ERROR level, enabling existing log-aggregation setups to alert on adapter-level errors.
- `_build_graph_worker` already calls `logger.exception(f"task {task_id} failed")`; the two log lines are complementary (adapter-context vs. task-context).
## Testing Strategy
This is an extension feature — the project's testing stance is intentionally minimal (`tech.md`: "pytest is wired ... but coverage is intentionally minimal. Don't add a heavy test harness without discussing scope.").
### Unit Tests (lightweight, optional)
- If we add a test, the right scope is a single pytest case for `_GraphNamespace.add_batch` that monkeypatches `self._g.add_episode` to raise, calls `add_batch`, and asserts the exception propagates and no `_EpisodeResult` is returned. Do not add a heavier harness.
### Manual / End-to-End
1. **Happy path (OpenAI)**: existing setup — verify graph build still completes with real nodes/edges (no behavior change expected).
2. **Happy path (Ollama)**: `ollama pull mxbai-embed-large`; set the three `EMBEDDING_*` env vars per `.env.example`; run the smoke-test `curl` to confirm 1024-dim response; run a graph build through the UI; verify Neo4j has nodes/edges.
3. **Failure path (typo'd model)**: set `EMBEDDING_MODEL=text-embedding-3-small-typo` against an Ollama base URL; trigger a graph build; verify the task transitions to `FAILED` with the underlying 404 message visible in `Task.error` and the UI; verify backend logs include the ERROR-level traceback.
### Performance / Load
- Not applicable. No throughput change expected on the happy path. Failure path returns earlier than today (bonus).
## Security Considerations
- No new secrets introduced. `EMBEDDING_API_KEY=ollama` is documented as a placeholder string ignored by Ollama; this is consistent with the project's existing handling of `ZEP_API_KEY` (empty string acceptable).
- `error-handling.md` § Logging forbids logging API keys / full prompts. `logger.exception(...)` includes the exception message and traceback — Graphiti's exceptions do not echo API keys, but the ERROR log line should not include the request body. Implementation note: log only `graph_id` and episode index alongside the exception.
## Migration Strategy
- None. The fix is purely additive on documentation and a strictly-more-correct behavior change in `add_batch`. Operators do not need to take action unless their graphs were silently empty, in which case this surfacing IS the migration trigger.

View File

@ -0,0 +1,99 @@
# Gap Analysis — graphiti-ollama-embedder
## 1. Current State Investigation
### Domain assets touched by this feature
- `backend/app/services/graphiti_adapter.py`
- Lines 92139 — `_build_llm_and_embedder(provider)`. Builds an `OpenAIEmbedder` (when `provider == "openai"`) using `EMBEDDING_API_KEY or LLM_API_KEY`, `EMBEDDING_BASE_URL or LLM_BASE_URL`, and `EMBEDDING_MODEL`. Already supports pointing the embedder at any OpenAI-compatible endpoint — no code change is needed for Ollama support. This is a **documentation gap, not a code gap**.
- Lines 455475 — `_GraphNamespace.add_batch`. Iterates episodes, calls `add_episode`, and on `except Exception as e` logs a one-line `WARNING` and substitutes a fresh placeholder UUID. This is the silent-swallow path.
- Line 441453 — `_GraphNamespace.add(...)`. Single-episode path. **Already raises naturally** because there is no `try/except`.
- Lines 504506 — `_GraphNamespace.search(...)`. Has its own `except Exception` that logs and returns empty results. Per `error-handling.md` ("for non-fatal search failures, log and return empty results") this is the documented contract; out of scope.
- `backend/app/services/graph_builder.py`
- Lines 256310 — `add_text_batches(...)`. Already wraps `client.graph.add_batch(...)` in `try/except Exception` and **re-raises** after a progress message. So if `_GraphNamespace.add_batch` raises, the exception propagates correctly.
- Lines 143234 — `_build_graph_worker`. Outer `try/except Exception` calls `self.task_manager.fail_task(task_id, error_msg)` with `f"{str(e)}\n{traceback.format_exc()}"`. This already implements the "task always terminates" rule from `error-handling.md`.
- `backend/app/config.py`
- Lines 40, 5051 — defines `EMBEDDING_MODEL`, `EMBEDDING_API_KEY`, `EMBEDDING_BASE_URL`. No change required.
- `.env.example` (project root) — currently only documents the OpenAI/Gemini path with commented-out `EMBEDDING_API_KEY` / `EMBEDDING_BASE_URL` lines.
- `CLAUDE.md` lines 6082 — "Required Environment Variables" section lists `EMBEDDING_MODEL` with a note about Gemini overrides only.
- `README.md` lines 148165 — "Required Environment Variables" section, mentions "uncomment if using a non-OpenAI provider, e.g. Gemini" but no Ollama example.
- `docker-compose.yml` lines 2137 — `mirofish` service uses `env_file: .env` and overrides `NEO4J_URI`. No Ollama hint, but the standard `host.docker.internal` route works.
### Conventions extracted from steering
- `tech.md`: "All graph reads/writes go through the `graphiti_adapter`; do not call Neo4j drivers directly from feature code." — adapter is the right place for the fix.
- `error-handling.md`: "Long-running tasks must always reach a terminal state (`COMPLETED` or `FAILED`)" — silent placeholder UUID violates this.
- `error-handling.md`: "Don't catch `Exception` inside an API handler just to log and continue" — same anti-pattern in the adapter today.
- `error-handling.md` § Logging: `WARNING` is for "retry triggered, transient failure, recovered state"; `ERROR` is for "task failure, unrecoverable exception". The current `WARNING` mislabels what is actually an unrecoverable failure for the task.
- `tech.md`: Ollama is **not currently** an officially listed provider. CLAUDE.md only enumerates OpenAI and Gemini.
- `commits.md` / `dev-guidelines.md`: 4-space indent, max 120 chars/line, double-quoted Python strings, snake_case, conventional commits.
### Integration surfaces
- The `OpenAIEmbedder` from `graphiti_core.embedder.openai` already accepts an arbitrary `base_url`. Ollama exposes `/v1/embeddings` at `http://localhost:11434/v1`. No new client class is required.
- Background-task lifecycle: API handler → `GraphBuilderService.build_graph_async()` → background thread → `_build_graph_worker``fail_task(task_id, msg)`. Already in place; this feature just needs to stop short-circuiting it.
## 2. Requirements Feasibility Analysis
| Req | Need | Maps to | Gap |
| --- | --- | --- | --- |
| R1.1 | `.env.example` Ollama block | `.env.example` | **Missing** (docs) |
| R1.2 | `CLAUDE.md` lists OpenAI/Gemini/Ollama, dim constraint | `CLAUDE.md` | **Missing** (docs) |
| R1.3 | Docker-compose / README note about `host.docker.internal:11434` | `docker-compose.yml` comments / `README.md` | **Missing** (docs) |
| R1.4 | `curl` smoke-test snippet | `README.md` | **Missing** (docs) |
| R1.5 | Pipeline works end-to-end with mxbai-embed-large | adapter is already provider-agnostic via OpenAI-SDK | **No code gap** — already supported, just undocumented |
| R2.1 | Drop placeholder-UUID fallback | `graphiti_adapter.py:471473` | **Constraint** — narrow change only |
| R2.2 | Propagate ingest exception | `graphiti_adapter.py:471473` + caller | **Missing** — adapter swallows; caller re-raises if it sees an exception |
| R2.3 | `Task` transitions to `FAILED` with non-empty `error` | `graph_builder.py:231234` | **Already implemented** — relies on R2.2 |
| R2.4 | Log at `ERROR` level | `graphiti_adapter.py:472` | **Missing** — currently `WARNING` |
| R2.5 | UI shows error, no fake-success placeholder | downstream of R2.3 | **Already implemented** via task polling |
| R2.6 | Preserve happy-path UUID contract | `graphiti_adapter.py:455474` | **Constraint** — keep return shape on success |
| R3.1 | OpenAI/Gemini behavior unchanged | `_build_llm_and_embedder` | **No change needed** — branch untouched |
| R3.2 | No new env var | scope rule | **Constraint** |
| R3.3 | Document 1024-dim constraint | `CLAUDE.md` | **Missing** (docs) |
### Research needed
- None for this feature — `OpenAIEmbedder` already supports custom `base_url`, and Ollama's `/v1/embeddings` is OpenAI-compatible (well-known and used in many projects). The 1024-dim constraint comes from `graphiti_core/embedder/client.py:22` (`EMBEDDING_DIM = 1024`) and is documented in the ticket itself.
- One mild unknown: whether to narrow the `except` to a transient subset (e.g., `httpx.TimeoutException`, `httpx.NetworkError`) and retry, or simply drop the catch entirely. Decided in design phase, not blocking.
### Complexity signal
- Mostly documentation. The code change is **5 lines** in one method.
## 3. Implementation Approach Options
### Option A — Pure narrow fix in `_GraphNamespace.add_batch` + docs only (RECOMMENDED)
- **What**: delete the `except Exception` block in `add_batch` (or replace with `logger.exception(...)` + `raise`); update `.env.example`, `CLAUDE.md`, `README.md`, `docker-compose.yml` comments.
- **Files**: `backend/app/services/graphiti_adapter.py`, `.env.example`, `CLAUDE.md`, `README.md`, `docker-compose.yml`.
- **Trade-offs**:
- ✅ Minimal blast radius — adapter behavior outside `add_batch` is untouched.
- ✅ Existing background-task contract carries the failure to the UI for free.
- ✅ Honors steering rules: don't catch `Exception` to log-and-continue; tasks must terminate; ERROR-level logging for unrecoverable failures.
- ❌ Loses the (currently broken) "best effort, keep going on a partial failure" intent. In practice that intent never produced a usable graph anyway, so the loss is theoretical.
### Option B — Narrow the catch to transient errors and retry, fail loud on the rest
- **What**: keep a `try/except`, but only catch a small set of transient classes (`httpx.TimeoutException`, `httpx.NetworkError`, `openai.APIConnectionError`), wrap the whole `add_episode` call in `retry_with_backoff` from `app/utils/retry.py`, and re-raise everything else immediately.
- **Trade-offs**:
- ✅ Adds small resilience for genuinely transient blips.
- ✅ Aligns with the existing `retry_with_backoff` pattern.
- ❌ More moving parts; broader change for a bug fix.
- ❌ Single-episode `add()` would also need the same treatment to avoid two divergent retry semantics.
- ❌ Out-of-scope creep: ticket is focused on stopping the silent swallow + documenting Ollama.
### Option C — Per-provider embedder factory + Option A
- **What**: extend `_build_llm_and_embedder` with a third provider literal (`"ollama"`) that uses `OpenAIEmbedder` under the hood with hardcoded sensible defaults.
- **Trade-offs**:
- ✅ Symmetric with `openai`/`gemini`.
- ❌ The ticket explicitly lists "per-provider embedder factory" as out of scope.
- ❌ Duplicate code path — Ollama is just OpenAI-SDK with a different base URL.
## 4. Effort & Risk
- **Effort**: **S** (≤1 day). One file, one method, ~5 LOC delta plus 4 doc edits.
- **Risk**: **Low**. The change makes a previously-silent failure loud; it cannot break the happy path because the happy-path branch is the same return statement. Documentation changes are not load-bearing.
One non-zero risk: if there are real-world users today whose graph builds succeed only by accident (i.e., the fallback hides intermittent embedding failures), they will start seeing failed tasks instead of (broken) successful ones. This is the intended correction — but worth noting in the PR description so the operator can re-check their embedder credentials.
## 5. Recommendations for design phase
- **Preferred approach**: **Option A**. Smallest correct fix; documentation reflects the already-supported configuration; follows steering's error-handling philosophy literally.
- **Key decisions to lock in design**:
1. Drop the `except` entirely, or narrow it? Default: drop. Rationale: the only retry path that matters is transient network blips, and those would also kill the surrounding `_run` loop today; addressing them would be a follow-up using the project's `retry_with_backoff` decorator on the underlying graph driver call, not a band-aid in `add_batch`.
2. Which docs files mention Ollama? Default: `.env.example`, `CLAUDE.md`, `README.md`, `docker-compose.yml` comment. Two-file or three-file split?
- **Carry-forward research**: none.

View File

@ -0,0 +1,128 @@
# Requirements Document
## Project Description (Input)
Fix Graphiti embedding integration with Ollama (mxbai-embed-large) and stop silently swallowing embedding failures. Two bugs: (1) No first-class support for local Ollama embedders — `EMBEDDING_MODEL` defaults to OpenAI's `text-embedding-3-small` and the embedder reuses `LLM_BASE_URL` when `EMBEDDING_BASE_URL` is unset, so Ollama users get 404s; `.env.example` and `CLAUDE.md` don't document Ollama. (2) `backend/app/services/graphiti_adapter.py:471-473` catches every exception during episode ingestion, logs a truncated `WARNING`, and substitutes a placeholder UUID, so a graph build appears to succeed but writes nothing. Tracked as GitHub issue #18.
## Introduction
This feature adds first-class documentation for using a local Ollama embedder
(`mxbai-embed-large`, 1024-dim) with the Graphiti adapter and removes the
silent placeholder-UUID fallback in `_GraphNamespace.add_batch` so that
embedding failures terminate the surrounding background `Task` with the
underlying error visible in the UI and logs.
The work spans two narrowly scoped changes:
1. **Documentation update**`.env.example`, `CLAUDE.md`, and the README /
docker-compose comments gain a short Ollama section that explains how to
point the embedder at a local Ollama instance, why `mxbai-embed-large` is
the recommended model (1024-dim, matches Graphiti's default
`EMBEDDING_DIM`), and how to smoke-test connectivity with one `curl`
command before kicking off a graph build.
2. **Loud failure** — the broad `except Exception` in
`_GraphNamespace.add_batch` is removed (or narrowed to a small set of
transient network errors). Episode ingestion failures now propagate to
the calling background task, which marks itself `FAILED` with the
underlying error message attached, rather than logging a `WARNING` and
returning a fake UUID.
No new dependency, environment variable, or config flag is introduced.
All existing OpenAI/Gemini configurations continue to work unchanged.
## Boundary Context
- **In scope**: documenting Ollama as a third supported embedder provider
in `.env.example`, `CLAUDE.md`, and the docker-compose / README comments;
removing the silent placeholder-UUID fallback in
`_GraphNamespace.add_batch`; surfacing the underlying ingestion error to
the background `Task` so it terminates with `status=FAILED`; documenting
a one-line `curl` smoke test for embedder connectivity.
- **Out of scope**: a startup-time embedder health probe that refuses to
boot on dim/model mismatch; making `EMBEDDING_DIM` env-configurable so
768-dim or 1536-dim embedders can be used; adding a per-provider
embedder factory (today the adapter only branches on `openai` and
`gemini`); generic retry/backoff policy changes elsewhere in the
pipeline.
- **Adjacent expectations**: the existing background-task error-handling
contract from `.kiro/steering/error-handling.md` already specifies that
worker exceptions must call `fail_task(...)`. This feature relies on
that contract — it does not introduce a new one. The single-episode
`_GraphNamespace.add(...)` path is left untouched because it already
re-raises naturally.
## Requirements
### Requirement 1: Ollama Embedder Documentation
**Objective:** As a self-hosting MiroFish operator, I want first-class
documentation for using a local Ollama embedder, so that I can run the
Graphiti pipeline without needing an OpenAI- or Gemini-compatible
embeddings endpoint.
#### Acceptance Criteria
1. The `.env.example` file shall contain a commented Ollama embedder block
showing `EMBEDDING_BASE_URL`, `EMBEDDING_API_KEY`, and `EMBEDDING_MODEL`
set to `http://host.docker.internal:11434/v1`, a non-empty placeholder
string, and `mxbai-embed-large` respectively, with a comment noting the
`ollama pull mxbai-embed-large` prerequisite.
2. The `CLAUDE.md` file shall list the three supported embedder providers
(OpenAI, Gemini, Ollama) and shall state the 1024-dim constraint that
forces `mxbai-embed-large` over `nomic-embed-text` (768-dim).
3. Where the user runs MiroFish in Docker, the docker-compose comments or
README shall note that Ollama on the host is reached from the
`mirofish` container via `host.docker.internal:11434`.
4. The documentation shall include a one-line `curl` example that calls
`$EMBEDDING_BASE_URL/embeddings` with the configured model and confirms
the response embedding length is 1024.
5. When the operator follows the documented Ollama configuration with
`mxbai-embed-large` pulled in Ollama, the existing graph-build pipeline
shall complete end-to-end and write real nodes and edges to Neo4j with
no code changes beyond the env-var configuration.
### Requirement 2: Loud Embedding Failure
**Objective:** As a MiroFish operator, I want embedding failures during
graph build to surface as a visible task failure with the underlying
error, so that I can fix my embedder configuration instead of seeing an
"empty graph" with no diagnostic.
#### Acceptance Criteria
1. The `_GraphNamespace.add_batch` method shall not return a placeholder
`_EpisodeResult` UUID when the underlying `add_episode` call raises an
exception.
2. If `add_episode` raises any exception other than a narrowly defined set
of transient network errors, then `_GraphNamespace.add_batch` shall
propagate the exception to its caller.
3. When `_GraphNamespace.add_batch` propagates an exception, the
surrounding graph-build background `Task` shall transition to
`FAILED` with `Task.error` containing a non-empty message derived from
the underlying exception (per the existing
`.kiro/steering/error-handling.md` contract).
4. While a graph-build task is failing because of a misconfigured
`EMBEDDING_MODEL`, `EMBEDDING_BASE_URL`, or `EMBEDDING_API_KEY`, the
adapter shall log the underlying `add_episode` error at `ERROR` level
(not `WARNING`) before raising, so the root cause is visible in
server logs.
5. Where the configured `EMBEDDING_MODEL` is invalid (e.g. a typo, or a
model not pulled in Ollama), the user-facing project state shall move
out of `GRAPH_BUILDING` and the task shall surface the underlying
embedder error to the frontend without producing a placeholder-UUID
"successful" episode.
6. The `_GraphNamespace.add_batch` method shall preserve its current
contract for successful episodes: each successfully ingested episode
shall still produce one `_EpisodeResult` whose `uuid_` matches the
Graphiti-assigned episode UUID, in input order.
### Requirement 3: Backwards Compatibility
**Objective:** As an existing MiroFish operator already running with an
OpenAI- or Gemini-compatible embedder, I want this change to be invisible
on the happy path, so that no upgrade action is required.
#### Acceptance Criteria
1. Where `EMBEDDING_BASE_URL`, `EMBEDDING_API_KEY`, and `EMBEDDING_MODEL`
are unset or set to OpenAI/Gemini-compatible values, the embedder
construction in `_build_llm_and_embedder` shall behave identically to
the current implementation.
2. The graph-build pipeline shall not require any new environment
variable to function; Ollama support shall be enabled purely by
setting the three existing `EMBEDDING_*` variables.
3. While Graphiti's default `EMBEDDING_DIM` is 1024, the documentation
shall explicitly note that any embedder model with a different output
dimension is unsupported by this change and is an explicit follow-up
item.

View File

@ -0,0 +1,103 @@
# Research & Design Decisions — graphiti-ollama-embedder
## Summary
- **Feature**: `graphiti-ollama-embedder`
- **Discovery Scope**: Extension (small, narrowly scoped change to an existing adapter + supporting docs)
- **Key Findings**:
- The Graphiti `OpenAIEmbedder` already accepts an arbitrary `base_url` and `api_key`. Pointing it at Ollama's OpenAI-compatible `/v1/embeddings` endpoint requires **no code change** — only documentation.
- The silent placeholder-UUID fallback in `_GraphNamespace.add_batch` violates the project's existing background-task error-handling contract (`error-handling.md`: "Long-running tasks must always reach a terminal state"). The plumbing to surface a failure already exists in `_build_graph_worker`.
- `mxbai-embed-large` is the only widely-available local embedder that matches Graphiti's hard-coded `EMBEDDING_DIM = 1024`. Smaller models (`nomic-embed-text` at 768) would silently mis-fit Neo4j vector indexes and are out of scope.
## Research Log
### Ollama's OpenAI-compatible embeddings API
- **Context**: Verify that no Ollama-specific Graphiti embedder class is required.
- **Sources Consulted**: Existing code at `backend/app/services/graphiti_adapter.py:92115` (`OpenAIEmbedderConfig` accepts arbitrary `base_url`); ticket #18 description; Graphiti `embedder/client.py:22` (`EMBEDDING_DIM = 1024`).
- **Findings**:
- Ollama exposes `POST /v1/embeddings` mirroring the OpenAI shape.
- The current `_build_llm_and_embedder("openai")` branch already uses `EMBEDDING_API_KEY or LLM_API_KEY` and `EMBEDDING_BASE_URL or LLM_BASE_URL`, so any OpenAI-compatible endpoint just works.
- Ollama ignores the auth header but `OpenAIEmbedderConfig` requires a non-empty `api_key`; the literal string `"ollama"` is the de-facto convention.
- **Implications**: This is a documentation-only ask for R1. No new provider literal, no new factory branch.
### Failure-propagation contract
- **Context**: Confirm that removing the broad `except` in `_GraphNamespace.add_batch` will result in `Task.status = FAILED` in the UI.
- **Sources Consulted**:
- `.kiro/steering/error-handling.md` § Background Task Errors — outer `except Exception` in worker calls `fail_task(task_id, str(e))`.
- `backend/app/services/graph_builder.py:289308``add_text_batches` already wraps `client.graph.add_batch` in `try/except` and re-raises after a localized progress message.
- `backend/app/services/graph_builder.py:231234``_build_graph_worker` catches every exception and calls `self.task_manager.fail_task(task_id, error_msg)` with a full traceback.
- **Findings**: The chain `add_episode → _GraphNamespace.add_batch → add_text_batches → _build_graph_worker → fail_task` is intact except for the swallow at the adapter layer. Removing the swallow is sufficient; no caller-side change is required.
- **Implications**: R2.3 / R2.5 are realized for free as soon as R2.2 is implemented.
### Single vs. batch ingestion path
- **Context**: Determine whether the single-episode `_GraphNamespace.add(...)` (line 441) needs a parallel fix.
- **Sources Consulted**: `graphiti_adapter.py:441453`. No `try/except`; exceptions bubble naturally.
- **Findings**: Only the batch path swallows. The single path already complies.
- **Implications**: Fix is local to `add_batch`. Do not introduce symmetric handling in `add(...)`.
### Logging level
- **Context**: Decide between `WARNING` and `ERROR` for the failure log line.
- **Sources Consulted**: `.kiro/steering/error-handling.md` § Logging:
- `ERROR` — task failure, unrecoverable exception
- `WARNING` — retry triggered, transient failure, recovered state
- **Findings**: A failure that terminates the surrounding task is unrecoverable from the task's perspective, so `ERROR` is correct. The current `WARNING` is mislabelled.
- **Implications**: R2.4 — change to `logger.exception(...)` (which logs at ERROR with traceback).
### Documentation surfaces
- **Context**: Decide which files need updating to satisfy R1.
- **Sources Consulted**: `.env.example` (canonical config), `CLAUDE.md` lines 6082, `README.md` lines 148165, `docker-compose.yml` lines 2137.
- **Findings**: All four are appropriate. `README.md` already has a placeholder for "non-OpenAI provider" and is the natural home for the `curl` smoke test snippet. `docker-compose.yml` benefits from one additional comment about `host.docker.internal`.
- **Implications**: Update all four; keep edits minimal and additive.
## Architecture Pattern Evaluation
| Option | Description | Strengths | Risks / Limitations | Notes |
|--------|-------------|-----------|---------------------|-------|
| A. Drop swallow + docs | Remove `except` block in `add_batch`; update four docs files | Smallest surface; honors steering rules; symmetric with `add()` | Loses (broken) "best effort" intent | Recommended |
| B. Narrow + retry | Catch only transient classes (`httpx.TimeoutException`, `openai.APIConnectionError`); use `retry_with_backoff` from `app/utils/retry.py`; raise everything else | Adds resilience to genuine network blips | More moving parts; would also need to update `add()` for symmetry | Defer to follow-up |
| C. New `ollama` provider literal | Extend `_build_llm_and_embedder` with a third branch | Symmetric with `openai`/`gemini` | Explicitly out of scope per ticket; duplicate code path (Ollama is OpenAI-SDK with custom `base_url`) | Rejected |
## Design Decisions
### Decision: Adopt Option A (drop the placeholder fallback entirely; documentation only for Ollama support)
- **Context**: R2 mandates that embedding failures during graph build surface as visible task failures. R1 mandates documentation for an Ollama embedder. The adapter already supports any OpenAI-compatible base URL.
- **Alternatives Considered**:
1. **Option B (narrow + retry)** — keep a small `except` clause for transient errors and use the project's `retry_with_backoff`.
2. **Option C (new provider literal)** — add an `ollama` branch in `_build_llm_and_embedder`.
- **Selected Approach**:
- In `_GraphNamespace.add_batch`, replace the `try/except Exception` block with a straightforward call. Failures from `_run(self._g.add_episode(...))` propagate to the caller.
- Use `logger.exception(...)` immediately before re-raise is unnecessary — `_build_graph_worker` already calls `logger.exception(f"task {task_id} failed")` per the error-handling steering. To honor R2.4 explicitly without double-logging, wrap the call in a narrow `try/except: logger.exception(...); raise` so the adapter-level context (`group_id`, episode index) is captured before bubbling.
- Update `.env.example`, `CLAUDE.md`, `README.md`, and `docker-compose.yml` to document Ollama configuration (R1).
- **Rationale**:
- The ticket explicitly lists transient-retry behavior and per-provider factory as out of scope.
- Steering's error-handling chapter forbids catch-and-continue in service code.
- Smaller surface = lower regression risk.
- **Trade-offs**:
- +Visibility: real config errors now surface at the UI.
- +Code symmetry: `add()` and `add_batch()` behave the same on failure.
- One-time noise: operators whose graph builds were "succeeding" only because of the silent fallback will now see a failed task. This is the intended correction; mention in PR body.
- **Follow-up**:
- If transient blips become an operational issue, revisit Option B in a separate ticket using `retry_with_backoff` against `_g.add_episode`.
### Decision: Use `logger.exception(...)` not `logger.error(...)`
- **Context**: R2.4 requires ERROR-level logging of the underlying exception.
- **Alternatives Considered**: `logger.error(str(e))` (no traceback), `logger.warning(...)` (current behavior).
- **Selected Approach**: `logger.exception("Episode add failed (group_id=%s)", graph_id)` then `raise`.
- **Rationale**: `logger.exception` logs at ERROR with the full traceback, which is what the steering doc prescribes for unrecoverable adapter failures.
- **Trade-offs**: A small amount of duplication if `_build_graph_worker` also logs via `logger.exception`. Acceptable — the two log lines describe different layers (adapter vs. task) and have different identifying context.
### Decision: Document Ollama under the existing OpenAI provider, not as a separate provider literal
- **Context**: The ticket lists "per-provider embedder factory" as out of scope; Ollama is already reachable via the existing `openai` branch.
- **Selected Approach**: Document Ollama as a configuration *choice* of the existing `openai` Graphiti provider (set the three `EMBEDDING_*` env vars).
- **Rationale**: Avoids code duplication and matches the ticket's scope.
## Risks & Mitigations
- **Risk**: Operators currently relying on the silent fallback see new failed tasks. **Mitigation**: PR body calls this out explicitly with a "what changed" note pointing at the embedder env vars.
- **Risk**: The `except` is removed but a transient timeout intermittently fails the entire graph build. **Mitigation**: Documented as a known follow-up (Option B). Acceptable today because the alternative was an empty graph that *looked* successful.
- **Risk**: Documentation drifts between `.env.example`, `CLAUDE.md`, `README.md`. **Mitigation**: Keep all four edits in this PR and reference the same env-var triple verbatim.
## References
- Ticket #18`.ticket/18.md` (snapshot in this repo)
- Steering — `.kiro/steering/error-handling.md` § Background Task Errors and § Logging
- Steering — `.kiro/steering/tech.md` § Key Libraries (`graphiti-core` adapter rule)
- Code — `backend/app/services/graphiti_adapter.py:92115, :441475`
- Code — `backend/app/services/graph_builder.py:143234, :256310`

View File

@ -0,0 +1,23 @@
{
"feature_name": "graphiti-ollama-embedder",
"created_at": "2026-05-07T20:24:55Z",
"updated_at": "2026-05-07T20:35:00Z",
"language": "en",
"phase": "tasks-generated",
"ticket": 18,
"approvals": {
"requirements": {
"generated": true,
"approved": true
},
"design": {
"generated": true,
"approved": true
},
"tasks": {
"generated": true,
"approved": true
}
},
"ready_for_implementation": true
}

View File

@ -0,0 +1,91 @@
# Implementation Tasks — graphiti-ollama-embedder
> Source spec: `.kiro/specs/graphiti-ollama-embedder/`
> Ticket: #18
## Plan
This feature has two narrowly scoped deliverables:
1. **Code change** — remove the silent placeholder-UUID fallback in `_GraphNamespace.add_batch` so embedding failures propagate and the surrounding graph-build `Task` ends in `FAILED`.
2. **Configuration documentation** — describe the existing-but-undocumented Ollama embedder configuration in `.env.example`, `CLAUDE.md`, `README.md`, and `docker-compose.yml`.
The code change is self-contained in one method. The configuration-file edits do not depend on the code change and can run in parallel with each other.
## Tasks
- [x] 1. Make embedding-batch failures loud (adapter fix)
- [x] 1.1 Replace the silent placeholder-UUID fallback in `_GraphNamespace.add_batch` with ERROR-level logging plus exception propagation
- Open the per-episode `try/except Exception` around the synchronous `add_episode` call in the batch ingestion path of the Graphiti adapter and remove the placeholder-UUID branch entirely.
- Replace the existing `WARNING`-level log line with a `logger.exception(...)` call that captures the `graph_id` and the index of the failing episode in its message; do not include the episode body, API keys, or full traceback duplication beyond what `logger.exception` emits.
- Re-raise the original exception so it bubbles up to `GraphBuilderService.add_text_batches` (which already re-raises) and on to `_build_graph_worker` (which already calls `fail_task`).
- Preserve the happy-path contract: a successful episode still produces exactly one `_EpisodeResult` whose `uuid_` matches the Graphiti-assigned episode UUID, and the returned list keeps input order.
- Leave the single-episode `add(...)` method untouched (it already raises naturally) and leave `_GraphNamespace.search(...)` untouched (its log-and-return-empty contract is documented in steering and out of scope).
- Observable completion: when the embedder is misconfigured (e.g. `EMBEDDING_MODEL` set to an unknown model on the configured base URL), starting a graph build through the UI causes the `Task` to transition to `FAILED` with `Task.error` populated by the underlying Graphiti exception message, and the backend log includes an ERROR-level entry from the Graphiti adapter naming the failing `graph_id`.
- _Requirements: 2.1, 2.2, 2.4, 2.6_
- _Boundary: graphiti_adapter._GraphNamespace.add_batch_
- [x] 2. Document the Ollama embedder configuration
- [x] 2.1 (P) Add a commented Ollama embedder block to `.env.example`
- Append three commented environment-variable lines configuring the existing `EMBEDDING_BASE_URL`, `EMBEDDING_API_KEY`, and `EMBEDDING_MODEL` for an Ollama deployment with `mxbai-embed-large`.
- Include a short comment explaining the prerequisite step (`ollama pull mxbai-embed-large`) and the rationale for `mxbai-embed-large` over `nomic-embed-text` (1024-dim vs 768-dim, must match Graphiti's default `EMBEDDING_DIM`).
- Use `http://host.docker.internal:11434/v1` as the base URL example so the snippet works from inside the `mirofish` container; mention that host-mode (`npm run dev`) operators can substitute `http://localhost:11434/v1`.
- Set the example `EMBEDDING_API_KEY` to a non-empty placeholder string (Ollama ignores the value but `OpenAIEmbedderConfig` requires it to be non-empty).
- Leave the existing OpenAI/Gemini commented examples untouched — the Ollama block is additive.
- Observable completion: a fresh `cp .env.example .env` followed by uncommenting only the three Ollama lines and pulling the model in Ollama is sufficient to point the existing `openai`-provider Graphiti embedder at the local Ollama daemon.
- _Requirements: 1.1_
- _Boundary: .env.example_
- [x] 2.2 (P) Extend the "Required Environment Variables" section in `CLAUDE.md`
- Update the `EMBEDDING_MODEL` notes to enumerate the three supported embedder configurations: OpenAI (`text-embedding-3-small`), Gemini (`text-embedding-004`), and Ollama (`mxbai-embed-large`).
- Document the 1024-dim constraint imposed by Graphiti's default `EMBEDDING_DIM` and explicitly note that 768-dim models such as `nomic-embed-text` are unsupported until `EMBEDDING_DIM` is made configurable.
- Cross-reference `.env.example` for the Ollama-specific `EMBEDDING_BASE_URL`/`EMBEDDING_API_KEY` triple instead of duplicating the values inline.
- Observable completion: a new contributor reading only `CLAUDE.md` § "Required Environment Variables" can identify all three supported embedder providers and the dim constraint without consulting external sources.
- _Requirements: 1.2, 3.3_
- _Boundary: CLAUDE.md_
- [x] 2.3 (P) Add an Ollama section and `curl` smoke test to `README.md`
- In the "Required Environment Variables" block, add an Ollama example alongside the existing Gemini hint covering `EMBEDDING_BASE_URL`, `EMBEDDING_API_KEY`, and `EMBEDDING_MODEL`.
- Append a one-line `curl` snippet that POSTs to `$EMBEDDING_BASE_URL/embeddings` with the configured model and a trivial input, then pipes through `jq '.data[0].embedding | length'` to verify a `1024` response — explicitly framed as a pre-build smoke test.
- Use the same `host.docker.internal:11434` convention as `.env.example` and `docker-compose.yml`, with a short note on the `localhost` substitution for host-mode operators.
- Keep the existing copy/install steps untouched; this edit is additive within the same `## Configure Environment Variables` (or equivalent) subsection.
- Observable completion: an operator running the new `curl` snippet against a correctly configured Ollama daemon sees `1024` printed to stdout and can use that as a go/no-go signal before kicking off the graph build.
- _Requirements: 1.3, 1.4_
- _Boundary: README.md_
- [x] 2.4 (P) Add a `host.docker.internal` comment to the `mirofish` service in `docker-compose.yml`
- Add a single comment line above (or alongside) the existing `NEO4J_URI` override in the `mirofish` service noting that an Ollama daemon running on the host is reachable from this container via `host.docker.internal:11434` and that this is the value to use for `EMBEDDING_BASE_URL` when running the Compose stack.
- Do not introduce any new service, environment variable, or volume; the change is comment-only.
- Observable completion: a reader of `docker-compose.yml` who sets up Ollama on the host can derive the correct `EMBEDDING_BASE_URL` value without consulting external Docker networking documentation.
- _Requirements: 1.3_
- _Boundary: docker-compose.yml_
- [ ] 3. Manual end-to-end verification (deferred to reviewer — requires running Neo4j + LLM stack)
- [ ] 3.1 Verify the happy and failure paths through the graph-build pipeline (deferred to reviewer)
- Run `npm run dev` against the existing OpenAI/Qwen-style embedder configuration to confirm the graph-build flow still completes with real nodes/edges in Neo4j (regression check for R3.1).
- Set `EMBEDDING_MODEL` to a deliberately invalid value (e.g. `text-embedding-3-small-typo`) against the same base URL, trigger a graph build through the UI, and confirm the project exits `GRAPH_BUILDING`, the backing `Task` reaches `status = FAILED`, and `Task.error` carries the underlying 404/unknown-model message (R2.3, R2.5). Inspect the backend logs for the new ERROR-level entry from the Graphiti adapter (R2.4).
- If an Ollama daemon with `mxbai-embed-large` is available, follow the documented `.env.example` snippet plus the `curl` smoke test, then run a full graph build and confirm Neo4j has nodes/edges scoped to the project's `group_id` (R1.5).
- Note in the PR body that, on a partial-batch failure, episodes successfully written before the failure remain committed in Neo4j (post-condition documented in design.md); a re-run appends rather than overwrites because Graphiti episode UUIDs are unique.
- Observable completion: PR description records the three scenarios (OpenAI happy path, deliberate-typo failure path, optional Ollama happy path) with the resulting `Task` status, an excerpt of `Task.error` for the failure case, and a link to (or extract from) the ERROR-level adapter log.
- _Depends: 1.1, 2.1, 2.2, 2.3, 2.4_
- _Requirements: 1.5, 2.3, 2.4, 2.5, 3.1, 3.2_
- _Boundary: end-to-end pipeline (verification only, no code change)_
## Requirements Coverage
| Requirement | Tasks |
|-------------|-------|
| 1.1 | 2.1 |
| 1.2 | 2.2 |
| 1.3 | 2.3, 2.4 |
| 1.4 | 2.3 |
| 1.5 | 3.1 |
| 2.1 | 1.1 |
| 2.2 | 1.1 |
| 2.3 | 3.1 (verification — already implemented in `_build_graph_worker`) |
| 2.4 | 1.1, 3.1 |
| 2.5 | 3.1 (verification — already implemented in frontend task polling) |
| 2.6 | 1.1 |
| 3.1 | 3.1 |
| 3.2 | 3.1 |
| 3.3 | 2.2 |

View File

@ -69,8 +69,15 @@ LLM_MODEL_NAME # Default: qwen-plus
NEO4J_URI # Default: bolt://localhost:7687
NEO4J_USER # Default: neo4j
NEO4J_PASSWORD # Default: mirofish123 (override in real env)
EMBEDDING_MODEL # Default: text-embedding-3-small
# Override for non-OpenAI providers (e.g. Gemini: text-embedding-004)
EMBEDDING_MODEL # Default: text-embedding-3-small (OpenAI)
# Other supported configurations:
# • Gemini: text-embedding-004
# • Ollama: mxbai-embed-large
# (also set EMBEDDING_BASE_URL / EMBEDDING_API_KEY;
# see .env.example for the full snippet)
# Constraint: model must produce 1024-dim vectors to match
# Graphiti's default EMBEDDING_DIM. 768-dim models such as
# nomic-embed-text are not supported.
# Optional — Accelerated LLM (omit entirely if not used)
LLM_BOOST_API_KEY

View File

@ -162,6 +162,25 @@ NEO4J_PASSWORD=your_neo4j_password
# Embedding model (uncomment if using a non-OpenAI provider, e.g. Gemini)
# EMBEDDING_MODEL=gemini-embedding-001
# Embedding model via local Ollama (free, no API key, OpenAI-compatible endpoint).
# Pre-requisite: `ollama pull mxbai-embed-large` (1024-dim, matches Graphiti).
# In Docker, host.docker.internal:11434 reaches the host daemon; in host mode
# (`npm run dev`) substitute http://localhost:11434/v1.
# EMBEDDING_BASE_URL=http://host.docker.internal:11434/v1
# EMBEDDING_API_KEY=ollama
# EMBEDDING_MODEL=mxbai-embed-large
```
**Embedder smoke test (recommended before the first graph build):**
```bash
curl -s "$EMBEDDING_BASE_URL/embeddings" \
-H "Authorization: Bearer $EMBEDDING_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"'"$EMBEDDING_MODEL"'","input":"ping"}' \
| jq '.data[0].embedding | length'
# Expected output: 1024
```
**Optional — Accelerated LLM Configuration:**

View File

@ -453,9 +453,17 @@ class _GraphNamespace:
return _EpisodeResult(uuid_=ep_uuid_out)
def add_batch(self, graph_id: str, episodes: List[Any]) -> List[_EpisodeResult]:
"""Add a batch of episodes. Returns list of EpisodeResult with uuid_."""
"""Add a batch of episodes. Returns one _EpisodeResult per episode in input order.
On the first ingestion failure the underlying exception is logged at ERROR
level (with traceback) and re-raised; episodes successfully ingested before
the failure remain committed in Neo4j. The caller (the graph-build worker)
translates the propagated exception into Task.status = FAILED with the
underlying error message never substitute a placeholder UUID, since that
would produce a Task that looks completed while the graph is empty.
"""
results = []
for ep in episodes:
for index, ep in enumerate(episodes):
text = getattr(ep, 'data', '') or str(ep)
try:
result = _run(self._g.add_episode(
@ -467,10 +475,13 @@ class _GraphNamespace:
group_id=graph_id,
update_communities=False,
))
ep_uuid_out = result.episode.uuid if result and result.episode else str(_uuid_mod.uuid4())
except Exception as e:
logger.warning(f"Episode add failed: {str(e)[:100]}, using placeholder uuid")
ep_uuid_out = str(_uuid_mod.uuid4())
except Exception:
logger.exception(
"Episode add failed (group_id=%s, episode_index=%d)",
graph_id, index,
)
raise
ep_uuid_out = result.episode.uuid if result and result.episode else str(_uuid_mod.uuid4())
results.append(_EpisodeResult(uuid_=ep_uuid_out))
return results

View File

@ -28,6 +28,9 @@ services:
environment:
# In-Docker override; host-mode (`npm run dev`) uses the bolt://localhost:7687 default from Config.
NEO4J_URI: bolt://neo4j:7687
# Note: an Ollama daemon running on the host is reached from this container
# via host.docker.internal:11434. Set EMBEDDING_BASE_URL=http://host.docker.internal:11434/v1
# in your .env to point the Graphiti embedder at a local Ollama instance.
depends_on:
neo4j:
condition: service_healthy