Add Venkai to an agent¶
A working integration in one file, then the decisions behind each part.
The complete integration¶
"""venkai_memory.py — the whole integration. Two calls."""
import os
import requests
BASE = os.environ.get("VENKAI_BASE_URL", "https://api.venkai.fr")
KEY = os.environ["VENKAI_API_KEY"] # fail at import, not mid-run
PROJECT = "billing-service" # constant: a typo makes a new project
_H = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}
_TIMEOUT = 10
def load_context(task: str, limit: int = 5, min_score: float = 0.0,
max_chars: int = 4000) -> str:
"""Ranked memories for `task`, as a prompt block.
Venkai ranks and bounds by count; the score floor and the character
budget are the caller's job — neither exists server-side.
"""
r = requests.get(f"{BASE}/api/context/{PROJECT}/relevant",
headers=_H, params={"query": task, "limit": limit},
timeout=_TIMEOUT)
r.raise_for_status()
selected, used = [], 0
for m in r.json()["memories"]:
if m["score"] < min_score:
continue
cost = len(m["content"])
if used + cost > max_chars:
continue # skip, not break: a shorter high-scorer may still fit
selected.append(f"- [{m['type']}] {m['content']}")
used += cost
if not selected:
return ""
return "Established context for this project:\n" + "\n".join(selected)
def record(content: str, type: str = "decision", importance: float = 0.8,
agent: str = "planner", metadata: dict | None = None) -> None:
"""Store a conclusion. Failing to remember must not fail the task."""
try:
requests.post(f"{BASE}/api/memory", headers=_H, timeout=_TIMEOUT, json={
"project_id": PROJECT, "agent_id": agent, "content": content,
"type": type, "importance": importance, "metadata": metadata or {},
}).raise_for_status()
except requests.RequestException as exc:
# ponytail: log and continue. Memory is an enhancement; an agent that
# crashes because it could not write a note is worse than a forgetful one.
print(f"[venkai] could not record memory: {exc}")
Use it:
def run(task: str):
prompt = f"{load_context(task)}\n\nTask: {task}"
result = call_your_llm(prompt)
if result.decision:
record(result.decision, type="decision", importance=0.85)
return result
That is the integration. The rest of this page is why.
Decisions in that code¶
Read fails loudly, write fails quietly¶
load_context raises: an agent running without its context will produce
confidently wrong output, and you want to know. record swallows: losing one
note is a small loss, crashing a task is a big one.
The project key is a module constant¶
Writing to a key that does not exist creates it and returns 200. There is no
error anywhere in the typo path — the write succeeds into a new empty project,
and later reads on the correct key come back empty. A constant removes the
class of bug.
The budget is enforced client-side¶
limit bounds the number of memories, not their size. min_score and
max_chars are the caller's, because neither exists server-side. See
Context selection.
continue, not break¶
Skipping an oversized memory lets a shorter, still-highly-ranked one through. Breaking on the first overflow throws away the tail of a good list.
Where the calls belong¶
flowchart TB
T["task arrives"] --> L["load_context(task)"]
L --> P["build prompt"]
P --> M["LLM"]
M --> A["act / iterate"]
A --> D{"did we conclude<br/>something durable?"}
D -->|yes| R["record(...)"]
D -->|no| E["done"]
R --> E
Read once per task, at the top — not per LLM call. A ten-step agent that recalls each step pays ten times and repeats the same memories in every prompt.
Write on conclusions, not observations:
| Write it | Skip it |
|---|---|
| "We chose Postgres because the ranking query needs joins" | "I am reading config.py" |
| "Refunds over 500 EUR need a second approver" | Tool output you can re-read |
| "The earlier assumption about tz handling was wrong" | The step-by-step trace |
| "This user prefers terse answers" | Anything already in the repo |
Every low-value row competes for retrieval budget forever. Restraint on the write path is what keeps the read path useful.
Placing the block in the prompt¶
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "system", "content": load_context(task)}, # before the task
{"role": "user", "content": task},
]
Keep it in a system message, distinct from the user's words, and labelled as established context. Merged into the user turn, models start treating stored constraints as part of the current request and try to act on them.
Letting the model write¶
Give the model record as a tool — it knows when it concluded something and
you do not:
TOOLS = [{
"name": "remember",
"description": (
"Record a durable conclusion about this project: a decision and its "
"reason, a hard constraint, or a correction of an earlier belief. "
"Do NOT record intermediate steps, tool output, or anything already "
"in the repository."
),
"input_schema": {
"type": "object",
"properties": {
"content": {"type": "string"},
"type": {"enum": ["fact", "decision", "preference",
"event", "constraint", "relationship"]},
"importance": {"type": "number", "minimum": 0, "maximum": 1},
},
"required": ["content", "type"],
},
}]
Do not do the same for recall. Models call it with vague queries or forget entirely; deterministic loading at the top of the task is more reliable.
Verify it works¶
# 1. the memory landed
curl -s "https://api.venkai.fr/api/context/billing-service?limit=5" \
-H "Authorization: Bearer $VENKAI_API_KEY" | python -m json.tool
# 2. it comes back for a plausible query
curl -sG https://api.venkai.fr/api/context/billing-service/relevant \
-H "Authorization: Bearer $VENKAI_API_KEY" \
--data-urlencode 'query=database choice' --data-urlencode 'limit=3'
If (1) shows rows and (2) does not return them, the ranking is the issue, not
the integration — raise importance, or switch to the semantic embedding
provider (Embeddings).