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
40 changes: 30 additions & 10 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
# === LLM & Embeddings ===
# Only needed if using the reference agent with Gemini (default)
# =============================================
# Behavioral Memory — Environment Configuration
# =============================================
# Copy this file to .env and fill in your values:
# cp .env.example .env
#
# Or use the interactive setup:
# behavioral-memory setup

# === LLM & Embeddings (required for agent/benchmark) ===
# Get a key from: https://aistudio.google.com/apikey
GOOGLE_API_KEY=your-google-api-key-here

# === PostgreSQL + pgvector ===
# Model to use (optional, defaults in code)
# GEMINI_MODEL=gemini-2.5-flash

# === PostgreSQL + pgvector (optional — InMemoryTraceStore works without it) ===
# If using Docker/Podman compose:
# VECTOR_STORE_URL=postgresql+psycopg://behavioral_memory:behavioral_memory@localhost:5432/behavioral_memory
# If using your own PostgreSQL:
VECTOR_STORE_URL=postgresql+psycopg://user:password@localhost:5432/behavioral_memory
VECTOR_STORE_COLLECTION=validated_traces

# === Langfuse (optional but recommended) ===
# Set to "true" to use PostgreSQL instead of in-memory store
USE_POSTGRES=false

# === Langfuse — Observability & Feedback Loop ===
# Sign up free: https://cloud.langfuse.com
# Then: Settings → API Keys → Create
LANGFUSE_SECRET_KEY=sk-lf-...
LANGFUSE_PUBLIC_KEY=pk-lf-...
LANGFUSE_HOST=https://cloud.langfuse.com

# === Framework Tuning ===
FEW_SHOT_K=3
MAX_PROMPT_TOKENS=3500
SIMILARITY_DEDUP_THRESHOLD=0.95
FEW_SHOT_K=3 # Number of traces to retrieve per query
MAX_PROMPT_TOKENS=3500 # Token budget for the prompt
SIMILARITY_DEDUP_THRESHOLD=0.95 # Cosine threshold for deduplication (Section III.E.3)

# === Feedback Loop ===
FEEDBACK_SCORE_NAME=quality
FEEDBACK_POSITIVE_THRESHOLD=1.0
FEEDBACK_POLL_INTERVAL=60
FEEDBACK_SCORE_NAME=quality # Langfuse score name to watch
FEEDBACK_POSITIVE_THRESHOLD=1.0 # Minimum score for positive feedback
FEEDBACK_POLL_INTERVAL=60 # Seconds between polling Langfuse
20 changes: 20 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
## Summary

<!-- What does this PR do? Why is it needed? -->

## Changes

<!-- List the key changes, e.g. -->
<!-- - Added X to Y -->
<!-- - Fixed Z in W -->

## Test Plan

- [ ] `make lint` passes
- [ ] `make typecheck` passes
- [ ] `make test` passes
- [ ] Tested locally with `make validate`

## Related Issues

<!-- Link any related issues, e.g. Fixes #123 -->
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ jobs:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v4
- run: uv sync --extra dev
- run: uv run ruff check src/ tests/
- run: uv run ruff format --check src/ tests/
- run: uv run ruff check src/ tests/ agent/ examples/ server.py
- run: uv run ruff format --check src/ tests/ agent/ examples/ server.py

typecheck:
runs-on: ubuntu-latest
Expand All @@ -35,4 +35,4 @@ jobs:
with:
python-version: ${{ matrix.python-version }}
- run: uv sync --extra dev --extra eval
- run: uv run pytest tests/unit/ -v
- run: uv run pytest tests/ -v --tb=short
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ htmlcov/
Thumbs.db

uv.lock

benchmark_results.json
.chainlit/
chainlit.md
18 changes: 11 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

All notable changes to this project will be documented in this file.

## [0.1.0] - 2025-XX-XX
## [0.1.0] - 2026-05-17

### Added

- Initial release of behavioral-memory framework
- Three-layer architecture: Behavioral Layer, Tool Layer, Executive Layer
- TraceStore with pgvector for semantic retrieval of execution traces
- GatekeeperPipeline: schema validation, sandboxed execution, semantic dedup
- TraceStore (PostgreSQL + pgvector) and InMemoryTraceStore (zero-infra) for semantic retrieval
- GatekeeperPipeline: schema validation, sandboxed execution, semantic dedup (cosine 0.95 threshold)
- Langfuse integration: trace logging, feedback polling, annotation handler
- PlanEngine: model-agnostic plan generation with any LangChain chat model
- Reference LangGraph 1.x agent with full paper implementation
- 30-task benchmark with 7 tools and 12 seed traces
- PlanEngine: model-agnostic plan generation with any LangChain-compatible chat model
- Reference LangGraph 1.x agent with interactive mode and LangGraph server support
- 30-task benchmark with 7 MCP tools and 12 seed traces
- Evaluation metrics: TSA, PV, PCR, ESA with bootstrap CI and McNemar's test
- CLI for benchmarks, memory management, and dataset inspection
- Gatekeeper ablation study script (Section IV.D.5 — memory poisoning experiment)
- CLI for benchmarks, demos, memory management, and dataset inspection
- Full CI/CD with GitHub Actions (lint, typecheck, test on Python 3.11/3.12/3.13)
- Makefile for common development tasks
- PEP 561 `py.typed` marker for downstream type checking
- Apache 2.0 license
47 changes: 47 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
.PHONY: help install dev lint format typecheck test test-unit test-e2e demo benchmark validate clean

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'

