Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 45 additions & 29 deletions docs/auto-extraction-setup.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
# Automatic Memory Extraction Setup

Lore can automatically extract memorable facts from Hermes conversations using a fast
LLM (Llama 3.1 8B via OpenRouter) and store them in your knowledge base.
LLM and store them in your knowledge base.

The default provider is **OpenRouter**, used as the transport but hard-pinned to Cerebras
via `provider_order: ["Cerebras"]` (which becomes `provider.only` in the request). Talking
to Cerebras Cloud directly is also supported as an alternative.

## Prerequisites

- A Lore MCP server running with PostgreSQL backend
- An OpenRouter API key (free tier is sufficient)
- An OpenRouter API key (default) **or** a Cerebras API key

## Option A: OpenRouter
## Option A: OpenRouter (default, pinned to Cerebras)

### 1. Get an OpenRouter API key
OpenRouter is used as the transport, but the request is hard-pinned to Cerebras via
`provider.only` so it fails loudly rather than silently routing to a different backend.

### 1. Get an OpenRouter API key
1. Sign up at https://openrouter.ai
2. Create a key at https://openrouter.ai/keys
3. Free tier: suitable for personal use (~$0.0001 per extraction call)

## 2. Set the environment variable
### 2. Set the environment variable

```bash
export OPENROUTER_API_KEY=sk-or-v1-...
Expand All @@ -27,7 +32,7 @@ Or add to your `.env` file:
OPENROUTER_API_KEY=sk-or-v1-...
```

## 3. Enable in plugin config
### 3. Enable in plugin config

In your `plugin.yaml` (or Hermes plugin config):

