Skip to content
Open
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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ print(readiness.graph, readiness.vector, readiness.hybrid)

Use the Runtime API URL with the SDK. Do not use a direct or pooled PostgreSQL connection string.

## Agent memory example

For a runnable end-to-end pattern over the Context API, see
[examples/agent_memory/README.md](examples/agent_memory/README.md). It mirrors
the pgContext SQL walkthrough for durable decision memory, hybrid retrieval,
and LLM context-pack assembly.

## Choose a retrieval method

| Need | Method |
Expand Down
164 changes: 164 additions & 0 deletions examples/agent_memory/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Agent Memory Example (Polygres SDK)

This example shows how an application retrieves **durable agent memory** through
the Polygres Runtime **Context API** (`project.context`). It mirrors the SQL
walkthrough in [Evokoa/pgContext](https://github.com/Evokoa/pgContext):
`examples/sql/06_agent_memory.sql`.

## What it demonstrates

- Scoped memory retrieval for one tenant and user
- Filtered dense search over a Context collection
- Hybrid dense + full-text retrieval with `text_hybrid`
- Application-side context-pack assembly for LLM prompts

The demo uses fixture 4-dimensional embeddings so you can run it without calling
an external embedding model. Replace the vectors in your application with outputs
from your production embedding model.

## Prerequisites

- Python 3.10+
- A [Polygres](https://polygres.com) project with Context enabled
- [polygres-cli](https://github.com/Evokoa/polygres-cli) for one-time setup

```bash
pip install polygres-cli polygres-sdk
polygres login
polygres projects use <your-project>
```

Create a Project API key from the project **Connect** page and export:

```bash
export POLYGRES_API_KEY=poly_live_...
export POLYGRES_RUNTIME_URL=https://...
```

## One-time project setup

### 1. Load schema and seed data

From this repository root:

```bash
polygres db psql < examples/agent_memory/schema.sql
```

This creates `agent_users`, `agent_sessions`, `agent_messages`, and
`agent_decisions` with the same fixture rows as the pgContext SQL example.

### 2. Preflight and create the Context collection

Inspect capabilities:

```bash
polygres --json context capabilities
```

Preflight the collection definition:

```bash
polygres --json context sources preflight --file examples/agent_memory/collection.json
```

Create the collection (requires explicit approval in agent-driven workflows):

```bash
polygres context collections create agent_memory_decisions \
--source existing \
--schema public \
--table agent_decisions \
--source-key-column id \
--vector-column embedding \
--dimensions 4 \
--metric cosine \
--text-column body \
--result-column summary \
--result-column body \
--result-column category \
--result-column decided_at \
--result-column user_id \
--result-column tenant_id \
--result-column session_id \
--filter-column tenant_id \
--filter-column user_id \
--filter-column category
```

Synchronize catalog points after loading seed rows. Use the collection UUID
returned by the create command (not the collection name):

```bash
COLLECTION_ID=<uuid-from-create-output>

polygres context points upsert "$COLLECTION_ID" \
d-billing-refund d-arch-postgres d-onboarding-delay d-other-billing
```

Or reconcile every row in the source table:

```bash
polygres context points reconcile "$COLLECTION_ID"
```

Exact CLI flag names may vary slightly by `polygres-cli` version. Use
`polygres context collections create --help` if a flag changed.

```bash
python examples/agent_memory/dry_run.py
```

This validates `collection.json` against the Context request schema and exercises
`demo.py` against mocked Runtime responses (no API key required).

## Run the demo

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r examples/agent_memory/requirements.txt
pip install -e .

python examples/agent_memory/demo.py
```

Optional override:

```bash
export POLYGRES_AGENT_MEMORY_COLLECTION=agent_memory_decisions
```

## Expected output

When setup succeeded, the script prints:

1. Context capability flags (`dense_search`, `text_hybrid`)
2. Filtered dense search returning `d-billing-refund` for tenant `acme` and user `u-alice`
3. Hybrid search ranking the billing decision highest for `billing refund`
4. A scoped context pack with summary and body fields

## Review checklist

- [ ] `schema.sql` loads without errors on the project database
- [ ] Collection creation completes and verification passes
- [ ] `demo.py` returns billing memory for the scoped user
- [ ] No API keys committed to git

## Related material

- pgContext SQL example: `Evokoa/pgContext/examples/sql/06_agent_memory.sql`
- SDK Context reference: `docs/reference-v1.md`
- Agent skill guidance: `Evokoa/polygres-skills` → `polygres-sdk/references/context.md`

## Troubleshooting

| Symptom | Likely cause |
|---|---|
| `Dense search unavailable` | Context collection missing or not verified |
| Empty results | Points not upserted after seed load |
| Dimension mismatch | Collection expects 4-D cosine vectors |
| `401` / auth errors | Invalid or expired API key |
| Hybrid skipped | `text_hybrid` capability blocked on project |

Never commit `POLYGRES_API_KEY` or paste it into logs.
29 changes: 29 additions & 0 deletions examples/agent_memory/collection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "agent_memory_decisions",
"source": {
"mode": "existing",
"schema_name": "public",
"table_name": "agent_decisions",
"source_key_column": "id"
},
"vector": {
"column_name": "embedding",
"dimensions": 4,
"metric": "cosine"
},
"text_column": "body",
"result_columns": [
"summary",
"body",
"category",
"decided_at",
"user_id",
"tenant_id",
"session_id"
],
"filter_columns": [
"tenant_id",
"user_id",
"category"
]
}
144 changes: 144 additions & 0 deletions examples/agent_memory/demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
"""Retrieve durable agent memory through the Polygres Context API.

Companion to the pgContext SQL example in Evokoa/pgContext:
examples/sql/06_agent_memory.sql

Requires:
- POLYGRES_API_KEY
- POLYGRES_RUNTIME_URL
- A Context collection named agent_memory_decisions (see README.md)
"""

from __future__ import annotations

import os
import sys
from typing import Any

from polygres import Polygres

COLLECTION = os.environ.get("POLYGRES_AGENT_MEMORY_COLLECTION", "agent_memory_decisions")
QUERY_EMBEDDING = [0.95, 0.05, 0.0, 0.0]
HYBRID_QUERY = "billing refund"
TENANT_ID = "acme"
USER_ID = "u-alice"
CATEGORY = "billing"


def _require_env(name: str) -> str:
value = os.environ.get(name)
if not value:
print(f"Missing required environment variable: {name}", file=sys.stderr)
sys.exit(2)
return value


def _source_key(result: Any) -> str:
source = getattr(result, "source", None)
if source is not None and getattr(source, "id", None):
return str(source.id)
properties = getattr(result, "properties", {}) or {}
for key in ("id", "source_key"):
if key in properties:
return str(properties[key])
return str(getattr(result, "point_id", "unknown"))


def _build_context_pack(results: list[Any], *, tenant_id: str, user_id: str) -> list[dict[str, Any]]:
pack: list[dict[str, Any]] = []
for result in results:
properties = dict(getattr(result, "properties", {}) or {})
if properties.get("tenant_id") not in (None, tenant_id):
continue
if properties.get("user_id") not in (None, user_id):
continue
pack.append(
{
"rank": getattr(result, "rank", len(pack) + 1),
"score": getattr(result, "score", None),
"decision_id": _source_key(result),
"summary": properties.get("summary"),
"body": properties.get("body"),
"category": properties.get("category"),
"decided_at": properties.get("decided_at"),
"session_id": properties.get("session_id"),
}
)
return pack


def main() -> None:
api_key = _require_env("POLYGRES_API_KEY")
runtime_url = _require_env("POLYGRES_RUNTIME_URL")

client = Polygres(api_key=api_key, runtime_url=runtime_url)
context = client.project().context

capabilities = context.get_capabilities()
print("Context capabilities:")
print(f" setup={capabilities.setup}")
print(f" dense_search={capabilities.dense_search}")
print(f" text_hybrid={capabilities.text_hybrid}")

if not capabilities.dense_search:
blocker = capabilities.dense_search_blocker or "unknown"
message = capabilities.dense_search_blocker_message or ""
print(f"Dense search unavailable ({blocker}): {message}", file=sys.stderr)
sys.exit(1)

memory_filter = {
"must": [
{"key": "tenant_id", "match": TENANT_ID},
{"key": "user_id", "match": USER_ID},
{"key": "category", "match": CATEGORY},
]
}

print("\nFiltered dense memory search:")
dense = context.search(
COLLECTION,
QUERY_EMBEDDING,
filter=memory_filter,
limit=5,
)
for result in dense.results:
print(f" {_source_key(result):<18} score={result.score:.6f}")

hybrid_results = dense.results
if capabilities.text_hybrid:
print("\nHybrid memory search (dense + full-text):")
hybrid = context.text_hybrid(
COLLECTION,
QUERY_EMBEDDING,
query=HYBRID_QUERY,
limit=5,
)
hybrid_results = hybrid.results
for result in hybrid.results:
print(f" {_source_key(result):<18} score={result.score:.6f}")
else:
blocker = capabilities.text_hybrid_blocker or "unknown"
print(f"\nSkipping hybrid search ({blocker}).")

print("\nContext pack for downstream LLM prompt assembly:")
context_pack = _build_context_pack(
list(hybrid_results),
tenant_id=TENANT_ID,
user_id=USER_ID,
)
if not context_pack:
print(" No scoped memory rows returned.", file=sys.stderr)
sys.exit(1)

for item in context_pack:
print(
f" #{item['rank']} {item['decision_id']} "
f"({item['category']}) score={item['score']:.6f}"
)
print(f" summary: {item['summary']}")
print(f" body: {item['body']}")


if __name__ == "__main__":
main()
Loading