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
83 changes: 83 additions & 0 deletions examples/memory-showcase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Memory Showcase

This example demonstrates kagent long-term memory with two interactions:

1. The agent stores a durable fact with the `save_memory` tool.
2. A later interaction asks for that fact from a new A2A context, and the agent retrieves it with `load_memory`.

The demo fact is:

```text
In the memory showcase, my release codename is blue-sunrise.
```

## How It Works

The manifest creates two `ModelConfig` resources and one declarative `Agent`:

- `memory-showcase-chat` uses `gpt-4.1-mini` for normal chat.
- `memory-showcase-embedding` uses `text-embedding-3-small` for memory embeddings.
- `memory-showcase-agent` enables memory with:

```yaml
memory:
modelConfig: memory-showcase-embedding
ttlDays: 15
```

When memory is configured, kagent adds memory tools to the ADK agent. The demo prompt tells the agent to use `save_memory` when the user asks it to remember a fact, and to call `load_memory` before answering questions about the saved release codename.

## Persistence Model

kagent stores long-term memory through the kagent API in the backend `memory` table, backed by pgvector search. Each memory row includes the agent name, user ID, text content, embedding vector, metadata, creation time, expiration time, and access count.

Memory search is scoped by agent name and user ID. This demo sends a stable `X-User-ID` header so the second interaction can retrieve memory saved by the first interaction, even though the script uses a separate A2A context ID for the second turn.

`ttlDays` controls how long a memory remains valid before it is eligible for pruning. This example sets `ttlDays: 15`; omitting it or setting it to zero uses the server default of 15 days. Expired memories with low access counts are deleted by pruning. Expired memories with an access count of at least 10 are considered popular, have their TTL extended by 15 days, and have their access count reset.

## Run The Demo

Prerequisites:

- A running kagent stack.
- The `kagent-openai` secret in the `kagent` namespace with `OPENAI_API_KEY`.
- Local `kubectl`, `curl`, and `jq`.
- Access to the kagent controller API. If running locally, port-forward it:

```bash
kubectl port-forward -n kagent deploy/kagent-controller 8083:8083
```

Apply the demo resources:

```bash
kubectl apply -f examples/memory-showcase/memory-showcase.yaml
kubectl wait --for=condition=Ready agent/memory-showcase-agent -n kagent --timeout=2m
```

Run the scripted interaction:

```bash
KAGENT_URL=http://localhost:8083 ./examples/memory-showcase/run-demo.sh
```

The output shows:

- Memory before turn 1, usually empty for `memory-showcase-user@example.com`.
- Turn 1 asking the agent to remember `blue-sunrise`.
- Memory after turn 1, showing the stored fact.
- Turn 2 asking from a separate A2A context and receiving `blue-sunrise` from memory.

By default the script deletes memories for `memory-showcase-agent` and the demo `USER_ID` before it starts, then exits nonzero if the saved memory or second response does not include `blue-sunrise`. Set `RESET_MEMORY=false` to keep existing memories for that agent/user.

For trusted-proxy auth mode, provide a valid bearer token and set `USER_ID` to the token subject:

```bash
AUTH_HEADER="Bearer ${TOKEN}" USER_ID="you@example.com" ./examples/memory-showcase/run-demo.sh
```

Clean up:

```bash
kubectl delete -f examples/memory-showcase/memory-showcase.yaml
```
61 changes: 61 additions & 0 deletions examples/memory-showcase/memory-showcase.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
apiVersion: kagent.dev/v1alpha2
kind: ModelConfig
metadata:
name: memory-showcase-chat
namespace: kagent
spec:
provider: OpenAI
model: gpt-4.1-mini
apiKeySecret: kagent-openai
apiKeySecretKey: OPENAI_API_KEY
openAI:
temperature: "0.2"
maxTokens: 1024
defaultHeaders:
User-Agent: kagent/1.0
---
apiVersion: kagent.dev/v1alpha2
kind: ModelConfig
metadata:
name: memory-showcase-embedding
namespace: kagent
spec:
provider: OpenAI
model: text-embedding-3-small
apiKeySecret: kagent-openai
apiKeySecretKey: OPENAI_API_KEY
defaultHeaders:
User-Agent: kagent/1.0
---
apiVersion: kagent.dev/v1alpha2
kind: Agent
metadata:
name: memory-showcase-agent
namespace: kagent
spec:
description: Demonstrates saving and retrieving long-term memory.
type: Declarative
declarative:
modelConfig: memory-showcase-chat
stream: true
systemMessage: |-
You are a focused demo agent for kagent long-term memory.
When the user asks you to remember a fact, call save_memory with only the durable fact to store.
After save_memory succeeds, confirm the fact was saved.
When the user asks for the memory showcase release codename, call load_memory with the query
"memory showcase release codename" before answering. If memory contains the codename, answer
with the exact codename and mention that it came from memory.
memory:
modelConfig: memory-showcase-embedding
ttlDays: 15
deployment:
resources:
requests:
cpu: 200m
memory: 684Mi
limits:
cpu: 3000m
memory: 2Gi
tools: []
177 changes: 177 additions & 0 deletions examples/memory-showcase/run-demo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
#!/usr/bin/env bash
set -euo pipefail