install: ## Install core package
uv sync

dev: ## Install with all dev dependencies
uv sync --extra dev --extra eval --extra agent --extra server
uv run pre-commit install

lint: ## Run linter
uv run ruff check src/ tests/ agent/ examples/ server.py

format: ## Format code
uv run ruff format src/ tests/ agent/ examples/ server.py

typecheck: ## Run mypy type checker
uv run mypy src/

test: ## Run all tests
uv run pytest tests/ -v --tb=short

test-unit: ## Run unit tests only
uv run pytest tests/unit/ -v

test-e2e: ## Run end-to-end tests only
uv run pytest tests/e2e/ -v

demo: ## Run offline demo (no API keys needed)
uv run behavioral-memory demo

benchmark: ## Run live benchmark (requires GOOGLE_API_KEY)
uv run python examples/run_live_benchmark.py

ablation: ## Run gatekeeper ablation study
uv run python examples/gatekeeper_ablation.py --verbose

validate: ## Validate full pipeline (no API keys needed)
uv run python examples/validate_pipeline.py

clean: ## Clean build artifacts
rm -rf dist/ build/ *.egg-info .mypy_cache .pytest_cache .ruff_cache
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true

ci: lint format typecheck test ## Run all CI checks locally
62 changes: 51 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,17 +243,19 @@ All **30 checks** pass with zero external dependencies.
### Install with uv (recommended)

```bash
uv add behavioral-memory # core framework
uv add behavioral-memory[agent] # + reference LangGraph agent
uv add behavioral-memory[eval] # + evaluation/statistics
uv add behavioral-memory[all] # everything
uv add behavioral-memory # core framework (no PostgreSQL needed)
uv add behavioral-memory[agent] # + reference LangGraph agent
uv add behavioral-memory[eval] # + evaluation/statistics (scipy)
uv add behavioral-memory[postgres] # + PostgreSQL/pgvector store
uv add behavioral-memory[all] # everything
```

### Install with pip

```bash
pip install behavioral-memory
pip install behavioral-memory[agent,eval]
pip install behavioral-memory[postgres] # only if using PostgreSQL
```

