rem-memory 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rem_memory-0.1.0/.gitignore +69 -0
- rem_memory-0.1.0/PKG-INFO +172 -0
- rem_memory-0.1.0/README.md +131 -0
- rem_memory-0.1.0/pyproject.toml +54 -0
- rem_memory-0.1.0/rem_memory/__init__.py +29 -0
- rem_memory-0.1.0/rem_memory/client.py +379 -0
- rem_memory-0.1.0/rem_memory/exceptions.py +43 -0
- rem_memory-0.1.0/rem_memory/integrations/autogen.py +196 -0
- rem_memory-0.1.0/rem_memory/integrations/langchain.py +105 -0
- rem_memory-0.1.0/rem_memory/types.py +88 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Binaries
|
|
2
|
+
*.exe
|
|
3
|
+
*.exe~
|
|
4
|
+
*.dll
|
|
5
|
+
*.so
|
|
6
|
+
*.dylib
|
|
7
|
+
|
|
8
|
+
# Go build output
|
|
9
|
+
go-api/bin/
|
|
10
|
+
go-api/tmp/
|
|
11
|
+
|
|
12
|
+
# Python
|
|
13
|
+
__pycache__/
|
|
14
|
+
*.py[cod]
|
|
15
|
+
*$py.class
|
|
16
|
+
*.egg-info/
|
|
17
|
+
dist/
|
|
18
|
+
build/
|
|
19
|
+
.eggs/
|
|
20
|
+
*.egg
|
|
21
|
+
.venv/
|
|
22
|
+
venv/
|
|
23
|
+
env/
|
|
24
|
+
.Python
|
|
25
|
+
|
|
26
|
+
# Node / Next.js
|
|
27
|
+
node_modules/
|
|
28
|
+
.next/
|
|
29
|
+
out/
|
|
30
|
+
dashboard/.next/
|
|
31
|
+
dashboard/out/
|
|
32
|
+
dashboard/node_modules/
|
|
33
|
+
|
|
34
|
+
# Environment files
|
|
35
|
+
.env
|
|
36
|
+
.env.local
|
|
37
|
+
.env.*.local
|
|
38
|
+
|
|
39
|
+
# IDE / Editor
|
|
40
|
+
.idea/
|
|
41
|
+
.vscode/settings.json
|
|
42
|
+
*.swp
|
|
43
|
+
*.swo
|
|
44
|
+
.DS_Store
|
|
45
|
+
Thumbs.db
|
|
46
|
+
|
|
47
|
+
# Logs
|
|
48
|
+
*.log
|
|
49
|
+
npm-debug.log*
|
|
50
|
+
yarn-debug.log*
|
|
51
|
+
yarn-error.log*
|
|
52
|
+
pnpm-debug.log*
|
|
53
|
+
|
|
54
|
+
# Test coverage
|
|
55
|
+
coverage/
|
|
56
|
+
*.coverprofile
|
|
57
|
+
htmlcov/
|
|
58
|
+
.coverage
|
|
59
|
+
|
|
60
|
+
# Compiled / generated
|
|
61
|
+
*.pb.go
|
|
62
|
+
*.pb.gw.go
|
|
63
|
+
|
|
64
|
+
# Docker
|
|
65
|
+
.dockerignore
|
|
66
|
+
|
|
67
|
+
# Misc
|
|
68
|
+
*.tmp
|
|
69
|
+
*.bak
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rem-memory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Recursive Episodic Memory for AI Agents — persistent memory infrastructure
|
|
5
|
+
Project-URL: Homepage, https://rem.ai
|
|
6
|
+
Project-URL: Documentation, https://rem.ai/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/yourusername/rem
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/yourusername/rem/issues
|
|
9
|
+
Author-email: Your Name <you@example.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: agents,ai,episodic-memory,llm,memory,rag
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.26.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Provides-Extra: all
|
|
25
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'all'
|
|
26
|
+
Requires-Dist: langchain>=0.1.0; extra == 'all'
|
|
27
|
+
Requires-Dist: pyautogen>=0.2.0; extra == 'all'
|
|
28
|
+
Provides-Extra: autogen
|
|
29
|
+
Requires-Dist: pyautogen>=0.2.0; extra == 'autogen'
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: black; extra == 'dev'
|
|
32
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
33
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
35
|
+
Requires-Dist: respx; extra == 'dev'
|
|
36
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
37
|
+
Provides-Extra: langchain
|
|
38
|
+
Requires-Dist: langchain-core>=0.1.0; extra == 'langchain'
|
|
39
|
+
Requires-Dist: langchain>=0.1.0; extra == 'langchain'
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
|
|
42
|
+
# REM Python SDK
|
|
43
|
+
|
|
44
|
+
[](https://pypi.org/project/rem-memory/)
|
|
45
|
+
[](https://pypi.org/project/rem-memory/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
**rem-memory** is the official Python SDK for [REM](https://github.com/your-org/rem) (Recursive Episodic Memory) — an open-source memory layer for AI agents.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install rem-memory
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
from rem_memory import REMClient
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
async with REMClient(api_key="rem_sk_...") as client:
|
|
66
|
+
# Write a memory episode
|
|
67
|
+
result = await client.write(
|
|
68
|
+
content="User prefers TypeScript with strict mode for all new projects.",
|
|
69
|
+
agent_id="agent_cursor",
|
|
70
|
+
user_id="user_alice",
|
|
71
|
+
)
|
|
72
|
+
print(result.episode_id)
|
|
73
|
+
|
|
74
|
+
# Retrieve relevant memories
|
|
75
|
+
memories = await client.retrieve(
|
|
76
|
+
query="Does the user prefer TypeScript or JavaScript?",
|
|
77
|
+
agent_id="agent_cursor",
|
|
78
|
+
top_k=5,
|
|
79
|
+
)
|
|
80
|
+
for m in memories.episodes:
|
|
81
|
+
print(m.intent, m.importance_score)
|
|
82
|
+
|
|
83
|
+
asyncio.run(main())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Configuration
|
|
87
|
+
|
|
88
|
+
| Parameter | Default | Description |
|
|
89
|
+
|-----------|---------|-------------|
|
|
90
|
+
| `api_key` | — | Your REM API key (`X-API-Key` header) |
|
|
91
|
+
| `base_url` | `http://localhost:8080` | Base URL of the REM Go API |
|
|
92
|
+
| `timeout` | `30` | Request timeout in seconds |
|
|
93
|
+
|
|
94
|
+
Environment variables are also supported:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
export REM_API_KEY=rem_sk_...
|
|
98
|
+
export REM_BASE_URL=http://localhost:8080
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Integrations
|
|
102
|
+
|
|
103
|
+
### LangChain
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from rem_memory.integrations.langchain import REMMemory
|
|
107
|
+
from langchain.chains import ConversationChain
|
|
108
|
+
from langchain_openai import ChatOpenAI
|
|
109
|
+
|
|
110
|
+
memory = REMMemory(
|
|
111
|
+
api_key="rem_sk_...",
|
|
112
|
+
agent_id="agent_cursor",
|
|
113
|
+
user_id="user_alice",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
chain = ConversationChain(llm=ChatOpenAI(), memory=memory)
|
|
117
|
+
response = chain.predict(input="What are my coding preferences?")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### AutoGen
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from rem_memory.integrations.autogen import REMMemoryStore
|
|
124
|
+
from autogen import ConversableAgent
|
|
125
|
+
|
|
126
|
+
memory_store = REMMemoryStore(
|
|
127
|
+
api_key="rem_sk_...",
|
|
128
|
+
agent_id="agent_autogen",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
agent = ConversableAgent(
|
|
132
|
+
name="assistant",
|
|
133
|
+
system_message="You are a helpful coding assistant.",
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Inject past memories into system prompt
|
|
137
|
+
context = await memory_store.get_context(query="user coding preferences")
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## API Reference
|
|
141
|
+
|
|
142
|
+
### `REMClient`
|
|
143
|
+
|
|
144
|
+
#### `write(content, agent_id, user_id, session_id?, metadata?)`
|
|
145
|
+
Write a raw interaction to episodic memory. Returns `WriteResult`.
|
|
146
|
+
|
|
147
|
+
#### `retrieve(query, agent_id, top_k?, include_semantic?)`
|
|
148
|
+
Retrieve semantically relevant episodes and facts. Returns `RetrieveResult`.
|
|
149
|
+
|
|
150
|
+
#### `get_agent(agent_id)`
|
|
151
|
+
Fetch agent metadata. Returns `Agent`.
|
|
152
|
+
|
|
153
|
+
#### `list_agents()`
|
|
154
|
+
List all agents. Returns `List[Agent]`.
|
|
155
|
+
|
|
156
|
+
#### `list_episodes(agent_id, limit?, offset?)`
|
|
157
|
+
Page through raw episodes for an agent. Returns `List[Episode]`.
|
|
158
|
+
|
|
159
|
+
#### `list_semantic_memories(agent_id)`
|
|
160
|
+
Fetch all consolidated semantic memories. Returns `List[SemanticMemory]`.
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cd sdk/python
|
|
166
|
+
pip install -e ".[dev]"
|
|
167
|
+
pytest
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT © REM Contributors
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# REM Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/rem-memory/)
|
|
4
|
+
[](https://pypi.org/project/rem-memory/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
|
|
7
|
+
**rem-memory** is the official Python SDK for [REM](https://github.com/your-org/rem) (Recursive Episodic Memory) — an open-source memory layer for AI agents.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install rem-memory
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
import asyncio
|
|
21
|
+
from rem_memory import REMClient
|
|
22
|
+
|
|
23
|
+
async def main():
|
|
24
|
+
async with REMClient(api_key="rem_sk_...") as client:
|
|
25
|
+
# Write a memory episode
|
|
26
|
+
result = await client.write(
|
|
27
|
+
content="User prefers TypeScript with strict mode for all new projects.",
|
|
28
|
+
agent_id="agent_cursor",
|
|
29
|
+
user_id="user_alice",
|
|
30
|
+
)
|
|
31
|
+
print(result.episode_id)
|
|
32
|
+
|
|
33
|
+
# Retrieve relevant memories
|
|
34
|
+
memories = await client.retrieve(
|
|
35
|
+
query="Does the user prefer TypeScript or JavaScript?",
|
|
36
|
+
agent_id="agent_cursor",
|
|
37
|
+
top_k=5,
|
|
38
|
+
)
|
|
39
|
+
for m in memories.episodes:
|
|
40
|
+
print(m.intent, m.importance_score)
|
|
41
|
+
|
|
42
|
+
asyncio.run(main())
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configuration
|
|
46
|
+
|
|
47
|
+
| Parameter | Default | Description |
|
|
48
|
+
|-----------|---------|-------------|
|
|
49
|
+
| `api_key` | — | Your REM API key (`X-API-Key` header) |
|
|
50
|
+
| `base_url` | `http://localhost:8080` | Base URL of the REM Go API |
|
|
51
|
+
| `timeout` | `30` | Request timeout in seconds |
|
|
52
|
+
|
|
53
|
+
Environment variables are also supported:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
export REM_API_KEY=rem_sk_...
|
|
57
|
+
export REM_BASE_URL=http://localhost:8080
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Integrations
|
|
61
|
+
|
|
62
|
+
### LangChain
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from rem_memory.integrations.langchain import REMMemory
|
|
66
|
+
from langchain.chains import ConversationChain
|
|
67
|
+
from langchain_openai import ChatOpenAI
|
|
68
|
+
|
|
69
|
+
memory = REMMemory(
|
|
70
|
+
api_key="rem_sk_...",
|
|
71
|
+
agent_id="agent_cursor",
|
|
72
|
+
user_id="user_alice",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
chain = ConversationChain(llm=ChatOpenAI(), memory=memory)
|
|
76
|
+
response = chain.predict(input="What are my coding preferences?")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### AutoGen
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from rem_memory.integrations.autogen import REMMemoryStore
|
|
83
|
+
from autogen import ConversableAgent
|
|
84
|
+
|
|
85
|
+
memory_store = REMMemoryStore(
|
|
86
|
+
api_key="rem_sk_...",
|
|
87
|
+
agent_id="agent_autogen",
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
agent = ConversableAgent(
|
|
91
|
+
name="assistant",
|
|
92
|
+
system_message="You are a helpful coding assistant.",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Inject past memories into system prompt
|
|
96
|
+
context = await memory_store.get_context(query="user coding preferences")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## API Reference
|
|
100
|
+
|
|
101
|
+
### `REMClient`
|
|
102
|
+
|
|
103
|
+
#### `write(content, agent_id, user_id, session_id?, metadata?)`
|
|
104
|
+
Write a raw interaction to episodic memory. Returns `WriteResult`.
|
|
105
|
+
|
|
106
|
+
#### `retrieve(query, agent_id, top_k?, include_semantic?)`
|
|
107
|
+
Retrieve semantically relevant episodes and facts. Returns `RetrieveResult`.
|
|
108
|
+
|
|
109
|
+
#### `get_agent(agent_id)`
|
|
110
|
+
Fetch agent metadata. Returns `Agent`.
|
|
111
|
+
|
|
112
|
+
#### `list_agents()`
|
|
113
|
+
List all agents. Returns `List[Agent]`.
|
|
114
|
+
|
|
115
|
+
#### `list_episodes(agent_id, limit?, offset?)`
|
|
116
|
+
Page through raw episodes for an agent. Returns `List[Episode]`.
|
|
117
|
+
|
|
118
|
+
#### `list_semantic_memories(agent_id)`
|
|
119
|
+
Fetch all consolidated semantic memories. Returns `List[SemanticMemory]`.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cd sdk/python
|
|
125
|
+
pip install -e ".[dev]"
|
|
126
|
+
pytest
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT © REM Contributors
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "rem-memory"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Recursive Episodic Memory for AI Agents — persistent memory infrastructure"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "Your Name", email = "you@example.com"}]
|
|
13
|
+
keywords = ["ai", "memory", "agents", "llm", "rag", "episodic-memory"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
dependencies = [
|
|
27
|
+
"httpx>=0.26.0",
|
|
28
|
+
"pydantic>=2.0.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
langchain = ["langchain>=0.1.0", "langchain-core>=0.1.0"]
|
|
33
|
+
autogen = ["pyautogen>=0.2.0"]
|
|
34
|
+
all = ["rem-memory[langchain,autogen]"]
|
|
35
|
+
dev = ["pytest", "pytest-asyncio", "respx", "black", "ruff", "mypy"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://rem.ai"
|
|
39
|
+
Documentation = "https://rem.ai/docs"
|
|
40
|
+
Repository = "https://github.com/yourusername/rem"
|
|
41
|
+
"Bug Tracker" = "https://github.com/yourusername/rem/issues"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["rem_memory"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
line-length = 88
|
|
48
|
+
target-version = "py39"
|
|
49
|
+
|
|
50
|
+
[tool.mypy]
|
|
51
|
+
python_version = "3.9"
|
|
52
|
+
strict = true
|
|
53
|
+
|
|
54
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
2
|
+
|
|
3
|
+
from .client import REMClient
|
|
4
|
+
from .types import Agent, RetrieveResult, SemanticMemory, WriteResult
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
REMError,
|
|
7
|
+
AuthenticationError,
|
|
8
|
+
NotFoundError,
|
|
9
|
+
RateLimitError,
|
|
10
|
+
APIError,
|
|
11
|
+
ConnectionError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"REMClient",
|
|
17
|
+
"WriteResult",
|
|
18
|
+
"RetrieveResult",
|
|
19
|
+
"SemanticMemory",
|
|
20
|
+
"Agent",
|
|
21
|
+
"REMError",
|
|
22
|
+
"AuthenticationError",
|
|
23
|
+
"NotFoundError",
|
|
24
|
+
"RateLimitError",
|
|
25
|
+
"APIError",
|
|
26
|
+
"ConnectionError",
|
|
27
|
+
"ValidationError",
|
|
28
|
+
]
|
|
29
|
+
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple, Literal
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .types import (
|
|
9
|
+
Agent,
|
|
10
|
+
CreateAgentRequest,
|
|
11
|
+
EpisodeResult,
|
|
12
|
+
RetrieveResult,
|
|
13
|
+
SemanticMemory,
|
|
14
|
+
WriteResult,
|
|
15
|
+
)
|
|
16
|
+
from .exceptions import (
|
|
17
|
+
REMError,
|
|
18
|
+
AuthenticationError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
RateLimitError,
|
|
21
|
+
APIError,
|
|
22
|
+
ConnectionError,
|
|
23
|
+
ValidationError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
SDK_VERSION = "0.1.0"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class REMClient:
|
|
31
|
+
"""
|
|
32
|
+
Recursive Episodic Memory client for AI agents.
|
|
33
|
+
|
|
34
|
+
Quick start:
|
|
35
|
+
client = REMClient(api_key="rem_sk_...")
|
|
36
|
+
|
|
37
|
+
# After agent task
|
|
38
|
+
await client.write(
|
|
39
|
+
content="User prefers TypeScript",
|
|
40
|
+
agent_id="my-agent",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Before next task
|
|
44
|
+
result = await client.retrieve(
|
|
45
|
+
query="user preferences",
|
|
46
|
+
agent_id="my-agent",
|
|
47
|
+
)
|
|
48
|
+
print(result.injection_prompt)
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
api_key: str,
|
|
54
|
+
base_url: str = "https://api.rem.ai",
|
|
55
|
+
timeout: int = 30,
|
|
56
|
+
max_retries: int = 3,
|
|
57
|
+
) -> None:
|
|
58
|
+
if not api_key:
|
|
59
|
+
raise AuthenticationError("api_key is required")
|
|
60
|
+
if not api_key.startswith("rem_sk_"):
|
|
61
|
+
raise AuthenticationError(
|
|
62
|
+
"Invalid API key format. Key must start with 'rem_sk_'",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
self.api_key = api_key
|
|
66
|
+
self.base_url = base_url.rstrip("/")
|
|
67
|
+
self.timeout = timeout
|
|
68
|
+
self.max_retries = max_retries
|
|
69
|
+
|
|
70
|
+
self._headers = {
|
|
71
|
+
"X-API-Key": api_key,
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"User-Agent": f"rem-memory-python/{SDK_VERSION}",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
self._async_client: Optional[httpx.AsyncClient] = None
|
|
77
|
+
self._sync_client: Optional[httpx.Client] = None
|
|
78
|
+
|
|
79
|
+
# ── ASYNC METHODS ────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
async def write(
|
|
82
|
+
self,
|
|
83
|
+
content: str,
|
|
84
|
+
agent_id: str,
|
|
85
|
+
user_id: str = "default",
|
|
86
|
+
session_id: str = "",
|
|
87
|
+
outcome: Literal["success", "failure", "partial", "unknown"] = "unknown",
|
|
88
|
+
metadata: Optional[Dict[str, str]] = None,
|
|
89
|
+
) -> WriteResult:
|
|
90
|
+
"""
|
|
91
|
+
Write an episode to memory after your agent completes a task.
|
|
92
|
+
"""
|
|
93
|
+
if not content or len(content.strip()) < 10:
|
|
94
|
+
raise ValidationError("content must be at least 10 characters")
|
|
95
|
+
if not agent_id:
|
|
96
|
+
raise ValidationError("agent_id is required")
|
|
97
|
+
|
|
98
|
+
response = await self._async_request(
|
|
99
|
+
"POST",
|
|
100
|
+
"/api/v1/episodes",
|
|
101
|
+
json={
|
|
102
|
+
"content": content,
|
|
103
|
+
"agent_id": agent_id,
|
|
104
|
+
"user_id": user_id,
|
|
105
|
+
"session_id": session_id,
|
|
106
|
+
"outcome": outcome,
|
|
107
|
+
"metadata": metadata or {},
|
|
108
|
+
},
|
|
109
|
+
)
|
|
110
|
+
return WriteResult(**response)
|
|
111
|
+
|
|
112
|
+
async def retrieve(
|
|
113
|
+
self,
|
|
114
|
+
query: str,
|
|
115
|
+
agent_id: str,
|
|
116
|
+
top_k: int = 5,
|
|
117
|
+
include_semantic: bool = True,
|
|
118
|
+
) -> RetrieveResult:
|
|
119
|
+
"""
|
|
120
|
+
Retrieve relevant memories before your agent starts a task.
|
|
121
|
+
Inject result.injection_prompt into your LLM system prompt.
|
|
122
|
+
"""
|
|
123
|
+
if not query:
|
|
124
|
+
raise ValidationError("query is required")
|
|
125
|
+
if not agent_id:
|
|
126
|
+
raise ValidationError("agent_id is required")
|
|
127
|
+
top_k = max(1, min(top_k, 20))
|
|
128
|
+
|
|
129
|
+
raw = await self._async_request(
|
|
130
|
+
"POST",
|
|
131
|
+
"/api/v1/retrieve",
|
|
132
|
+
json={
|
|
133
|
+
"query": query,
|
|
134
|
+
"agent_id": agent_id,
|
|
135
|
+
"top_k": top_k,
|
|
136
|
+
"include_semantic": include_semantic,
|
|
137
|
+
},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Flatten episode results (Go API returns {"episode": {...}, "score": ..., "retrieval_source": ...})
|
|
141
|
+
flat_episodes: List[EpisodeResult] = []
|
|
142
|
+
for item in raw.get("episodes", []):
|
|
143
|
+
ep = item.get("episode") or {}
|
|
144
|
+
flat_episodes.append(
|
|
145
|
+
EpisodeResult(
|
|
146
|
+
episode_id=ep.get("episode_id", ""),
|
|
147
|
+
agent_id=ep.get("agent_id", ""),
|
|
148
|
+
raw_content=ep.get("raw_content", ""),
|
|
149
|
+
intent=ep.get("intent", ""),
|
|
150
|
+
domain=ep.get("domain", "general"),
|
|
151
|
+
outcome=ep.get("outcome", "unknown"),
|
|
152
|
+
importance_score=float(ep.get("importance_score", 0.5)),
|
|
153
|
+
retrieval_count=int(ep.get("retrieval_count", 0)),
|
|
154
|
+
consolidated=bool(ep.get("consolidated", False)),
|
|
155
|
+
created_at=ep.get("created_at"),
|
|
156
|
+
score=float(item.get("score", 0.0)),
|
|
157
|
+
retrieval_source=str(item.get("retrieval_source", "qdrant")),
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
sem_raw = raw.get("semantic_memories", [])
|
|
162
|
+
semantic: List[SemanticMemory] = []
|
|
163
|
+
for sm in sem_raw:
|
|
164
|
+
semantic.append(
|
|
165
|
+
SemanticMemory(
|
|
166
|
+
semantic_id=sm.get("semantic_id", ""),
|
|
167
|
+
agent_id=sm.get("agent_id", ""),
|
|
168
|
+
fact=sm.get("fact", ""),
|
|
169
|
+
confidence=float(sm.get("confidence", 0.0)),
|
|
170
|
+
evidence_count=int(sm.get("evidence_count", 0)),
|
|
171
|
+
domain=sm.get("domain", "general"),
|
|
172
|
+
fact_type=sm.get("fact_type", "pattern"),
|
|
173
|
+
created_at=sm.get("created_at"),
|
|
174
|
+
score=sm.get("score"),
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
result = RetrieveResult(
|
|
179
|
+
episodes=flat_episodes,
|
|
180
|
+
semantic_memories=semantic,
|
|
181
|
+
injection_prompt=raw.get("injection_prompt", ""),
|
|
182
|
+
latency_ms=int(raw.get("latency_ms", 0)),
|
|
183
|
+
)
|
|
184
|
+
return result
|
|
185
|
+
|
|
186
|
+
async def create_agent(
|
|
187
|
+
self,
|
|
188
|
+
name: str,
|
|
189
|
+
description: str = "",
|
|
190
|
+
) -> Agent:
|
|
191
|
+
"""Create a new agent to store memories for."""
|
|
192
|
+
if not name:
|
|
193
|
+
raise ValidationError("name is required")
|
|
194
|
+
|
|
195
|
+
req = CreateAgentRequest(name=name, description=description)
|
|
196
|
+
response = await self._async_request(
|
|
197
|
+
"POST",
|
|
198
|
+
"/api/v1/agents",
|
|
199
|
+
json=req.model_dump(),
|
|
200
|
+
)
|
|
201
|
+
return Agent(**response)
|
|
202
|
+
|
|
203
|
+
async def get_semantic_memory(
|
|
204
|
+
self,
|
|
205
|
+
agent_id: str,
|
|
206
|
+
limit: int = 20,
|
|
207
|
+
) -> List[SemanticMemory]:
|
|
208
|
+
"""Get semantic memories (consolidated facts) for an agent."""
|
|
209
|
+
if not agent_id:
|
|
210
|
+
raise ValidationError("agent_id is required")
|
|
211
|
+
response = await self._async_request(
|
|
212
|
+
"GET",
|
|
213
|
+
f"/api/v1/semantic?agent_id={agent_id}&limit={limit}",
|
|
214
|
+
)
|
|
215
|
+
items = response.get("memories") or response.get("items") or []
|
|
216
|
+
return [
|
|
217
|
+
SemanticMemory(
|
|
218
|
+
semantic_id=sm.get("semantic_id", ""),
|
|
219
|
+
agent_id=sm.get("agent_id", ""),
|
|
220
|
+
fact=sm.get("fact", ""),
|
|
221
|
+
confidence=float(sm.get("confidence", 0.0)),
|
|
222
|
+
evidence_count=int(sm.get("evidence_count", 0)),
|
|
223
|
+
domain=sm.get("domain", "general"),
|
|
224
|
+
fact_type=sm.get("fact_type", "pattern"),
|
|
225
|
+
created_at=sm.get("created_at"),
|
|
226
|
+
score=sm.get("score"),
|
|
227
|
+
)
|
|
228
|
+
for sm in items
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
async def list_episodes(
|
|
232
|
+
self,
|
|
233
|
+
agent_id: str,
|
|
234
|
+
limit: int = 20,
|
|
235
|
+
offset: int = 0,
|
|
236
|
+
domain: Optional[str] = None,
|
|
237
|
+
outcome: Optional[str] = None,
|
|
238
|
+
) -> Tuple[List[EpisodeResult], int]:
|
|
239
|
+
"""
|
|
240
|
+
List episodes for an agent. Returns (episodes, total_count).
|
|
241
|
+
|
|
242
|
+
This flattens the Go API /episodes response into EpisodeResult objects
|
|
243
|
+
with score=0 and retrieval_source="episodes".
|
|
244
|
+
"""
|
|
245
|
+
if not agent_id:
|
|
246
|
+
raise ValidationError("agent_id is required")
|
|
247
|
+
|
|
248
|
+
params = f"agent_id={agent_id}&limit={limit}&offset={offset}"
|
|
249
|
+
if domain:
|
|
250
|
+
params += f"&domain={domain}"
|
|
251
|
+
if outcome:
|
|
252
|
+
params += f"&outcome={outcome}"
|
|
253
|
+
|
|
254
|
+
response = await self._async_request("GET", f"/api/v1/episodes?{params}")
|
|
255
|
+
eps: List[EpisodeResult] = []
|
|
256
|
+
for ep in response.get("episodes", []):
|
|
257
|
+
eps.append(
|
|
258
|
+
EpisodeResult(
|
|
259
|
+
episode_id=ep.get("episode_id", ""),
|
|
260
|
+
agent_id=ep.get("agent_id", ""),
|
|
261
|
+
raw_content=ep.get("raw_content", ""),
|
|
262
|
+
intent=ep.get("intent", ""),
|
|
263
|
+
domain=ep.get("domain", "general"),
|
|
264
|
+
outcome=ep.get("outcome", "unknown"),
|
|
265
|
+
importance_score=float(ep.get("importance_score", 0.5)),
|
|
266
|
+
retrieval_count=int(ep.get("retrieval_count", 0)),
|
|
267
|
+
consolidated=bool(ep.get("consolidated", False)),
|
|
268
|
+
created_at=ep.get("created_at"),
|
|
269
|
+
score=0.0,
|
|
270
|
+
retrieval_source="episodes",
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
total = int(response.get("total", len(eps)))
|
|
274
|
+
return eps, total
|
|
275
|
+
|
|
276
|
+
async def close(self) -> None:
|
|
277
|
+
"""Close the async HTTP client. Call when done."""
|
|
278
|
+
if self._async_client is not None:
|
|
279
|
+
await self._async_client.aclose()
|
|
280
|
+
if self._sync_client is not None:
|
|
281
|
+
self._sync_client.close()
|
|
282
|
+
|
|
283
|
+
# ── SYNC WRAPPERS ─────────────────────────────────────────────
|
|
284
|
+
# For developers not using async — wraps async methods
|
|
285
|
+
|
|
286
|
+
def write_sync(self, content: str, agent_id: str, **kwargs: Any) -> WriteResult:
|
|
287
|
+
"""Synchronous version of write(). Use in non-async code."""
|
|
288
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
289
|
+
self.write(content, agent_id, **kwargs),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def retrieve_sync(self, query: str, agent_id: str, **kwargs: Any) -> RetrieveResult:
|
|
293
|
+
"""Synchronous version of retrieve(). Use in non-async code."""
|
|
294
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
295
|
+
self.retrieve(query, agent_id, **kwargs),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def create_agent_sync(self, name: str, description: str = "") -> Agent:
|
|
299
|
+
"""Synchronous version of create_agent()."""
|
|
300
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
301
|
+
self.create_agent(name, description),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# ── CONTEXT MANAGER ───────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
async def __aenter__(self) -> "REMClient":
|
|
307
|
+
return self
|
|
308
|
+
|
|
309
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore[override]
|
|
310
|
+
await self.close()
|
|
311
|
+
|
|
312
|
+
# ── PRIVATE HTTP LAYER ────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
def _get_async_client(self) -> httpx.AsyncClient:
|
|
315
|
+
if self._async_client is None or self._async_client.is_closed:
|
|
316
|
+
self._async_client = httpx.AsyncClient(
|
|
317
|
+
base_url=self.base_url,
|
|
318
|
+
headers=self._headers,
|
|
319
|
+
timeout=self.timeout,
|
|
320
|
+
)
|
|
321
|
+
return self._async_client
|
|
322
|
+
|
|
323
|
+
async def _async_request(
|
|
324
|
+
self,
|
|
325
|
+
method: str,
|
|
326
|
+
path: str,
|
|
327
|
+
**kwargs: Any,
|
|
328
|
+
) -> Dict[str, Any]:
|
|
329
|
+
client = self._get_async_client()
|
|
330
|
+
last_error: Optional[REMError] = None
|
|
331
|
+
|
|
332
|
+
for attempt in range(self.max_retries):
|
|
333
|
+
try:
|
|
334
|
+
response = await client.request(method, path, **kwargs)
|
|
335
|
+
return self._handle_response(response)
|
|
336
|
+
except httpx.ConnectError as e:
|
|
337
|
+
last_error = ConnectionError(
|
|
338
|
+
f"Cannot connect to REM API at {self.base_url}. "
|
|
339
|
+
f"Is the server running? Error: {e}",
|
|
340
|
+
)
|
|
341
|
+
if attempt < self.max_retries - 1:
|
|
342
|
+
await asyncio.sleep(2**attempt)
|
|
343
|
+
except httpx.TimeoutException:
|
|
344
|
+
last_error = APIError(
|
|
345
|
+
f"Request to {self.base_url}{path} timed out after {self.timeout}s",
|
|
346
|
+
)
|
|
347
|
+
if attempt < self.max_retries - 1:
|
|
348
|
+
await asyncio.sleep(1)
|
|
349
|
+
|
|
350
|
+
if last_error is not None:
|
|
351
|
+
raise last_error
|
|
352
|
+
raise ConnectionError("Request failed and no error captured")
|
|
353
|
+
|
|
354
|
+
def _handle_response(self, response: httpx.Response) -> Dict[str, Any]:
|
|
355
|
+
if response.status_code in (200, 201):
|
|
356
|
+
data = response.json()
|
|
357
|
+
if isinstance(data, Dict):
|
|
358
|
+
return data
|
|
359
|
+
return {"data": data}
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
error_body = response.json()
|
|
363
|
+
message = error_body.get("error", f"HTTP {response.status_code}")
|
|
364
|
+
except Exception:
|
|
365
|
+
message = f"HTTP {response.status_code}"
|
|
366
|
+
|
|
367
|
+
status = response.status_code
|
|
368
|
+
if status == 400:
|
|
369
|
+
raise ValidationError(message, status)
|
|
370
|
+
if status == 401:
|
|
371
|
+
raise AuthenticationError(message, status)
|
|
372
|
+
if status == 404:
|
|
373
|
+
raise NotFoundError(message, status)
|
|
374
|
+
if status == 429:
|
|
375
|
+
retry_after = int(response.headers.get("Retry-After", "60"))
|
|
376
|
+
raise RateLimitError(message, retry_after=retry_after)
|
|
377
|
+
raise APIError(message, status)
|
|
378
|
+
|
|
379
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class REMError(Exception):
|
|
7
|
+
"""Base exception for all REM errors."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, message: str, status_code: Optional[int] = None) -> None:
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
self.message = message
|
|
12
|
+
self.status_code = status_code
|
|
13
|
+
|
|
14
|
+
def __repr__(self) -> str:
|
|
15
|
+
return f"{self.__class__.__name__}({self.message!r})"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AuthenticationError(REMError):
|
|
19
|
+
"""Raised when API key is invalid or missing."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NotFoundError(REMError):
|
|
23
|
+
"""Raised when requested resource doesn't exist."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RateLimitError(REMError):
|
|
27
|
+
"""Raised when rate limit exceeded."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, retry_after: Optional[int] = None) -> None:
|
|
30
|
+
super().__init__(message, 429)
|
|
31
|
+
self.retry_after = retry_after
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class APIError(REMError):
|
|
35
|
+
"""Generic API error with status code."""
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ConnectionError(REMError):
|
|
39
|
+
"""Raised when cannot connect to REM API."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ValidationError(REMError):
|
|
43
|
+
"""Raised when request data is invalid."""
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoGen integration for REM (Recursive Episodic Memory).
|
|
3
|
+
|
|
4
|
+
This module provides a drop-in memory store that injects relevant context
|
|
5
|
+
from REM into AutoGen agent conversations and persists interactions back
|
|
6
|
+
to episodic memory automatically.
|
|
7
|
+
|
|
8
|
+
Usage::
|
|
9
|
+
|
|
10
|
+
from rem_memory.integrations.autogen import REMMemoryStore
|
|
11
|
+
|
|
12
|
+
store = REMMemoryStore(api_key="rem_sk_...", agent_id="agent_autogen_001")
|
|
13
|
+
|
|
14
|
+
# Retrieve relevant context string for injection into system_message
|
|
15
|
+
context = await store.get_context("user's TypeScript preferences")
|
|
16
|
+
|
|
17
|
+
# After a conversation turn, persist it
|
|
18
|
+
await store.save_turn(user_input="...", assistant_output="...")
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
import logging
|
|
24
|
+
from typing import Any, Dict, List, Optional
|
|
25
|
+
|
|
26
|
+
from ..client import REMClient
|
|
27
|
+
from ..types import RetrieveResult
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class REMMemoryStore:
|
|
33
|
+
"""
|
|
34
|
+
Async memory store for AutoGen agents backed by REM.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
api_key:
|
|
39
|
+
REM API key (``X-API-Key`` header).
|
|
40
|
+
agent_id:
|
|
41
|
+
Identifier for the AutoGen agent.
|
|
42
|
+
user_id:
|
|
43
|
+
Optional user identifier scoped to this conversation.
|
|
44
|
+
base_url:
|
|
45
|
+
Base URL for the REM Go API (default ``http://localhost:8080``).
|
|
46
|
+
top_k:
|
|
47
|
+
How many episodes/facts to retrieve for context injection.
|
|
48
|
+
include_semantic:
|
|
49
|
+
Whether to include consolidated semantic facts in retrieval.
|
|
50
|
+
inject_format:
|
|
51
|
+
``"prose"`` (paragraph) or ``"bullets"`` (markdown list).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
api_key: str,
|
|
57
|
+
agent_id: str,
|
|
58
|
+
user_id: str = "user_default",
|
|
59
|
+
base_url: str = "http://localhost:8080",
|
|
60
|
+
top_k: int = 5,
|
|
61
|
+
include_semantic: bool = True,
|
|
62
|
+
inject_format: str = "prose",
|
|
63
|
+
) -> None:
|
|
64
|
+
self.agent_id = agent_id
|
|
65
|
+
self.user_id = user_id
|
|
66
|
+
self.top_k = top_k
|
|
67
|
+
self.include_semantic = include_semantic
|
|
68
|
+
self.inject_format = inject_format
|
|
69
|
+
self._client = REMClient(api_key=api_key, base_url=base_url)
|
|
70
|
+
|
|
71
|
+
# ── Context retrieval ────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
async def get_context(self, query: str) -> str:
|
|
74
|
+
"""
|
|
75
|
+
Retrieve relevant memories and return them as a formatted string
|
|
76
|
+
suitable for injection into an AutoGen agent's system message or
|
|
77
|
+
the first turn of a conversation.
|
|
78
|
+
|
|
79
|
+
Parameters
|
|
80
|
+
----------
|
|
81
|
+
query:
|
|
82
|
+
The user input or topic to retrieve relevant memories for.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
str
|
|
87
|
+
Formatted context string, or empty string if no memories found.
|
|
88
|
+
"""
|
|
89
|
+
async with self._client:
|
|
90
|
+
result: RetrieveResult = await self._client.retrieve(
|
|
91
|
+
query=query,
|
|
92
|
+
agent_id=self.agent_id,
|
|
93
|
+
top_k=self.top_k,
|
|
94
|
+
include_semantic=self.include_semantic,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if not result.episodes and not result.semantic_memories:
|
|
98
|
+
return ""
|
|
99
|
+
|
|
100
|
+
if hasattr(result, "injection_prompt") and result.injection_prompt:
|
|
101
|
+
return result.injection_prompt
|
|
102
|
+
|
|
103
|
+
return self._format_context(result)
|
|
104
|
+
|
|
105
|
+
def get_context_sync(self, query: str) -> str:
|
|
106
|
+
"""Synchronous wrapper around :meth:`get_context`."""
|
|
107
|
+
return asyncio.get_event_loop().run_until_complete(self.get_context(query))
|
|
108
|
+
|
|
109
|
+
# ── Saving turns ─────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
async def save_turn(
|
|
112
|
+
self,
|
|
113
|
+
user_input: str,
|
|
114
|
+
assistant_output: str,
|
|
115
|
+
session_id: Optional[str] = None,
|
|
116
|
+
outcome: str = "success",
|
|
117
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Persist a conversation turn to REM episodic memory.
|
|
121
|
+
|
|
122
|
+
Parameters
|
|
123
|
+
----------
|
|
124
|
+
user_input:
|
|
125
|
+
The user message for this turn.
|
|
126
|
+
assistant_output:
|
|
127
|
+
The agent's response for this turn.
|
|
128
|
+
session_id:
|
|
129
|
+
Optional session identifier for grouping turns.
|
|
130
|
+
outcome:
|
|
131
|
+
Conversation outcome signal: ``"success"``, ``"partial"``, or ``"failure"``.
|
|
132
|
+
metadata:
|
|
133
|
+
Additional key/value pairs to attach to the episode.
|
|
134
|
+
"""
|
|
135
|
+
content = f"User: {user_input}\nAssistant: {assistant_output}"
|
|
136
|
+
async with self._client:
|
|
137
|
+
await self._client.write(
|
|
138
|
+
content=content,
|
|
139
|
+
agent_id=self.agent_id,
|
|
140
|
+
user_id=self.user_id,
|
|
141
|
+
session_id=session_id,
|
|
142
|
+
outcome=outcome,
|
|
143
|
+
metadata=metadata or {},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def save_turn_sync(self, user_input: str, assistant_output: str, **kwargs: Any) -> None:
|
|
147
|
+
"""Synchronous wrapper around :meth:`save_turn`."""
|
|
148
|
+
asyncio.get_event_loop().run_until_complete(
|
|
149
|
+
self.save_turn(user_input, assistant_output, **kwargs)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# ── AutoGen hook helpers ─────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
def build_system_prefix(self, query: str) -> str:
|
|
155
|
+
"""
|
|
156
|
+
Return a system message prefix string that injects relevant memories.
|
|
157
|
+
|
|
158
|
+
Suitable for prepending to an AutoGen agent's ``system_message``:
|
|
159
|
+
|
|
160
|
+
.. code-block:: python
|
|
161
|
+
|
|
162
|
+
prefix = store.build_system_prefix("code style preferences")
|
|
163
|
+
agent = ConversableAgent(
|
|
164
|
+
name="assistant",
|
|
165
|
+
system_message=prefix + "\\n\\nYou are a helpful coding assistant.",
|
|
166
|
+
)
|
|
167
|
+
"""
|
|
168
|
+
context = self.get_context_sync(query)
|
|
169
|
+
if not context:
|
|
170
|
+
return ""
|
|
171
|
+
return f"## Relevant memory context\\n\\n{context}\\n\\n---\\n\\n"
|
|
172
|
+
|
|
173
|
+
# ── Formatting helpers ───────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
def _format_context(self, result: RetrieveResult) -> str:
|
|
176
|
+
parts: List[str] = []
|
|
177
|
+
|
|
178
|
+
if result.semantic_memories:
|
|
179
|
+
if self.inject_format == "bullets":
|
|
180
|
+
parts.append("**Known facts about this user/agent:**")
|
|
181
|
+
for sm in result.semantic_memories:
|
|
182
|
+
parts.append(f"- {sm.fact} (confidence: {sm.confidence:.0%})")
|
|
183
|
+
else:
|
|
184
|
+
facts = "; ".join(sm.fact for sm in result.semantic_memories)
|
|
185
|
+
parts.append(f"Known facts: {facts}.")
|
|
186
|
+
|
|
187
|
+
if result.episodes:
|
|
188
|
+
if self.inject_format == "bullets":
|
|
189
|
+
parts.append("**Recent relevant episodes:**")
|
|
190
|
+
for ep in result.episodes[:3]:
|
|
191
|
+
parts.append(f"- {ep.intent} [{ep.outcome}]")
|
|
192
|
+
else:
|
|
193
|
+
recent = "; ".join(ep.intent for ep in result.episodes[:3])
|
|
194
|
+
parts.append(f"Recent interactions: {recent}.")
|
|
195
|
+
|
|
196
|
+
return "\n".join(parts)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, PrivateAttr
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from langchain.memory import BaseChatMemory
|
|
10
|
+
except ImportError as exc: # pragma: no cover - import-time guard
|
|
11
|
+
raise ImportError(
|
|
12
|
+
"langchain package required. Install with: pip install rem-memory[langchain]",
|
|
13
|
+
) from exc
|
|
14
|
+
|
|
15
|
+
from ..client import REMClient
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class REMMemory(BaseChatMemory, BaseModel):
|
|
19
|
+
"""
|
|
20
|
+
Drop-in LangChain memory backed by REM.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
from rem_memory.integrations.langchain import REMMemory
|
|
24
|
+
|
|
25
|
+
memory = REMMemory(
|
|
26
|
+
api_key="rem_sk_...",
|
|
27
|
+
agent_id="my-agent",
|
|
28
|
+
)
|
|
29
|
+
chain = ConversationChain(llm=ChatOpenAI(), memory=memory)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
api_key: str
|
|
33
|
+
agent_id: str
|
|
34
|
+
user_id: str = "default"
|
|
35
|
+
memory_key: str = "relevant_memories"
|
|
36
|
+
return_messages: bool = False
|
|
37
|
+
|
|
38
|
+
_client: Optional[REMClient] = PrivateAttr(default=None)
|
|
39
|
+
|
|
40
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
41
|
+
|
|
42
|
+
def model_post_init(self, __context: Any) -> None: # type: ignore[override]
|
|
43
|
+
base_url = os.getenv("REM_BASE_URL", "https://api.rem.ai")
|
|
44
|
+
self._client = REMClient(api_key=self.api_key, base_url=base_url)
|
|
45
|
+
super().model_post_init(__context) # type: ignore[misc]
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def memory_variables(self) -> List[str]:
|
|
49
|
+
return [self.memory_key]
|
|
50
|
+
|
|
51
|
+
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
52
|
+
"""Called before LLM invoke — fetches relevant memories."""
|
|
53
|
+
query = (
|
|
54
|
+
inputs.get("input")
|
|
55
|
+
or inputs.get("question")
|
|
56
|
+
or inputs.get("human_input")
|
|
57
|
+
or ""
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if not query or self._client is None:
|
|
61
|
+
return {self.memory_key: ""}
|
|
62
|
+
|
|
63
|
+
result = self._client.retrieve_sync(
|
|
64
|
+
query=query,
|
|
65
|
+
agent_id=self.agent_id,
|
|
66
|
+
top_k=5,
|
|
67
|
+
include_semantic=True,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
return {self.memory_key: result.injection_prompt}
|
|
71
|
+
|
|
72
|
+
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, Any]) -> None:
|
|
73
|
+
"""Called after LLM response — writes episode to REM."""
|
|
74
|
+
if self._client is None:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
human_input = (
|
|
78
|
+
inputs.get("input")
|
|
79
|
+
or inputs.get("question")
|
|
80
|
+
or inputs.get("human_input")
|
|
81
|
+
or ""
|
|
82
|
+
)
|
|
83
|
+
ai_output = (
|
|
84
|
+
outputs.get("response")
|
|
85
|
+
or outputs.get("output")
|
|
86
|
+
or outputs.get("text")
|
|
87
|
+
or ""
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if not human_input:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
content = f"Human: {human_input}\nAssistant: {ai_output}"
|
|
94
|
+
|
|
95
|
+
self._client.write_sync(
|
|
96
|
+
content=content,
|
|
97
|
+
agent_id=self.agent_id,
|
|
98
|
+
user_id=self.user_id,
|
|
99
|
+
outcome="success",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def clear(self) -> None:
|
|
103
|
+
"""REM memories persist. This is intentional."""
|
|
104
|
+
return
|
|
105
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, ConfigDict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _ensure_aware(dt: datetime) -> datetime:
|
|
10
|
+
"""Ensure datetimes are timezone-aware (UTC)."""
|
|
11
|
+
if dt.tzinfo is None:
|
|
12
|
+
return dt.replace(tzinfo=timezone.utc)
|
|
13
|
+
return dt
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseModelTZ(BaseModel):
|
|
17
|
+
"""Base model that normalises datetimes to timezone-aware UTC."""
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
20
|
+
|
|
21
|
+
def model_post_init(self, __context) -> None: # type: ignore[override]
|
|
22
|
+
for field_name, field in self.model_fields.items(): # type: ignore[attr-defined]
|
|
23
|
+
value = getattr(self, field_name)
|
|
24
|
+
if isinstance(value, datetime):
|
|
25
|
+
setattr(self, field_name, _ensure_aware(value))
|
|
26
|
+
super().model_post_init(__context) # type: ignore[misc]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class WriteResult(BaseModelTZ):
|
|
30
|
+
episode_id: str
|
|
31
|
+
agent_id: str
|
|
32
|
+
created_at: datetime
|
|
33
|
+
status: str # "stored"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class EpisodeResult(BaseModelTZ):
|
|
37
|
+
episode_id: str
|
|
38
|
+
agent_id: str
|
|
39
|
+
raw_content: str
|
|
40
|
+
intent: str
|
|
41
|
+
domain: str
|
|
42
|
+
outcome: str
|
|
43
|
+
importance_score: float
|
|
44
|
+
retrieval_count: int
|
|
45
|
+
consolidated: bool
|
|
46
|
+
created_at: datetime
|
|
47
|
+
score: float # retrieval relevance score
|
|
48
|
+
retrieval_source: str # "qdrant" | "graph" | "semantic" | "episodes"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SemanticMemory(BaseModelTZ):
|
|
52
|
+
semantic_id: str
|
|
53
|
+
agent_id: str
|
|
54
|
+
fact: str
|
|
55
|
+
confidence: float
|
|
56
|
+
evidence_count: int
|
|
57
|
+
domain: str
|
|
58
|
+
fact_type: str # "preference" | "rule" | "pattern" | "skill" | "fact"
|
|
59
|
+
created_at: datetime
|
|
60
|
+
score: Optional[float] = None # if retrieved, has relevance score
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class RetrieveResult(BaseModelTZ):
|
|
64
|
+
episodes: List[EpisodeResult]
|
|
65
|
+
semantic_memories: List[SemanticMemory]
|
|
66
|
+
injection_prompt: str
|
|
67
|
+
latency_ms: int
|
|
68
|
+
|
|
69
|
+
def to_prompt(self) -> str:
|
|
70
|
+
"""Alias for injection_prompt — most common usage."""
|
|
71
|
+
return self.injection_prompt
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class Agent(BaseModelTZ):
|
|
75
|
+
agent_id: str
|
|
76
|
+
name: str
|
|
77
|
+
description: str
|
|
78
|
+
total_episodes: int
|
|
79
|
+
total_semantic_memories: int
|
|
80
|
+
last_active_at: Optional[datetime]
|
|
81
|
+
created_at: datetime
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class CreateAgentRequest(BaseModelTZ):
|
|
85
|
+
name: str
|
|
86
|
+
description: str = ""
|
|
87
|
+
|
|
88
|
+
|