From 5ec7867d02cfd4f48f57b4f6652983945c59d994 Mon Sep 17 00:00:00 2001 From: scotty-bot Date: Wed, 13 May 2026 06:51:28 +0000 Subject: [PATCH] MIL-116 Add memory showcase example --- examples/memory-showcase/README.md | 83 ++++++++ examples/memory-showcase/memory-showcase.yaml | 61 ++++++ examples/memory-showcase/run-demo.sh | 177 ++++++++++++++++++ 3 files changed, 321 insertions(+) create mode 100644 examples/memory-showcase/README.md create mode 100644 examples/memory-showcase/memory-showcase.yaml create mode 100755 examples/memory-showcase/run-demo.sh diff --git a/examples/memory-showcase/README.md b/examples/memory-showcase/README.md new file mode 100644 index 000000000..a6f9838c4 --- /dev/null +++ b/examples/memory-showcase/README.md @@ -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 +``` diff --git a/examples/memory-showcase/memory-showcase.yaml b/examples/memory-showcase/memory-showcase.yaml new file mode 100644 index 000000000..1355f7cce --- /dev/null +++ b/examples/memory-showcase/memory-showcase.yaml @@ -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: [] diff --git a/examples/memory-showcase/run-demo.sh b/examples/memory-showcase/run-demo.sh new file mode 100755 index 000000000..d16bdf5ce --- /dev/null +++ b/examples/memory-showcase/run-demo.sh @@ -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}."