Skip to content

Commit d83528a

Browse files
committed
Qwen agent integration
1 parent e1a6208 commit d83528a

File tree

3 files changed

+281
-0
lines changed

3 files changed

+281
-0
lines changed

examples/qwen_agent/README.md

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Qwen Agent + MemMachine (Tool Integration Example)
2+
3+
This example shows how to integrate **MemMachine** into a **Qwen Agent** app by exposing MemMachine operations as Qwen Agent tools:
4+
5+
- `save_memory`: writes a message to MemMachine
6+
- `search_memory`: queries MemMachine and formats results back to the agent
7+
8+
The runnable script is in [qwen_agent_example.py](qwen_agent_example.py).
9+
10+
## Overview
11+
12+
The integration pattern is:
13+
14+
1. Your Qwen Agent calls a tool (`save_memory` / `search_memory`) based on the user request.
15+
2. The tool uses the MemMachine Python SDK to connect to a MemMachine project.
16+
3. The tool stores or retrieves memory, then returns a plain-text summary to the agent.
17+
18+
## Architecture
19+
20+
```
21+
qwen_agent/
22+
├── qwen_agent_example.py # Qwen Agent script + MemMachine tools
23+
└── README.md # This file
24+
```
25+
26+
## Prerequisites
27+
28+
- Python 3.10+ (recommended: the same version you use for MemMachine)
29+
- A running MemMachine server (start one locally or connect to an existing deployment)
30+
- Qwen Agent credentials/configuration (follow the `qwen-agent` docs for how to authenticate)
31+
32+
## Start MemMachine
33+
34+
From the repo root, you can start MemMachine using the provided compose helper:
35+
36+
```bash
37+
./memmachine-compose.sh
38+
```
39+
40+
Once it’s running, verify:
41+
42+
```bash
43+
curl -s http://localhost:8080/health
44+
```
45+
46+
## Install Dependencies
47+
48+
This example only needs the MemMachine **Python client** package plus Qwen Agent:
49+
50+
```bash
51+
pip install memmachine-client qwen-agent
52+
```
53+
54+
If you are developing MemMachine from source in this repo, you can still install the local package instead:
55+
56+
```bash
57+
pip install -e .
58+
```
59+
60+
## Configuration
61+
62+
This example reads the following environment variables:
63+
64+
- `MEMMACHINE_BASE_URL` (default: `http://localhost:8080`)
65+
- `MEMMACHINE_API_KEY` (default: empty)
66+
- `MEMMACHINE_ORG_ID` (default: `default_org`)
67+
- `MEMMACHINE_PROJECT_ID` (default: `qwen_agent_demo`)
68+
69+
Example:
70+
71+
```bash
72+
export MEMMACHINE_BASE_URL="http://localhost:8080" # or your existing MemMachine deployment URL
73+
export MEMMACHINE_ORG_ID="default_org"
74+
export MEMMACHINE_PROJECT_ID="qwen_agent_demo"
75+
```
76+
77+
Qwen Agent authentication is configured via the `DASHSCOPE_API_KEY` environment variable:
78+
79+
```bash
80+
export DASHSCOPE_API_KEY="your-dashscope-api-key"
81+
```
82+
83+
## Run the Example
84+
85+
```bash
86+
cd examples/qwen_agent
87+
python qwen_agent_example.py
88+
```
89+
90+
The script runs two short conversations:
91+
92+
1. A user message that asks the agent to remember something (it should call `save_memory`).
93+
2. A user message that asks the agent to recall it (it should call `search_memory`).
94+
95+
## Notes
96+
97+
- This script uses `project.memory()` with no explicit `user_id` / `session_id`. For real applications, you typically want per-user/per-session isolation, e.g.:
98+
99+
```python
100+
mem = project.memory(user_id="user123", session_id="session456")
101+
```
102+
103+
- If you see connection errors, confirm the server is running and `MEMMACHINE_BASE_URL` is reachable.
104+
105+
## Troubleshooting
106+
107+
- **MemMachine connection refused**: ensure the server is running and `MEMMACHINE_BASE_URL` is correct.
108+
- **Qwen model/auth errors**: verify your Qwen Agent credentials/config follow `qwen-agent` requirements.

examples/qwen_agent/__init__.py