Expand All @@ -37,43 +42,53 @@ config:
enabled: true
model: meta-llama/llama-3.1-8b-instruct
provider_order:
- Groq # ~800 tok/s — fastest
- Together # fallback
- Fireworks # fallback
confidence_threshold: 0.75 # 0–1; higher = fewer but more reliable memories
dedup_similarity_threshold: 0.85 # cosine similarity for merge vs new entry
min_turns: 3 # skip single-exchange sessions
- Cerebras # hard-pinned via "only" — will not fall through to other providers
confidence_threshold: 0.75
dedup_similarity_threshold: 0.85
min_turns: 3
```

## Option B: Cerebras (recommended if already using Cerebras for Hermes)
No `provider` key is needed — `openrouter` is the default. The `provider_order` defaults
to `["Cerebras"]` if omitted.

Note: `provider_order` maps to `provider.only` in the OpenRouter request body, not
`provider.order`. This means the request will return an error rather than silently
routing to Groq, Together, or Fireworks if Cerebras is unavailable.

## Option B: Cerebras (direct)

Cerebras Cloud runs on wafer-scale chips. With 300+ TPS and 91–99% prompt cache hit rate,
the static extraction system prompt is effectively free after the first call.
If you prefer to talk to Cerebras Cloud directly instead of through OpenRouter, set
`provider: cerebras`. Cerebras Cloud runs on wafer-scale chips. With 300+ TPS and 91–99%
prompt cache hit rate, the static extraction system prompt is effectively free after the
first call.

### Get a Cerebras API key
### 1. Get a Cerebras API key
1. Sign up at https://cloud.cerebras.ai
2. Create an API key

### Set environment variable
### 2. Set the environment variable

```bash
export CEREBRAS_API_KEY=csk-...
```

### Configure
### 3. Enable in plugin config

```yaml
auto_extract:
enabled: true
provider: cerebras
model: gpt-oss-120b
confidence_threshold: 0.75
dedup_similarity_threshold: 0.12
min_turns: 3
config:
auto_extract:
enabled: true
provider: cerebras
model: gpt-oss-120b
confidence_threshold: 0.75
dedup_similarity_threshold: 0.12
min_turns: 3
```

## How it works

At the end of each session, Lore asynchronously:
1. Sends the conversation turns to Llama 3.1 8B via OpenRouter (Groq backend)
1. Sends the conversation turns to a fast model via OpenRouter pinned to Cerebras (or Cerebras direct)
2. Extracts facts, preferences, goals, events, and system facts
3. Deduplicates against your existing KB using vector similarity
4. Writes new entries or updates existing ones
Expand Down Expand Up @@ -117,5 +132,6 @@ to the agent until approved.

## Disabling

Set `enabled: false` or remove `OPENROUTER_API_KEY`. The plugin degrades gracefully —
manual `kb_add` continues to work as before.
Set `enabled: false` or remove the relevant API key (`CEREBRAS_API_KEY` or
`OPENROUTER_API_KEY`). The plugin degrades gracefully — manual `kb_add` continues to work
as before.
15 changes: 9 additions & 6 deletions src/lore/extraction/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
Two OpenAI-compatible providers are supported, selected via the
``provider`` config key:

* ``openrouter`` (default) — Llama 3.1 8B routed through Groq/Together/Fireworks.
* ``cerebras`` — gpt-oss-120b on Cerebras Cloud (300+ TPS, 91-99% prompt cache).
* ``openrouter`` (default) — used as the transport, but hard-pinned to Cerebras
via ``provider.only`` so requests fail loudly rather than silently routing elsewhere.
* ``cerebras`` — gpt-oss-120b on Cerebras Cloud directly (300+ TPS, 91-99% prompt cache).

Extraction is *best-effort*: every failure mode (missing API key, HTTP error,
malformed JSON, unknown provider) is logged and converted to an empty result.
Expand All @@ -35,7 +36,9 @@
DEFAULT_PROVIDER = "openrouter"
DEFAULT_MODEL = "meta-llama/llama-3.1-8b-instruct"
CEREBRAS_DEFAULT_MODEL = "gpt-oss-120b"
DEFAULT_PROVIDER_ORDER = ["Groq", "Together", "Fireworks"]
# Default provider order for OpenRouter routing. Using "only" (not "order") for a hard pin
# so the request fails rather than silently routing to a different provider.
DEFAULT_PROVIDER_ORDER = ["Cerebras"]
# Llama 3.1 8B is the largest model this pipeline uses on purpose — extraction
# must stay fast and cheap. Do not raise this to a reasoning/larger model.
_REQUEST_TIMEOUT = 30.0
Expand All @@ -44,8 +47,8 @@
class ExtractionClient:
"""Thin async wrapper over an OpenAI-compatible chat-completions endpoint.

Dispatches to OpenRouter (default) or Cerebras based on the ``provider``
config key.
Dispatches to OpenRouter (default, hard-pinned to Cerebras via
``provider.only``) or Cerebras directly, based on the ``provider`` config key.
"""

def __init__(self, config: dict[str, Any] | None = None):
Expand Down Expand Up @@ -118,7 +121,7 @@ def _build_request(
}
body = {
"model": model,
"provider": {"order": list(provider_order)},
"provider": {"only": list(provider_order)},
"messages": messages,
"response_format": {"type": "json_object"},
"temperature": 0.1,
Expand Down
8 changes: 4 additions & 4 deletions tests/test_extraction_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def __init__(self, response, recorder):
async def __aenter__(self):
return self

async def __aexit__(self, *exc):
async def __aexit__(self, *_):
return False

async def post(self, url, *, headers=None, json=None): # noqa: A002
Expand All @@ -57,7 +57,7 @@ async def post(self, url, *, headers=None, json=None): # noqa: A002


def _patch_client(response, recorder):
def _factory(*args, **kwargs):
def _factory(*_args, **_kwargs):
return _FakeAsyncClient(response, recorder)

return mock.patch.object(httpx, "AsyncClient", _factory)
Expand Down Expand Up @@ -128,7 +128,7 @@ def test_provider_order_in_request_body(monkeypatch):
response = _FakeResponse(_valid_payload('{"memories": []}'))
with _patch_client(response, recorder):
asyncio.run(ExtractionClient().extract("[user]: hi\n", {}))
assert recorder["body"]["provider"]["order"] == ["Groq", "Together", "Fireworks"]
assert recorder["body"]["provider"]["only"] == ["Cerebras"]


def test_model_in_request_body(monkeypatch):
Expand Down Expand Up @@ -188,4 +188,4 @@ def test_openrouter_still_has_provider_routing(monkeypatch):
ExtractionClient().extract("[user]: hi\n", {"auto_extract": {"provider": "openrouter"}})
)
assert recorder["url"] == "https://openrouter.ai/api/v1/chat/completions"
assert recorder["body"]["provider"]["order"] == ["Groq", "Together", "Fireworks"]
assert recorder["body"]["provider"]["only"] == ["Cerebras"]
Loading