### Environment Setup
Expand Down Expand Up @@ -298,10 +300,12 @@ behavioral-memory/
│ ├── integration/ # 3 integration tests
│ └── e2e/ # 40 end-to-end tests
├── examples/
│ ├── validate_pipeline.py # Full pipeline validation (no API keys)
│ ├── run_live_benchmark.py # Real benchmark (needs API key)
│ └── run_benchmark.py # Benchmark with PostgreSQL
└── .github/workflows/ # CI/CD
│ ├── validate_pipeline.py # Full pipeline validation (no API keys)
│ ├── run_live_benchmark.py # Real benchmark (needs API key)
│ ├── gatekeeper_ablation.py # Gatekeeper ablation study (Section IV.D.5)
│ └── run_benchmark.py # Benchmark with PostgreSQL
├── Makefile # Common dev tasks (make lint, make test, etc.)
└── .github/workflows/ # CI/CD (lint, typecheck, test on 3.11/3.12/3.13)
```

### Store Options
Expand Down Expand Up @@ -370,9 +374,45 @@ python examples/validate_pipeline.py # 30 checks, 0 external deps
### Linting and type checking

```bash
ruff check src/ tests/ agent/
ruff format src/ tests/ agent/
mypy src/
make lint # or: ruff check src/ tests/ agent/ examples/ server.py
make format # or: ruff format src/ tests/ agent/ examples/ server.py
make typecheck # or: mypy src/
```

---

## Gatekeeper Ablation Study (Section IV.D.5)

Tests the critical role of the gatekeeper by injecting 8 deliberately poisoned traces
(wrong conventions, broken dependencies, incorrect tools) into memory:

```bash
python examples/gatekeeper_ablation.py --verbose
```

Three conditions are compared:
1. **Baseline** — only valid seed traces (gatekeeper ON)
2. **Poisoned** — bad traces injected (gatekeeper OFF)
3. **Recovered** — gatekeeper re-enabled, bad traces filtered out

The script shows how poisoned traces degrade plan quality (PCR drops) and how the
gatekeeper catches and rejects them. Recovery restores baseline performance.

---

## Development

```bash
# Using the Makefile (recommended)
make dev # Install all dev dependencies + pre-commit hooks
make lint # Run ruff linter
make format # Auto-format code
make typecheck # Run mypy
make test # Run all 104 tests
make ci # Run all CI checks locally
make ablation # Run gatekeeper ablation study
make validate # Pipeline validation (no API keys)
make demo # Offline demo
```

---
Expand Down
34 changes: 21 additions & 13 deletions agent/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ def create_agent(model: str = "gemini-2.5-pro", use_postgres: bool = False):

if use_postgres:
from behavioral_memory.memory.store import TraceStore

store = TraceStore(embeddings=embeddings, settings=settings)
else:
from behavioral_memory.memory.in_memory_store import InMemoryTraceStore

store = InMemoryTraceStore(embeddings=embeddings, settings=settings)

registry = ToolRegistry()
Expand All @@ -50,7 +52,10 @@ def create_agent(model: str = "gemini-2.5-pro", use_postgres: bool = False):
store.add_bulk(seed_traces)

graph = build_agent_graph(
llm=llm, store=store, registry=registry, settings=settings,
llm=llm,
store=store,
registry=registry,
settings=settings,
)

return graph, store, registry, tracer, settings
Expand Down Expand Up @@ -102,17 +107,19 @@ def run_single(query: str, model: str = "gemini-2.5-pro", verbose: bool = True)

def run_interactive(model: str = "gemini-2.5-pro") -> None:
"""Interactive REPL — type queries, see plans, compare with/without memory."""
console.print(Panel.fit(
"[bold]Behavioral Memory Agent — Interactive Mode[/bold]\n\n"
f"Model: {model}\n"
"Type a query to generate a plan. The agent retrieves relevant\n"
"traces from behavioral memory to guide its planning.\n\n"
"Special commands:\n"
" /compare <query> — run with AND without memory, show difference\n"
" /memory — show what's in behavioral memory\n"
" /quit — exit",
title="Interactive Agent",
))
console.print(
Panel.fit(
"[bold]Behavioral Memory Agent — Interactive Mode[/bold]\n\n"
f"Model: {model}\n"
"Type a query to generate a plan. The agent retrieves relevant\n"
"traces from behavioral memory to guide its planning.\n\n"
"Special commands:\n"
" /compare <query> — run with AND without memory, show difference\n"
" /memory — show what's in behavioral memory\n"
" /quit — exit",
title="Interactive Agent",
)
)

graph, store, registry, tracer, settings = create_agent(model=model)
compiled = graph.compile()
Expand All @@ -132,6 +139,7 @@ def run_interactive(model: str = "gemini-2.5-pro") -> None:

if query.startswith("/memory"):
from behavioral_memory.evaluation.seed_traces import get_seed_traces

for trace in get_seed_traces():
tools = " → ".join(trace.tool_names)
console.print(f" [cyan]{trace.task_description[:60]}[/cyan]")
Expand Down Expand Up @@ -167,7 +175,7 @@ def _run_comparison(compiled, query, store, registry, settings, tracer):
from behavioral_memory.planner.prompt import build_prompt
from behavioral_memory.tools.mock_tools import get_tool_schemas

console.print(f"\n[bold]Comparing: \"{query}\"[/bold]\n")
console.print(f'\n[bold]Comparing: "{query}"[/bold]\n')

result_with = compiled.invoke({"query": query})
plan_with = result_with.get("plan")
Expand Down
14 changes: 8 additions & 6 deletions agent/nodes/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ def execute_tools(state: AgentState) -> dict:

results: list[dict[str, Any]] = []
for step in plan.steps:
results.append({
"step_id": step.step_id,
"tool_name": step.tool_name,
"status": "executed_stub",
"parameters": step.parameters,
})
results.append(
{
"step_id": step.step_id,
"tool_name": step.tool_name,
"status": "executed_stub",
"parameters": step.parameters,
}
)

return {"execution_results": results}
20 changes: 20 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
services:
pgvector:
image: pgvector/pgvector:pg16
container_name: behavioral-memory-pgvector
environment:
POSTGRES_USER: behavioral_memory
POSTGRES_PASSWORD: behavioral_memory
POSTGRES_DB: behavioral_memory
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U behavioral_memory"]
interval: 5s
timeout: 5s
retries: 5

volumes:
pgdata:
Loading
Loading