Skip to content

Commit 7377075

Browse files
committed
example: add proactive example
1 parent 16ae534 commit 7377075

8 files changed

Lines changed: 375 additions & 0 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
memorize_config = {
2+
"memory_types": [
3+
"record",
4+
],
5+
"memory_type_prompts": {
6+
"record": {
7+
"objective": {
8+
"ordinal": 10,
9+
"prompt": "# Task Objective\nYou will be given a conversation between a user and an coding agent. Your goal is to extract detailed records for what are planed to do, and what have been done.",
10+
},
11+
"workflow": {
12+
"ordinal": 10,
13+
"prompt": "# Workflow\nRead through the conversation and extract records. You should expecially focus on:\n- What the user ask the agent to do\n- What plan does the agent suggest\n- What the agent has done",
14+
},
15+
"rules": {
16+
"ordinal": -1,
17+
"prompt": None,
18+
},
19+
"examples": {
20+
"ordinal": 60,
21+
"prompt": "# Example\n## Output\n<item>\n <memory>\n <content>The user ask the agent to generate a code example for fastapi</content>\n <categories>\n <category>todo</category>\n </categories>\n </memory>\n <memory>\n <content>The agent suggest to use the code example from the document</content>\n <categories>\n <category>todo</category>\n </categories>\n </memory>\n <memory>\n <content>The agent ask the user to specify the response type</content>\n <categories>\n <category>todo</category>\n </categories>\n </memory>\n</item>",
22+
},
23+
}
24+
},
25+
"memory_categories": [
26+
{
27+
"name": "todo",
28+
"description": "This file traces the latest status of the task. All records should be included in this file.",
29+
"target_length": None,
30+
"custom_prompt": {
31+
"objective": {
32+
"ordinal": 10,
33+
"prompt": "# Task Objective\nYou are a specialist in task management. You should update the markdown file to reflect the latest status of the task.",
34+
},
35+
"workflow": {
36+
"ordinal": 20,
37+
"prompt": "# Workflow\nRead through the existing markdown file and the new records. Then update the markdown file to reflect:\n- What existing tasks are completed\n- What new tasks are added\n- What tasks are still in progress",
38+
},
39+
"rules": {
40+
"ordinal": -1,
41+
"prompt": None,
42+
},
43+
"examples": {
44+
"ordinal": 50,
45+
"prompt": "# Example\n## Output\n```markdown\n# Task\n## Task Objective\nThe user ask the agent to generate a code example for fastapi\n## Workflow\nThe agent suggest to use the code example from the document\nThe agent ask the user to specify the response type\n```",
46+
},
47+
},
48+
}
49+
],
50+
}

examples/proactive/memory/local/__init__.py

