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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ See the [`examples/`](examples/) directory:

- [`basic_usage.py`](examples/basic_usage.py) — vectors, namespaces, queries, filters
- [`hybrid_search.py`](examples/hybrid_search.py) — full-text, vector, and hybrid search
- [`ollama_memory_chat.py`](examples/ollama_memory_chat.py) — persistent memory for a local Ollama chat loop
- [`ollama_memory_proxy.py`](examples/ollama_memory_proxy.py) — transparent memory proxy in front of Ollama (`/api/chat`)
- [`tealtiger_governance.py`](examples/tealtiger_governance.py) — TealTiger governance middleware

---
Expand Down
107 changes: 107 additions & 0 deletions examples/ollama_memory_chat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env python3
"""
Dakera Python SDK — persistent memory for a local Ollama chat loop.

Ollama's ``/api/chat`` endpoint is stateless: every call starts fresh. This
example wraps an Ollama chat with Dakera so the app transparently *recalls*
relevant long-term context before each turn and *stores* the turn afterwards.
Because memories are importance-scored and decay over time, stale context stops
competing with fresh, relevant facts.

Key Dakera features shown:
* ``ChatMemorySession`` — a session-scoped helper that recalls across the
agent's full memory (not just this session) and stores turns as episodic
memories.
* Importance weighting — durable preferences are stored at high importance
so they outrank ordinary chatter during recall.
* Semantic recall with relevance scores.

Prerequisites:
* Ollama running locally with a model pulled, e.g.::

ollama pull llama3.2

* A Dakera server. The canonical self-hosted path is the public
``dakera-deploy`` docker-compose (server image + object store):
https://github.com/dakera-ai/dakera-deploy
* ``pip install "dakera" "ollama>=0.4"``

Run:
OLLAMA_MODEL=llama3.2 python examples/ollama_memory_chat.py
"""

import os
import sys

try:
import ollama
except ImportError:
sys.exit("This example needs the Ollama client: pip install 'ollama>=0.4'")

from dakera import ChatMemorySession, DakeraClient

MODEL = os.environ.get("OLLAMA_MODEL", "llama3.2")
AGENT_ID = "ollama-demo-user"


def chat(session: ChatMemorySession, user_msg: str, top_k: int = 5) -> str:
"""Answer *user_msg* with an Ollama model grounded in Dakera memory."""
# 1. Recall relevant long-term context for this agent.
recalled = session.recall(user_msg, top_k=top_k)
if recalled:
context = "\n".join(f"- {m.content}" for m in recalled)
system = f"Relevant context from earlier conversations:\n{context}"
else:
system = "You are a helpful assistant."

# 2. Normal stateless Ollama call, now grounded in memory. Context is
# passed as a system *message* (Ollama /api/chat has no top-level
# "system" field — it takes a messages array).
response = ollama.chat(
model=MODEL,
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user_msg},
],
)
answer = response["message"]["content"]

# 3. Persist the turn. Ordinary turns use the session default importance
# (0.6); ChatMemorySession tags each memory with its role automatically.
session.store("user", user_msg)
session.store("assistant", answer)
return answer


def main() -> None:
client = DakeraClient(
os.environ.get("DAKERA_API_URL", "http://localhost:3000"),
api_key=os.environ.get("DAKERA_API_KEY", "dk-mykey"),
)

session = ChatMemorySession.create(
client, AGENT_ID, metadata={"app": "ollama-memory-chat"}
)
try:
# Seed a durable preference at high importance so it reliably surfaces
# on later, semantically related turns.
session.store(
"user",
"My name is Sam and I always want answers in metric units.",
importance=0.95,
)

for turn in [
"What's a good outdoor temperature for a run?",
"Remind me — what's my name and which units do I prefer?",
]:
print(f"\n> {turn}")
print(chat(session, turn))
finally:
# Closing summarizes the session; the stored turns remain recallable
# by this agent in future sessions.
session.close(summary="Ollama memory-chat demo session.")


if __name__ == "__main__":
main()
98 changes: 98 additions & 0 deletions examples/ollama_memory_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""
Dakera Python SDK — a transparent persistent-memory proxy in front of Ollama.

This is a drop-in middleware: point any existing Ollama client at this proxy
(``http://localhost:8080``) instead of Ollama directly (``http://localhost:11434``)
and every ``/api/chat`` call transparently gains long-term memory. The proxy:

1. recalls relevant context for the incoming conversation from Dakera,
2. injects it as a system message and forwards the request to Ollama,
3. stores the user turn and the assistant reply back into Dakera.

No changes to the calling application are required — it keeps speaking the
Ollama API. This mirrors the recipe requested in ollama/ollama#16987, with the
grounding corrected: Dakera listens on port 3000 (not 3300), self-hosting uses
the ``dakera-deploy`` compose (the server needs its object store), and recalled
context is injected as a system *message* rather than a non-existent top-level
``system`` field.

Prerequisites:
* Ollama running locally (``ollama serve`` + e.g. ``ollama pull llama3.2``).
* A Dakera server — self-host via https://github.com/dakera-ai/dakera-deploy
* ``pip install "dakera[async]" fastapi uvicorn httpx``

Run:
uvicorn examples.ollama_memory_proxy:app --port 8080
# then, from any Ollama client:
# curl http://localhost:8080/api/chat -d '{
# "model": "llama3.2", "stream": false,
# "messages": [{"role": "user", "content": "Hi, I am Sam."}]}'
"""

import os
import sys

try:
import httpx
from fastapi import FastAPI, Request
except ImportError:
sys.exit("This example needs: pip install fastapi uvicorn httpx 'dakera[async]'")

from dakera import AsyncChatMemorySession, AsyncDakeraClient

OLLAMA_URL = os.environ.get("OLLAMA_URL", "http://localhost:11434")
AGENT_ID = os.environ.get("DAKERA_AGENT_ID", "ollama-proxy-user")

app = FastAPI(title="Dakera → Ollama memory proxy")

_memory = AsyncDakeraClient(
os.environ.get("DAKERA_API_URL", "http://localhost:3000"),
api_key=os.environ.get("DAKERA_API_KEY", "dk-mykey"),
)
_ollama = httpx.AsyncClient(base_url=OLLAMA_URL, timeout=120.0)


def _last_user_message(messages: list[dict]) -> str:
"""Return the content of the most recent user message, if any."""
for message in reversed(messages):
if message.get("role") == "user":
return message.get("content", "")
return ""


@app.post("/api/chat")
async def chat_with_memory(request: Request) -> dict:
"""Ollama-compatible /api/chat that transparently recalls and stores memory."""
body = await request.json()
messages: list[dict] = list(body.get("messages", []))
user_msg = _last_user_message(messages)

session = await AsyncChatMemorySession.create(_memory, AGENT_ID)
try:
# 1. Recall and inject relevant context as a leading system message.
if user_msg:
recalled = await session.recall(user_msg, top_k=5)
if recalled:
context = "\n".join(f"- {m.content}" for m in recalled)
messages.insert(
0,
{"role": "system", "content": f"Relevant context:\n{context}"},
)

# 2. Forward to Ollama. Non-streaming so we can capture the full reply;
# a streaming variant would tee chunks and store on completion.
body["messages"] = messages
body["stream"] = False
resp = await _ollama.post("/api/chat", json=body)
data = resp.json()

# 3. Store the exchange for future recall.
if resp.status_code == 200 and user_msg:
answer = data.get("message", {}).get("content", "")
await session.store("user", user_msg)
if answer:
await session.store("assistant", answer)
return data
finally:
await session.close()
Loading