diff --git a/.cursor/skills/behavioral-memory/SKILL.md b/.cursor/skills/behavioral-memory/SKILL.md new file mode 100644 index 0000000..98f99bc --- /dev/null +++ b/.cursor/skills/behavioral-memory/SKILL.md @@ -0,0 +1,231 @@ +--- +name: behavioral-memory +description: >- + Integrate behavioral-memory into any LangChain/LangGraph agent. Use when the + user asks to add behavioral memory, execution trace retrieval, validated + memory, learning from feedback, or tool orchestration memory to their agent. + Also use when wiring thumbs-up/down feedback into a trace store, connecting + Langfuse feedback loops, or setting up pgvector persistence for agent traces. +disable-model-invocation: true +--- + +# Behavioral Memory Integration + +Add validated execution trace retrieval to any LLM agent in under 50 lines. +The library stores task-to-tool-chain mappings and retrieves semantically +similar ones at query time so the agent learns from past successes. + +## Install + +```bash +pip install behavioral-memory # in-memory store (no DB) +pip install "behavioral-memory[postgres]" # pgvector persistence +``` + +## Prerequisites (check before starting) + +| Need | Why | +|------|-----| +| A LangChain-compatible LLM (`BaseChatModel`) | PlanEngine calls it | +| A LangChain-compatible `Embeddings` model | Vector similarity search | +| Python 3.11+ | Library requirement | +| PostgreSQL + pgvector (optional) | Persistent `TraceStore` | +| Langfuse account (optional) | Feedback loop | + +## Quick Integration (3 steps) + +### Step 1 — Create store + engine + +```python +from behavioral_memory import InMemoryTraceStore, PlanEngine, ToolRegistry, ToolSchema + +store = InMemoryTraceStore(embeddings=your_embeddings) +registry = ToolRegistry() + +# Register every tool your agent has +for tool in your_agent_tools: + registry.register(ToolSchema( + name=tool.name, + description=tool.description, + parameters_schema=tool.args_schema.model_json_schema() if tool.args_schema else {}, + )) + +engine = PlanEngine(llm=your_llm, store=store, registry=registry) +``` + +### Step 2 — Seed domain knowledge + +```python +from behavioral_memory import ExecutionTrace, ToolCall + +store.add(ExecutionTrace( + task_description="your natural language task here", + tool_chain=[ + ToolCall(step_id="s1", tool_name="tool_a", parameters={"key": "val"}), + ToolCall(step_id="s2", tool_name="tool_b", parameters={"source_step": "s1"}), + ], + source="seed", +)) +``` + +### Step 3 — Generate plans with memory + +```python +plan = engine.generate(query="user's task description") +for step in plan.steps: + print(f"{step.step_id}: {step.tool_name}({step.parameters})") +``` + +## Persistence — pgvector (production) + +`TraceStore` persists traces across restarts. The collection name is stable +— it does NOT recreate on every deployment. Deduplication (cosine >= 0.95) +prevents the same trace from being stored twice. + +```python +from behavioral_memory import TraceStore # requires [postgres] extra + +store = TraceStore( + embeddings=your_embeddings, + connection_url="postgresql+psycopg://user:pass@host:5432/dbname", + collection_name="validated_traces", # stable across deploys +) +``` + +**Critical**: both `InMemoryTraceStore` and `TraceStore` expose the same API +(`search`, `add`, `add_bulk`, `similarity_score`, `count`). Swap freely. + +## Gatekeeper — validate before storing + +Never store unvalidated traces. The gatekeeper runs three gates: + +1. **Schema validation** — tools exist, required params present, deps valid +2. **Sandbox execution** — dry-run data-flow check with timeout +3. **Semantic dedup** — cosine similarity >= 0.95 → rejected + +```python +from behavioral_memory import GatekeeperPipeline + +gatekeeper = GatekeeperPipeline(store=store, registry=registry) +result = gatekeeper.submit(trace) # validates AND stores if accepted +# result.accepted, result.schema_valid, result.is_duplicate +``` + +## Feedback Loop — learn from thumbs-up (Langfuse v4+) + +Wire your existing feedback endpoint to behavioral memory. When a user +gives thumbs-up, capture the trace and feed it through the gatekeeper. + +### Option A: Direct capture in your feedback handler + +```python +from behavioral_memory import ExecutionTrace, ToolCall, GatekeeperPipeline + +async def on_positive_feedback(run_id: str, user_query: str, tool_calls: list): + trace = ExecutionTrace( + task_description=user_query, + tool_chain=[ + ToolCall( + step_id=f"step_{i+1}", + tool_name=tc["name"], + parameters=tc.get("args", {}), + ) + for i, tc in enumerate(tool_calls) + ], + source="feedback", + metadata={"run_id": run_id}, + ) + result = gatekeeper.submit(trace) + return result.accepted +``` + +### Option B: Poll Langfuse for positively scored traces + +```python +from behavioral_memory import FeedbackPoller, AnnotationHandler, Settings + +settings = Settings( + langfuse_secret_key="sk-lf-...", + langfuse_public_key="pk-lf-...", + langfuse_host="https://us.cloud.langfuse.com", + feedback_score_name="user_feedback", # must match your Langfuse score name + feedback_positive_threshold=1.0, # score >= this = positive +) + +poller = FeedbackPoller(settings=settings) +handler = AnnotationHandler(poller=poller, gatekeeper=gatekeeper) +handler.run_once() # single poll cycle +# handler.run_loop() # continuous background polling +``` + +### Langfuse v4+ compatibility + +The library uses `client.api.trace.list()` and `client.api.scores.list()` +(Langfuse SDK v4 API). If your agent logs scores via `client.create_score()` +or `client.score()`, the poller reads them correctly. The key mapping: + +| Your agent sends | Poller reads | +|---|---| +| `client.create_score(trace_id=run_id, name="user_feedback", value=1)` | `score.name == settings.feedback_score_name` | +| `client.score(trace_id=run_id, name="quality", value=1)` | Same — both APIs write to the same store | + +## Injecting traces into a ReAct agent prompt + +If you don't use `PlanEngine` and want to inject traces into your own prompt: + +```python +from behavioral_memory.planner.prompt import build_prompt, SYSTEM_PROMPT +from behavioral_memory.memory.token_budget import select_traces_within_budget + +traces = select_traces_within_budget(store=store, query=user_query, tool_schemas=schemas) +prompt = build_prompt(query=user_query, traces=traces, tool_schemas=schemas) +# Send SYSTEM_PROMPT as system message, prompt as user message to your LLM +``` + +## Configuration (env vars / .env) + +| Variable | Default | Purpose | +|---|---|---| +| `FEW_SHOT_K` | `3` | Traces to retrieve per query | +| `MAX_PROMPT_TOKENS` | `3500` | Token budget for prompt | +| `SIMILARITY_DEDUP_THRESHOLD` | `0.95` | Reject traces above this cosine similarity | +| `SANDBOX_TIMEOUT_SECONDS` | `30` | Gatekeeper sandbox timeout | +| `VECTOR_STORE_URL` | — | PostgreSQL connection string | +| `VECTOR_STORE_COLLECTION` | `validated_traces` | pgvector collection name | +| `LANGFUSE_SECRET_KEY` | — | For feedback polling | +| `LANGFUSE_PUBLIC_KEY` | — | For feedback polling | +| `LANGFUSE_HOST` | `https://cloud.langfuse.com` | Langfuse instance URL | +| `FEEDBACK_SCORE_NAME` | `quality` | Langfuse score name to watch | +| `FEEDBACK_POSITIVE_THRESHOLD` | `1.0` | Minimum score to accept | + +## Common mistakes + +1. **Forgetting to register tools** — `GatekeeperPipeline` rejects traces + referencing unknown tools. Register all tools BEFORE submitting traces. +2. **Using TraceStore without `[postgres]`** — causes `ImportError`. Use + `InMemoryTraceStore` for dev or install the extra. +3. **Langfuse score name mismatch** — if your agent sends + `name="user_feedback_positive"` but settings say `feedback_score_name="quality"`, + the poller finds nothing. These must match. +4. **pgvector distance vs similarity** — `TraceStore.similarity_score()` already + converts cosine distance to similarity (1 - distance). The 0.95 dedup threshold + works correctly out of the box. + +## For detailed integration examples + +See [integration-examples.md](integration-examples.md) for: +- LangGraph ReAct agent integration +- FastAPI feedback endpoint wiring +- Multi-agent system setup +- Bootstrap script usage + +## Bootstrap script + +Run the bootstrap script to validate your setup: + +```bash +python .cursor/skills/behavioral-memory/scripts/verify_setup.py +``` + +This checks: import works, store initializes, add/search cycle passes, +gatekeeper accepts valid traces, and optionally tests Langfuse connectivity. diff --git a/.cursor/skills/behavioral-memory/integration-examples.md b/.cursor/skills/behavioral-memory/integration-examples.md new file mode 100644 index 0000000..b778246 --- /dev/null +++ b/.cursor/skills/behavioral-memory/integration-examples.md @@ -0,0 +1,220 @@ +# Integration Examples + +Concrete patterns for wiring behavioral-memory into real agents. + +## 1. LangGraph ReAct Agent (create_react_agent / custom StateGraph) + +Add a **pre-planning step** that retrieves traces and injects them into +the system prompt before the ReAct loop starts. + +```python +from behavioral_memory import InMemoryTraceStore, PlanEngine, ToolRegistry, ToolSchema +from langchain_google_genai import ChatGoogleGenerativeAI, GoogleGenerativeAIEmbeddings +from langgraph.prebuilt import create_react_agent + +# --- 1. Your existing agent setup --- +llm = ChatGoogleGenerativeAI(model="gemini-2.5-pro", temperature=0) +embeddings = GoogleGenerativeAIEmbeddings(model="models/gemini-embedding-001") +tools = [your_tool_a, your_tool_b, your_tool_c] + +# --- 2. Add behavioral memory (new code) --- +store = InMemoryTraceStore(embeddings=embeddings) +registry = ToolRegistry() +for t in tools: + registry.register(ToolSchema( + name=t.name, + description=t.description, + parameters_schema=t.args_schema.model_json_schema() if t.args_schema else {}, + )) + +# Seed domain knowledge +from behavioral_memory import ExecutionTrace, ToolCall +store.add(ExecutionTrace( + task_description="Example task that worked well", + tool_chain=[ + ToolCall(step_id="s1", tool_name="your_tool_a", parameters={"query": "example"}), + ToolCall(step_id="s2", tool_name="your_tool_b", parameters={"source_step": "s1"}), + ], + source="seed", +)) + +# --- 3. Build enhanced system prompt --- +from behavioral_memory.memory.token_budget import select_traces_within_budget +from behavioral_memory.planner.prompt import build_prompt + +def get_enhanced_prompt(user_query: str, base_prompt: str) -> str: + traces = select_traces_within_budget( + store=store, query=user_query, tool_schemas=registry.list_tools() + ) + if not traces: + return base_prompt + trace_section = "\n".join( + f"Reference: {t.task_description}\n→ {' → '.join(t.tool_names)}" + for t in traces + ) + return f"{base_prompt}\n\n## Validated Patterns:\n{trace_section}" + +# --- 4. Use with create_react_agent --- +enhanced_prompt = get_enhanced_prompt("user query", "You are a helpful assistant.") +agent = create_react_agent(model=llm, tools=tools, state_modifier=enhanced_prompt) +result = agent.invoke({"messages": [("user", "user query")]}) +``` + +## 2. FastAPI Feedback Endpoint (thumbs-up → behavioral memory) + +Wire into an existing `/feedback` endpoint that sends scores to Langfuse. + +```python +from fastapi import APIRouter +from behavioral_memory import ( + ExecutionTrace, ToolCall, GatekeeperPipeline, ToolRegistry, + InMemoryTraceStore, Settings, +) + +router = APIRouter() + +# Initialize once at startup (reuse across requests) +# In production, use TraceStore with your existing PostgreSQL +store = InMemoryTraceStore(embeddings=your_embeddings) +registry = ToolRegistry() +# ... register your tools ... +gatekeeper = GatekeeperPipeline(store=store, registry=registry) + + +@router.post("/v1/feedback") +async def feedback(run_id: str, score: float, user_query: str, tool_calls: list): + # Your existing Langfuse scoring (keep as-is) + langfuse_client.create_score(trace_id=run_id, name="user_feedback", value=score) + + # NEW: On positive feedback, capture into behavioral memory + if score >= 1.0 and tool_calls: + trace = ExecutionTrace( + task_description=user_query, + tool_chain=[ + ToolCall( + step_id=f"step_{i+1}", + tool_name=tc["name"], + parameters=tc.get("args", {}), + ) + for i, tc in enumerate(tool_calls) + ], + source="feedback", + metadata={"run_id": run_id}, + ) + result = gatekeeper.submit(trace) + logger.info(f"Behavioral memory: {'accepted' if result.accepted else result.rejection_reason}") + + return {"status": "ok"} +``` + +## 3. LangGraph Custom StateGraph with Behavioral Memory Node + +Add a dedicated `retrieve_traces` node to your existing graph. + +```python +from langgraph.graph import StateGraph, START, END, MessagesState +from behavioral_memory.memory.token_budget import select_traces_within_budget + +# Your existing nodes +def call_model(state): ... +def should_continue(state): ... + +# NEW: behavioral memory retrieval node +def retrieve_traces(state: MessagesState): + user_msg = state["messages"][-1].content + traces = select_traces_within_budget( + store=store, query=user_msg, tool_schemas=registry.list_tools() + ) + if traces: + context = "Validated patterns from past successful executions:\n" + for t in traces: + context += f"- {t.task_description}: {' → '.join(t.tool_names)}\n" + from langchain_core.messages import SystemMessage + return {"messages": [SystemMessage(content=context)]} + return {"messages": []} + +# Wire into graph +wf = StateGraph(MessagesState) +wf.add_node("retrieve_memory", retrieve_traces) # NEW +wf.add_node("LLM", call_model) +wf.add_node("tools", tool_node) +wf.add_edge(START, "retrieve_memory") # NEW: memory first +wf.add_edge("retrieve_memory", "LLM") # then LLM +wf.add_conditional_edges("LLM", should_continue) +wf.add_edge("tools", "LLM") +agent = wf.compile() +``` + +## 4. Production Setup with pgvector (persistent across deploys) + +```python +from behavioral_memory import TraceStore, Settings + +settings = Settings( + vector_store_url="postgresql+psycopg://user:pass@db-host:5432/mydb", + vector_store_collection="validated_traces", # stable name + similarity_dedup_threshold=0.95, + few_shot_k=3, + max_prompt_tokens=3500, +) + +store = TraceStore( + embeddings=your_embeddings, + connection_url=settings.vector_store_url, + collection_name=settings.vector_store_collection, + settings=settings, +) + +# On first deploy: seed traces +if store.count() == 0: + store.add_bulk(your_seed_traces) + +# On subsequent deploys: traces persist, no re-seeding needed +# Dedup gate (cosine >= 0.95) prevents accidental duplicates +``` + +## 5. Background Feedback Poller (Langfuse v4+) + +Run as a background task or separate service. + +```python +import asyncio +from behavioral_memory import FeedbackPoller, AnnotationHandler, Settings + +settings = Settings( + langfuse_secret_key="sk-lf-...", + langfuse_public_key="pk-lf-...", + langfuse_host="https://us.cloud.langfuse.com", + feedback_score_name="user_feedback", # MUST match what your agent sends + feedback_positive_threshold=1.0, + feedback_poll_interval=300, # poll every 5 minutes +) + +poller = FeedbackPoller(settings=settings) +handler = AnnotationHandler(poller=poller, gatekeeper=gatekeeper) + +# As a background task in FastAPI +@app.on_event("startup") +async def start_feedback_loop(): + asyncio.create_task(asyncio.to_thread(handler.run_loop)) +``` + +## 6. Using PlanEngine Directly (full planning, not just retrieval) + +When you want behavioral memory to generate a complete structured plan: + +```python +from behavioral_memory import PlanEngine + +engine = PlanEngine(llm=your_llm, store=store, registry=registry) + +# With memory (retrieves similar traces automatically) +plan = engine.generate(query="Build a revenue dashboard") + +# Without memory (zero-shot baseline) +plan_zs = engine.generate_zero_shot(query="Build a revenue dashboard", tool_schemas=schemas) + +# Compare +print(f"With memory: {len(plan.steps)} steps, {len(plan.retrieved_traces)} traces used") +print(f"Zero-shot: {len(plan_zs.steps)} steps") +``` diff --git a/.cursor/skills/behavioral-memory/scripts/verify_setup.py b/.cursor/skills/behavioral-memory/scripts/verify_setup.py new file mode 100644 index 0000000..d05f299 --- /dev/null +++ b/.cursor/skills/behavioral-memory/scripts/verify_setup.py @@ -0,0 +1,189 @@ +#!/usr/bin/env python3 +"""Verify that behavioral-memory is installed and working correctly. + +Run: python .cursor/skills/behavioral-memory/scripts/verify_setup.py + +Checks: + 1. Import works + 2. InMemoryTraceStore initializes (needs an embeddings model) + 3. Add / search round-trip works + 4. GatekeeperPipeline accepts a valid trace + 5. (Optional) pgvector TraceStore connects + 6. (Optional) Langfuse connectivity +""" +from __future__ import annotations + +import os +import sys + +PASS = "\033[92m PASS \033[0m" +FAIL = "\033[91m FAIL \033[0m" +SKIP = "\033[93m SKIP \033[0m" + + +def check(label: str, fn, skip_if=None): + if skip_if: + print(f" [{SKIP}] {label} — {skip_if}") + return True + try: + fn() + print(f" [{PASS}] {label}") + return True + except Exception as e: + print(f" [{FAIL}] {label}: {e}") + return False + + +def main(): + print("\n=== behavioral-memory setup verification ===\n") + all_ok = True + + # 1. Import + def _import(): + import behavioral_memory # noqa: F401 + assert hasattr(behavioral_memory, "__version__") + + all_ok &= check("Import behavioral_memory", _import) + + # 2. Check embeddings model availability + embeddings = None + llm = None + + def _init_models(): + nonlocal embeddings, llm + api_key = os.environ.get("GOOGLE_API_KEY", "") + openai_key = os.environ.get("OPENAI_API_KEY", "") + + if api_key: + from langchain_google_genai import GoogleGenerativeAIEmbeddings, ChatGoogleGenerativeAI + embeddings = GoogleGenerativeAIEmbeddings(model="models/gemini-embedding-001") + llm = ChatGoogleGenerativeAI(model="gemini-2.5-flash", temperature=0) + elif openai_key: + from langchain_openai import OpenAIEmbeddings, ChatOpenAI + embeddings = OpenAIEmbeddings() + llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) + else: + raise RuntimeError( + "Set GOOGLE_API_KEY or OPENAI_API_KEY to run full checks" + ) + + all_ok &= check("Initialize embeddings + LLM", _init_models) + + if embeddings is None: + print("\n Skipping remaining checks (no embeddings model).") + sys.exit(1) + + # 3. InMemoryTraceStore round-trip + store = None + + def _store_roundtrip(): + nonlocal store + from behavioral_memory import InMemoryTraceStore, ExecutionTrace, ToolCall + store = InMemoryTraceStore(embeddings=embeddings) + store.add(ExecutionTrace( + task_description="fetch customer records from CRM", + tool_chain=[ + ToolCall(step_id="s1", tool_name="crm_search", parameters={"query": "customer"}), + ], + source="seed", + )) + assert store.count() == 1 + results = store.search("find customers", k=1) + assert len(results) == 1 + assert results[0][1] > 0.5 # similarity should be high + + all_ok &= check("InMemoryTraceStore add + search", _store_roundtrip) + + # 4. GatekeeperPipeline validation + def _gatekeeper(): + from behavioral_memory import ( + GatekeeperPipeline, ToolRegistry, ToolSchema, + ExecutionTrace, ToolCall, + ) + reg = ToolRegistry() + reg.register(ToolSchema( + name="crm_search", + description="Search CRM", + parameters_schema={"type": "object", "properties": {"query": {"type": "string"}}}, + required_params=["query"], + )) + gk = GatekeeperPipeline(store=store, registry=reg) + trace = ExecutionTrace( + task_description="look up a customer in CRM", + tool_chain=[ToolCall(step_id="s1", tool_name="crm_search", parameters={"query": "test"})], + source="seed", + ) + result = gk.submit(trace) + assert result.accepted or result.is_duplicate, f"Unexpected rejection: {result.rejection_reason}" + + all_ok &= check("GatekeeperPipeline submit", _gatekeeper) + + # 5. PlanEngine generate + def _plan_engine(): + from behavioral_memory import PlanEngine, ToolRegistry, ToolSchema + reg = ToolRegistry() + reg.register(ToolSchema( + name="crm_search", description="Search CRM", + parameters_schema={"type": "object", "properties": {"query": {"type": "string"}}}, + required_params=["query"], + )) + reg.register(ToolSchema( + name="send_email", description="Send email", + parameters_schema={"type": "object", "properties": {"to": {"type": "string"}, "body": {"type": "string"}}}, + required_params=["to", "body"], + )) + engine = PlanEngine(llm=llm, store=store, registry=reg) + plan = engine.generate("send a follow-up email to the customer") + assert len(plan.steps) > 0, "Plan has no steps" + + all_ok &= check("PlanEngine generate", _plan_engine) + + # 6. pgvector TraceStore (optional) + pg_url = os.environ.get("VECTOR_STORE_URL", "") + + def _pgvector(): + from behavioral_memory import TraceStore + pg_store = TraceStore( + embeddings=embeddings, + connection_url=pg_url, + collection_name="verify_test", + ) + _ = pg_store.count() + + all_ok &= check( + "pgvector TraceStore connect", + _pgvector, + skip_if=None if pg_url else "VECTOR_STORE_URL not set", + ) + + # 7. Langfuse (optional) + lf_secret = os.environ.get("LANGFUSE_SECRET_KEY", "") + lf_public = os.environ.get("LANGFUSE_PUBLIC_KEY", "") + + def _langfuse(): + from behavioral_memory import FeedbackPoller, Settings + settings = Settings( + langfuse_secret_key=lf_secret, + langfuse_public_key=lf_public, + ) + poller = FeedbackPoller(settings=settings) + assert poller.client is not None, "Langfuse client failed to initialize" + + all_ok &= check( + "Langfuse connectivity", + _langfuse, + skip_if=None if (lf_secret and lf_public) else "LANGFUSE_SECRET_KEY/PUBLIC_KEY not set", + ) + + # Summary + print() + if all_ok: + print(" All checks passed. behavioral-memory is ready to use.") + else: + print(" Some checks failed. Review errors above.") + print() + sys.exit(0 if all_ok else 1) + + +if __name__ == "__main__": + main() diff --git a/README.md b/README.md index fd4fe36..346ec3e 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,19 @@ A reference LangGraph agent is included at `agent/` for demo purposes. --- +## Cursor Agent Skill + +This repo ships a [Cursor Agent Skill](.cursor/skills/behavioral-memory/) for guided integration. Open this repo in Cursor and type `/behavioral-memory` in the Agent chat to invoke the skill — it walks through store setup, seed traces, feedback loops, Langfuse v4 wiring, and pgvector persistence. + +```bash +# Verify your setup after following the skill +python .cursor/skills/behavioral-memory/scripts/verify_setup.py +``` + +See [integration-examples.md](.cursor/skills/behavioral-memory/integration-examples.md) for LangGraph, FastAPI, and production patterns. + +--- + ## Development ```bash