Whitespace-only changes.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import json
2+
import os
3+
from pathlib import Path
4+
from typing import Any
5+
6+
import pendulum
7+
8+
from memu.app import MemoryService
9+
10+
from ..config import memorize_config
11+
12+
USER_ID = "claude_user"
13+
14+
15+
async def dump_conversation_resource(
16+
conversation_messages: list[dict[str, Any]],
17+
) -> str:
18+
resource_data = {
19+
"content": [
20+
{
21+
"role": message.get("role", "system"),
22+
"content": {"text": message.get("content", "")},
23+
"created_at": message.get("timestamp", pendulum.now().isoformat()),
24+
}
25+
for message in conversation_messages
26+
]
27+
}
28+
time_string = pendulum.now().format("YYYYMMDD_HHmmss")
29+
resource_url = Path(__file__).parent / "data" / f"conv_{time_string}.json"
30+
with open(resource_url, "w") as f:
31+
json.dump(resource_data, f, indent=4, ensure_ascii=False)
32+
return resource_url.as_posix()
33+
34+
35+
async def memorize(conversation_messages: list[dict[str, Any]]) -> str | None:
36+
api_key = os.getenv("OPENAI_API_KEY")
37+
if not api_key:
38+
msg = "Please set OPENAI_API_KEY environment variable"
39+
raise ValueError(msg)
40+
41+
memory_service = MemoryService(
42+
llm_profiles={
43+
"default": {
44+
"api_key": api_key,
45+
"chat_model": "gpt-4o-mini",
46+
},
47+
},
48+
memorize_config=memorize_config,
49+
)
50+
51+
resource_url = await dump_conversation_resource(conversation_messages)
52+
result = await memory_service.memorize(
53+
resource_url=resource_url, modality="conversation", user={"user_id": USER_ID}
54+
)
55+
return result
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import os
2+
from typing import Any
3+
4+
from claude_agent_sdk import create_sdk_mcp_server, tool
5+
6+
from memu.app import MemoryService
7+
8+
USER_ID = "claude_user"
9+
10+
11+
@tool("memu_memory", "Retrieve memory based on a query", {"query": str})
12+
async def get_memory(args: dict[str, Any]) -> dict[str, Any]:
13+
"""Retrieve memory from the memory API based on the provided query."""
14+
query = {"role": "user", "content": args["query"]}
15+
16+
api_key = os.getenv("OPENAI_API_KEY")
17+
if not api_key:
18+
msg = "Please set OPENAI_API_KEY environment variable"
19+
raise ValueError(msg)
20+
21+
memory_service = MemoryService(
22+
llm_profiles={
23+
"default": {
24+
"api_key": api_key,
25+
"chat_model": "gpt-4o-mini",
26+
},
27+
},
28+
retrieve_config={
29+
"method": "rag",
30+
"route_intention": False,
31+
"sufficiency_check": False,
32+
"category": {
33+
"enabled": False,
34+
},
35+
"item": {
36+
"enabled": True,
37+
"top_k": 10,
38+
},
39+
"resource": {
40+
"enabled": False,
41+
},
42+
},
43+
)
44+
45+
result = await memory_service.retrieve(query, where={"user_id": USER_ID})
46+
47+
return {"content": [{"type": "text", "text": str(result)}]}
48+
49+
50+
async def _get_todos() -> str:
51+
memory_service = MemoryService()
52+
53+
result = await memory_service.list_memory_categories(where={"user_id": USER_ID})
54+
55+
categories = result["categories"]
56+
todos = ""
57+
for category in categories:
58+
if category["name"] == "todo":
59+
todos = category["summary"]
60+
return todos
61+
62+
63+
@tool("memu_todos", "Retrieve todos for the user", {})
64+
async def get_todos() -> dict[str, Any]:
65+
"""Retrieve todos from the memory API."""
66+
todos = await _get_todos()
67+
return {"content": [{"type": "text", "text": str(todos)}]}
68+
69+
70+
memu_server = create_sdk_mcp_server(name="memu", version="1.0.0", tools=[get_memory, get_todos])

examples/proactive/memory/platform/__init__.py

Whitespace-only changes.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from typing import Any
2+
3+
import aiohttp
4+
5+
BASE_URL = "https://api.memu.so"
6+
API_KEY = "your memu api key"
7+
USER_ID = "claude_user"
8+
AGENT_ID = "claude_agent"
9+
10+
from ..config import memorize_config
11+
12+
13+
async def memorize(conversation_messages: list[dict[str, Any]]) -> str | None:
14+
payload = {
15+
"conversation": conversation_messages,
16+
"user_id": USER_ID,
17+
"agent_id": AGENT_ID,
18+
"override_config": memorize_config,
19+
}
20+
21+
async with (
22+
aiohttp.ClientSession() as session,
23+
session.post(
24+
f"{BASE_URL}/api/v3/memory/memorize",
25+
headers={"Authorization": f"Bearer {API_KEY}"},
26+
json=payload,
27+
) as response,
28+
):
29+
result = await response.json()
30+
task_id = result["task_id"]
31+
return task_id
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
from typing import Any
2+
3+
import aiohttp
4+
from claude_agent_sdk import create_sdk_mcp_server, tool
5+
6+
BASE_URL = "https://api.memu.so"
7+
API_KEY = "your memu api key"
8+
USER_ID = "claude_user"
9+
AGENT_ID = "claude_agent"
10+
11+
12+
@tool("memu_memory", "Retrieve memory based on a query", {"query": str})
13+
async def get_memory(args: dict[str, Any]) -> dict[str, Any]:
14+
"""Retrieve memory from the memory API based on the provided query."""
15+
query = args["query"]
16+
url = f"{BASE_URL}/api/v3/memory/retrieve"
17+
headers = {"Authorization": f"Bearer {API_KEY}"}
18+
data = {"user_id": USER_ID, "agent_id": AGENT_ID, "query": query}
19+
20+
async with aiohttp.ClientSession() as session, session.post(url, headers=headers, json=data) as response:
21+
result = await response.json()
22+
23+
return {"content": [{"type": "text", "text": str(result)}]}
24+
25+
26+
async def _get_todos() -> str:
27+
url = f"{BASE_URL}/api/v3/memory/categories"
28+
headers = {"Authorization": f"Bearer {API_KEY}"}
29+
data = {
30+
"user_id": USER_ID,
31+
"agent_id": AGENT_ID,
32+
}
33+
async with aiohttp.ClientSession() as session, session.post(url, headers=headers, json=data) as response:
34+
result = await response.json()
35+
36+
categories = result["categories"]
37+
todos = ""
38+
for category in categories:
39+
if category["name"] == "todo":
40+
todos = category["summary"]
41+
return todos
42+
43+
44+
@tool("memu_todos", "Retrieve todos for the user", {})
45+
async def get_todos() -> dict[str, Any]:
46+
"""Retrieve todos from the memory API."""
47+
todos = await _get_todos()
48+
return {"content": [{"type": "text", "text": str(todos)}]}
49+
50+
51+
# Create the MCP server with the tool
52+
memu_server = create_sdk_mcp_server(name="memu", version="1.0.0", tools=[get_memory, get_todos])

