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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/rem-memory)](https://pypi.org/project/rem-memory/)
45
+ [![Python](https://img.shields.io/pypi/pyversions/rem-memory)](https://pypi.org/project/rem-memory/)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/rem-memory)](https://pypi.org/project/rem-memory/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/rem-memory)](https://pypi.org/project/rem-memory/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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
+