KAGENT_URL="${KAGENT_URL:-http://localhost:8083}"
NAMESPACE="${NAMESPACE:-kagent}"
AGENT_NAME="${AGENT_NAME:-memory-showcase-agent}"
USER_ID="${USER_ID:-memory-showcase-user@example.com}"
TIMEOUT_SECONDS="${TIMEOUT_SECONDS:-180}"
AUTH_HEADER="${AUTH_HEADER:-}"
RESET_MEMORY="${RESET_MEMORY:-true}"
DEMO_FACT="blue-sunrise"

require() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "Missing required command: $1" >&2
exit 1
fi
}

new_id() {
if command -v uuidgen >/dev/null 2>&1; then
uuidgen | tr '[:upper:]' '[:lower:]'
else
printf "memory-showcase-%s-%s" "$(date +%s)" "$RANDOM"
fi
}

json_payload() {
local prompt="$1"
local context_id="$2"
local message_id rpc_id
message_id="$(new_id)"
rpc_id="$(new_id)"

jq -n \
--arg rpc_id "$rpc_id" \
--arg message_id "$message_id" \
--arg context_id "$context_id" \
--arg prompt "$prompt" \
'{
jsonrpc: "2.0",
method: "message/stream",
id: $rpc_id,
params: {
message: {
kind: "message",
messageId: $message_id,
contextId: $context_id,
role: "user",
parts: [{ kind: "text", text: $prompt }]
},
metadata: {}
}
}'
}

curl_headers=(
-H "Content-Type: application/json"
-H "Accept: text/event-stream"
-H "X-User-ID: ${USER_ID}"
)

if [[ -n "$AUTH_HEADER" ]]; then
curl_headers+=(-H "Authorization: ${AUTH_HEADER}")
fi

send_message() {
local prompt="$1"
local context_id="$2"
local output_file="$3"

echo
echo ">>> ${prompt}"
curl -fsS -N \
--max-time "$TIMEOUT_SECONDS" \
"${curl_headers[@]}" \
-d "$(json_payload "$prompt" "$context_id")" \
"${KAGENT_URL%/}/api/a2a/${NAMESPACE}/${AGENT_NAME}/" \
| tee "$output_file" >/dev/null
}

extract_text() {
local output_file="$1"
sed -n 's/^data: //p' "$output_file" \
| sed '/^\[DONE\]$/d' \
| jq -r '.. | objects | select(has("text")) | .text' \
| sed '/^$/d'
}

list_memories() {
local encoded_agent encoded_user
encoded_agent="$(jq -rn --arg value "$AGENT_NAME" '$value | @uri')"
encoded_user="$(jq -rn --arg value "$USER_ID" '$value | @uri')"

curl -fsS \
"${curl_headers[@]}" \
"${KAGENT_URL%/}/api/memories?agent_name=${encoded_agent}&user_id=${encoded_user}" \
| jq -r '.[]? | "- " + .content'
}

delete_memories() {
local encoded_agent encoded_user
encoded_agent="$(jq -rn --arg value "$AGENT_NAME" '$value | @uri')"
encoded_user="$(jq -rn --arg value "$USER_ID" '$value | @uri')"

curl -fsS -X DELETE \
"${curl_headers[@]}" \
"${KAGENT_URL%/}/api/memories?agent_name=${encoded_agent}&user_id=${encoded_user}" \
>/dev/null
}

require curl
require jq

first_context_id="$(new_id)"
second_context_id="$(new_id)"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT

echo "Kagent URL: ${KAGENT_URL%/}"
echo "Agent: ${NAMESPACE}/${AGENT_NAME}"
echo "User ID: ${USER_ID}"
echo "First context: ${first_context_id}"
echo "Second context: ${second_context_id}"

if [[ "$RESET_MEMORY" == "true" ]]; then
echo
echo "Resetting existing memories for this demo agent/user."
delete_memories
fi

echo
echo "Memory before turn 1:"
before="$(list_memories)"
if [[ -z "$before" ]]; then
echo "- no memories found for this agent/user"
else
echo "$before"
fi

turn1_output="$tmp_dir/turn1.sse"
send_message \
"Remember this exact fact in long-term memory: In the memory showcase, my release codename is blue-sunrise." \
"$first_context_id" \
"$turn1_output"

echo
echo "Turn 1 response text:"
turn1_text="$(extract_text "$turn1_output")"
echo "$turn1_text"

echo
echo "Memory after turn 1:"
after="$(list_memories)"
echo "$after"
if ! grep -qi "$DEMO_FACT" <<<"$after"; then
echo "Expected saved memory containing ${DEMO_FACT}, but it was not found." >&2
exit 1
fi

turn2_output="$tmp_dir/turn2.sse"
send_message \
"What is my release codename for the memory showcase? Use memory before answering." \
"$second_context_id" \
"$turn2_output"

echo
echo "Turn 2 response text:"
turn2_text="$(extract_text "$turn2_output")"
echo "$turn2_text"
if ! grep -qi "$DEMO_FACT" <<<"$turn2_text"; then
echo "Expected second response to include ${DEMO_FACT}, but it did not." >&2
exit 1
fi

echo
echo "Success: the second response used memory and included ${DEMO_FACT}."
Loading