examples/proactive/proactive.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import asyncio
2+
3+
from claude_agent_sdk import (
4+
AssistantMessage,
5+
ClaudeAgentOptions,
6+
ClaudeSDKClient,
7+
ResultMessage,
8+
TextBlock,
9+
)
10+
from memory.local.memorize import memorize
11+
from memory.local.tools import _get_todos, memu_server
12+
13+
# Set your Anthropic API key here if it's not set in the environment variables
14+
# os.environ["ANTHROPIC_API_KEY"] = ""
15+
16+
N_MESSAGES_MEMORIZE = 2
17+
18+
19+
async def trigger_memorize(messages: list[dict[str, any]]) -> bool:
20+
"""Background task to memorize conversation messages."""
21+
try:
22+
await memorize(messages)
23+
print("\n[Background] Memorization submitted.")
24+
return True
25+
except Exception as e:
26+
print(f"\n[Background] Memorization failed: {e!r}")
27+
return False
28+
29+
30+
async def main():
31+
options = ClaudeAgentOptions(
32+
mcp_servers={"memu": memu_server},
33+
allowed_tools=[
34+
# "mcp__memu__memu_memory",
35+
"mcp__memu__memu_todos",
36+
],
37+
)
38+
39+
conversation_messages: list[dict[str, any]] = []
40+
pending_tasks: list[asyncio.Task] = []
41+
42+
print("Claude Autorun")
43+
print("Type 'quit' or 'exit' to end the session.")
44+
print("-" * 40)
45+
46+
round = 0
47+
async with ClaudeSDKClient(options=options) as client:
48+
while True:
49+
want_user_input = False
50+
51+
if round == 0:
52+
want_user_input = True
53+
else:
54+
todos = await _get_todos()
55+
if todos:
56+
user_input = f"Please continue with the following todos:\n{todos}"
57+
else:
58+
want_user_input = True
59+
60+
if want_user_input:
61+
try:
62+
user_input = input("\nYou: ").strip()
63+
except EOFError:
64+
break
65+
66+
if not user_input:
67+
continue
68+
69+
if user_input.lower() in ("quit", "exit"):
70+
break
71+
72+
# Record user message
73+
conversation_messages.append({"role": "user", "content": user_input})
74+
75+
# Send query to Claude
76+
await client.query(user_input)
77+
78+
# Collect assistant response
79+
assistant_text_parts: list[str] = []
80+
81+
async for message in client.receive_response():
82+
if isinstance(message, AssistantMessage):
83+
for block in message.content:
84+
if isinstance(block, TextBlock):
85+
print(f"Claude: {block.text}")
86+
assistant_text_parts.append(block.text)
87+
elif isinstance(message, ResultMessage):
88+
print(f"Result: {message.result}")
89+
90+
# Record assistant message
91+
if assistant_text_parts:
92+
conversation_messages.append({"role": "assistant", "content": "\n".join(assistant_text_parts)})
93+
94+
# Check if we should trigger memorization
95+
if len(conversation_messages) >= N_MESSAGES_MEMORIZE:
96+
print(f"\n[Info] Reached {N_MESSAGES_MEMORIZE} messages, triggering memorization...")
97+
success = await trigger_memorize(conversation_messages.copy())
98+
if success:
99+
conversation_messages.clear()
100+
101+
round += 1
102+
103+
# User quit - memorize remaining messages if any
104+
if conversation_messages:
105+
print("\n[Info] Session ended, memorizing remaining messages...")
106+
success = await trigger_memorize(conversation_messages.copy())
107+
108+
# Wait for all pending memorization tasks to complete
109+
if pending_tasks:
110+
print("[Info] Waiting for memorization tasks to complete...")
111+
await asyncio.gather(*pending_tasks, return_exceptions=True)
112+
113+
print("\nDone")
114+
115+
116+
if __name__ == "__main__":
117+
asyncio.run(main())

0 commit comments

Comments
 (0)