From 7a172c7d31686c3d1f46c4039557bc9f76e57377 Mon Sep 17 00:00:00 2001 From: Mohamed Amine Ferhi Date: Fri, 3 Jul 2026 06:55:32 +0000 Subject: [PATCH] examples: add Ollama persistent-memory recipes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two runnable examples showing how to give a stateless Ollama chat persistent, decay-weighted memory via Dakera: - ollama_memory_chat.py: embeds recall/store around ollama.chat using ChatMemorySession (recalls across the agent's full memory, stores each turn as episodic memory, weights durable preferences by importance). - ollama_memory_proxy.py: a transparent FastAPI proxy — point any Ollama client at :8080 instead of :11434 and /api/chat gains memory with no app changes. Implements the recipe requested in ollama/ollama#16987 with corrected grounding (port 3000, dakera-deploy self-host, context injected as a system message rather than a non-existent top-level field). Both are lint-clean (ruff) and excluded from the live-server examples CI job since they require a running Ollama with a pulled model. --- README.md | 2 + examples/ollama_memory_chat.py | 107 ++++++++++++++++++++++++++++++++ examples/ollama_memory_proxy.py | 98 +++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 examples/ollama_memory_chat.py create mode 100644 examples/ollama_memory_proxy.py diff --git a/README.md b/README.md index 11ef07f..3f44cfd 100644 --- a/README.md +++ b/README.md @@ -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 --- diff --git a/examples/ollama_memory_chat.py b/examples/ollama_memory_chat.py new file mode 100644 index 0000000..0619d51 --- /dev/null +++ b/examples/ollama_memory_chat.py @@ -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() diff --git a/examples/ollama_memory_proxy.py b/examples/ollama_memory_proxy.py new file mode 100644 index 0000000..1e9498b --- /dev/null +++ b/examples/ollama_memory_proxy.py @@ -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()