Whitespace-only changes.
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import os
2+
from typing import ClassVar
3+
4+
from qwen_agent.agents import Assistant
5+
from qwen_agent.tools.base import BaseTool, register_tool
6+
from qwen_agent.utils.output_beautify import typewriter_print
7+
8+
from memmachine import MemMachineClient
9+
10+
_MEMMACHINE_CLIENT = None
11+
_MEMMACHINE_PROJECT = None
12+
13+
14+
def _get_memmachine_project():
15+
"""Create (or reuse) a MemMachine Project handle (global boundary)."""
16+
global _MEMMACHINE_CLIENT, _MEMMACHINE_PROJECT
17+
if _MEMMACHINE_PROJECT is not None:
18+
return _MEMMACHINE_PROJECT
19+
20+
base_url = os.getenv("MEMMACHINE_BASE_URL") or "http://localhost:8080"
21+
api_key = os.getenv("MEMMACHINE_API_KEY") or ""
22+
org_id = os.getenv("MEMMACHINE_ORG_ID") or "default_org"
23+
project_id = os.getenv("MEMMACHINE_PROJECT_ID") or "qwen_agent_demo"
24+
25+
_MEMMACHINE_CLIENT = MemMachineClient(
26+
api_key=api_key, base_url=base_url, timeout=30
27+
)
28+
_MEMMACHINE_PROJECT = _MEMMACHINE_CLIENT.get_or_create_project(
29+
org_id=org_id,
30+
project_id=project_id,
31+
description="qwen-agent tool memory integration",
32+
)
33+
return _MEMMACHINE_PROJECT
34+
35+
36+
@register_tool("save_memory")
37+
class SaveMemory(BaseTool):
38+
description = "Save a memory entry to MemMachine."
39+
parameters: ClassVar[list[dict] | dict] = [
40+
{
41+
"name": "content",
42+
"type": "string",
43+
"description": "The content to save.",
44+
"required": True,
45+
},
46+
]
47+
48+
def call(self, params: str | dict, **kwargs) -> str:
49+
data = self._verify_json_format_args(params)
50+
content = data["content"]
51+
project = _get_memmachine_project()
52+
mem = project.memory()
53+
results = mem.add(
54+
content=content,
55+
role="assistant",
56+
metadata={"type": "message"},
57+
)
58+
uid = results[0].uid if results else ""
59+
return f"Saved to MemMachine ({uid}): {content}"
60+
61+
62+
@register_tool("search_memory")
63+
class SearchMemory(BaseTool):
64+
description = "Search memory for information matching the query."
65+
parameters: ClassVar[list[dict] | dict] = [
66+
{
67+
"name": "query",
68+
"type": "string",
69+
"description": "The query to search in memory.",
70+
"required": True,
71+
},
72+
]
73+
74+
@staticmethod
75+
def _format_episodic_bucket(bucket_name: str, bucket: dict) -> list[str]:
76+
episodes = bucket.get("episodes")
77+
if not isinstance(episodes, list) or not episodes:
78+
return []
79+
80+
lines: list[str] = [f"{bucket_name}:"]
81+
lines.extend(f"- {ep['content']}" for ep in episodes)
82+
return lines
83+
84+
@staticmethod
85+
def _format_episodic_memory(episodic: dict) -> list[str]:
86+
lines: list[str] = ["episodic_memory:"]
87+
88+
long_term = episodic.get("long_term_memory")
89+
if isinstance(long_term, dict):
90+
lines.extend(
91+
SearchMemory._format_episodic_bucket("long_term_memory", long_term)
92+
)
93+
94+
short_term = episodic.get("short_term_memory")
95+
if isinstance(short_term, dict):
96+
lines.extend(
97+
SearchMemory._format_episodic_bucket("short_term_memory", short_term)
98+
)
99+
100+
summaries = short_term.get("episode_summary")
101+
if isinstance(summaries, list) and summaries:
102+
lines.append("episode_summary:")
103+
lines.extend(f"- {s}" for s in summaries)
104+
105+
return lines
106+
107+
@staticmethod
108+
def _format_semantic_memory(semantic: list) -> list[str]:
109+
lines: list[str] = ["semantic_memory:"]
110+
for item in semantic:
111+
if not isinstance(item, dict):
112+
lines.append(f"- {item!s}")
113+
continue
114+
category = item.get("category")
115+
tag = item.get("tag")
116+
feature_name = item.get("feature_name")
117+
value = item.get("value")
118+
lines.append(f"- [{category}/{tag}] {feature_name} = {value}")
119+
return lines
120+
121+
@staticmethod
122+
def _format_search_content(content: dict) -> list[str]:
123+
lines: list[str] = []
124+
125+
episodic = content.get("episodic_memory")
126+
if isinstance(episodic, dict) and episodic:
127+
lines.extend(SearchMemory._format_episodic_memory(episodic))
128+
129+
semantic = content.get("semantic_memory")
130+
if isinstance(semantic, list) and semantic:
131+
lines.extend(SearchMemory._format_semantic_memory(semantic))
132+
133+
return lines
134+
135+
def call(self, params: str | dict, **kwargs) -> str:
136+
data = self._verify_json_format_args(params)
137+
query = data["query"]
138+
project = _get_memmachine_project()
139+
mem = project.memory()
140+
result = mem.search(query=query, limit=10)
141+
142+
content = result.content
143+
lines = self._format_search_content(content)
144+
return "MemMachine search result:\n" + "\n".join(lines)
145+
146+
147+
if __name__ == "__main__":
148+
llm_cfg = {"model": "qwen3-max"}
149+
system_message = "You are an assistant that can remember information using tools. When asked to remember something, call save_memory. When asked to recall or search information, call search_memory."
150+
bot = Assistant(
151+
llm=llm_cfg,
152+
system_message=system_message,
153+
function_list=["save_memory", "search_memory"],
154+
)
155+
156+
messages = [
157+
{
158+
"role": "user",
159+
"content": "My name is Alice and my favorite color is blue. Please remember that.",
160+
}
161+
]
162+
response_plain_text = ""
163+
for response in bot.run(messages=messages):
164+
response_plain_text = typewriter_print(response, response_plain_text)
165+
166+
messages = [
167+
{
168+
"role": "user",
169+
"content": "What is my name and favorite color? Search your memory.",
170+
}
171+
]
172+
for response in bot.run(messages=messages):
173+
response_plain_text = typewriter_print(response, response_plain_text)

0 commit comments

Comments
 (0)