autogen-substrate-memory 0.1.0__py3-none-any.whl
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.
- autogen_substrate/__init__.py +6 -0
- autogen_substrate/client.py +173 -0
- autogen_substrate/memory.py +346 -0
- autogen_substrate/py.typed +0 -0
- autogen_substrate_memory-0.1.0.dist-info/METADATA +137 -0
- autogen_substrate_memory-0.1.0.dist-info/RECORD +7 -0
- autogen_substrate_memory-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Lightweight async HTTP client for the SUBSTRATE MCP server.
|
|
2
|
+
|
|
3
|
+
Sends JSON-RPC 2.0 requests over HTTPS with Bearer token authentication.
|
|
4
|
+
All responses are validated and errors are surfaced as typed exceptions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger("autogen_substrate.client")
|
|
17
|
+
|
|
18
|
+
DEFAULT_MCP_URL = "https://substrate.garmolabs.com/mcp-server/mcp"
|
|
19
|
+
DEFAULT_TIMEOUT_SECONDS = 30.0
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SubstrateClientError(Exception):
|
|
23
|
+
"""Base exception for SUBSTRATE client errors."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SubstrateAuthError(SubstrateClientError):
|
|
27
|
+
"""Raised when the API key is invalid or missing."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SubstrateRPCError(SubstrateClientError):
|
|
31
|
+
"""Raised when the MCP server returns a JSON-RPC error."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, code: int, message: str, data: Any = None) -> None:
|
|
34
|
+
self.code = code
|
|
35
|
+
self.rpc_message = message
|
|
36
|
+
self.data = data
|
|
37
|
+
super().__init__(f"JSON-RPC error {code}: {message}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class RPCResponse:
|
|
42
|
+
"""Immutable wrapper around a successful JSON-RPC result."""
|
|
43
|
+
|
|
44
|
+
id: int | str | None
|
|
45
|
+
result: Any
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class SubstrateClientConfig:
|
|
50
|
+
"""Immutable configuration for the SUBSTRATE MCP client."""
|
|
51
|
+
|
|
52
|
+
api_key: str
|
|
53
|
+
mcp_url: str = DEFAULT_MCP_URL
|
|
54
|
+
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS
|
|
55
|
+
extra_headers: dict[str, str] = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SubstrateClient:
|
|
59
|
+
"""Async HTTP client for SUBSTRATE MCP JSON-RPC calls.
|
|
60
|
+
|
|
61
|
+
Usage::
|
|
62
|
+
|
|
63
|
+
config = SubstrateClientConfig(api_key="sk_sub_...")
|
|
64
|
+
async with SubstrateClient(config) as client:
|
|
65
|
+
result = await client.call_tool("memory_search", {"query": "hello"})
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, config: SubstrateClientConfig) -> None:
|
|
69
|
+
if not config.api_key:
|
|
70
|
+
raise SubstrateAuthError("SUBSTRATE API key must not be empty")
|
|
71
|
+
self._config = config
|
|
72
|
+
self._request_id = 0
|
|
73
|
+
self._http: httpx.AsyncClient | None = None
|
|
74
|
+
|
|
75
|
+
async def __aenter__(self) -> SubstrateClient:
|
|
76
|
+
self._http = httpx.AsyncClient(
|
|
77
|
+
base_url=self._config.mcp_url,
|
|
78
|
+
headers={
|
|
79
|
+
"Authorization": f"Bearer {self._config.api_key}",
|
|
80
|
+
"Content-Type": "application/json",
|
|
81
|
+
**self._config.extra_headers,
|
|
82
|
+
},
|
|
83
|
+
timeout=httpx.Timeout(self._config.timeout_seconds),
|
|
84
|
+
)
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
88
|
+
if self._http is not None:
|
|
89
|
+
await self._http.aclose()
|
|
90
|
+
self._http = None
|
|
91
|
+
|
|
92
|
+
def _next_id(self) -> int:
|
|
93
|
+
self._request_id += 1
|
|
94
|
+
return self._request_id
|
|
95
|
+
|
|
96
|
+
async def _send_rpc(self, method: str, params: dict[str, Any] | None = None) -> RPCResponse:
|
|
97
|
+
"""Send a JSON-RPC 2.0 request and return the parsed response."""
|
|
98
|
+
if self._http is None:
|
|
99
|
+
raise SubstrateClientError("Client not initialized. Use 'async with' context manager.")
|
|
100
|
+
|
|
101
|
+
request_id = self._next_id()
|
|
102
|
+
payload: dict[str, Any] = {
|
|
103
|
+
"jsonrpc": "2.0",
|
|
104
|
+
"id": request_id,
|
|
105
|
+
"method": method,
|
|
106
|
+
}
|
|
107
|
+
if params is not None:
|
|
108
|
+
payload["params"] = params
|
|
109
|
+
|
|
110
|
+
logger.debug("MCP request: method=%s id=%d", method, request_id)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
response = await self._http.post("", content=json.dumps(payload))
|
|
114
|
+
except httpx.TimeoutException as exc:
|
|
115
|
+
raise SubstrateClientError(f"Request timed out after {self._config.timeout_seconds}s") from exc
|
|
116
|
+
except httpx.HTTPError as exc:
|
|
117
|
+
raise SubstrateClientError(f"HTTP error: {exc}") from exc
|
|
118
|
+
|
|
119
|
+
if response.status_code == 401:
|
|
120
|
+
raise SubstrateAuthError("Invalid or expired API key")
|
|
121
|
+
if response.status_code == 429:
|
|
122
|
+
raise SubstrateClientError("Rate limit exceeded")
|
|
123
|
+
if response.status_code >= 400:
|
|
124
|
+
raise SubstrateClientError(
|
|
125
|
+
f"HTTP {response.status_code}: {response.text[:200]}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
body = response.json()
|
|
130
|
+
except (json.JSONDecodeError, ValueError) as exc:
|
|
131
|
+
raise SubstrateClientError(f"Invalid JSON response: {response.text[:200]}") from exc
|
|
132
|
+
|
|
133
|
+
if "error" in body and body["error"] is not None:
|
|
134
|
+
err = body["error"]
|
|
135
|
+
raise SubstrateRPCError(
|
|
136
|
+
code=err.get("code", -32603),
|
|
137
|
+
message=err.get("message", "Unknown error"),
|
|
138
|
+
data=err.get("data"),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return RPCResponse(id=body.get("id"), result=body.get("result"))
|
|
142
|
+
|
|
143
|
+
async def call_tool(self, tool_name: str, arguments: dict[str, Any] | None = None) -> Any:
|
|
144
|
+
"""Call an MCP tool and return the result content.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
tool_name: Name of the SUBSTRATE MCP tool (e.g. ``hybrid_search``).
|
|
148
|
+
arguments: Tool-specific arguments dict.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The ``result`` field from the JSON-RPC response.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
SubstrateRPCError: If the server returns a JSON-RPC error.
|
|
155
|
+
SubstrateAuthError: If authentication fails.
|
|
156
|
+
SubstrateClientError: On network or protocol errors.
|
|
157
|
+
"""
|
|
158
|
+
params: dict[str, Any] = {"name": tool_name}
|
|
159
|
+
if arguments:
|
|
160
|
+
params["arguments"] = arguments
|
|
161
|
+
|
|
162
|
+
rpc = await self._send_rpc("tools/call", params)
|
|
163
|
+
return rpc.result
|
|
164
|
+
|
|
165
|
+
async def list_tools(self) -> list[dict[str, Any]]:
|
|
166
|
+
"""List all available MCP tools for the authenticated tier."""
|
|
167
|
+
rpc = await self._send_rpc("tools/list")
|
|
168
|
+
result = rpc.result
|
|
169
|
+
if isinstance(result, dict) and "tools" in result:
|
|
170
|
+
return result["tools"]
|
|
171
|
+
if isinstance(result, list):
|
|
172
|
+
return result
|
|
173
|
+
return []
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""SUBSTRATE Memory provider for Microsoft AutoGen agents.
|
|
2
|
+
|
|
3
|
+
Implements the ``autogen_core.memory.Memory`` protocol, backed by the
|
|
4
|
+
SUBSTRATE cognitive entity framework. Queries combine hybrid search with
|
|
5
|
+
emotional state awareness; context injection provides identity, emotion,
|
|
6
|
+
and relevant memories as a system message.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any, Sequence
|
|
16
|
+
|
|
17
|
+
from autogen_core.memory import Memory, MemoryContent, MemoryMimeType, MemoryQueryResult
|
|
18
|
+
from autogen_core.model_context import ChatCompletionContext
|
|
19
|
+
from autogen_core.models import SystemMessage
|
|
20
|
+
|
|
21
|
+
from autogen_substrate.client import (
|
|
22
|
+
SubstrateClient,
|
|
23
|
+
SubstrateClientConfig,
|
|
24
|
+
SubstrateClientError,
|
|
25
|
+
SubstrateRPCError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("autogen_substrate.memory")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _extract_text(content: Any) -> str:
|
|
32
|
+
"""Extract text from various MCP tool response shapes."""
|
|
33
|
+
if isinstance(content, str):
|
|
34
|
+
return content
|
|
35
|
+
if isinstance(content, dict):
|
|
36
|
+
# MCP content blocks: [{"type": "text", "text": "..."}]
|
|
37
|
+
if "content" in content and isinstance(content["content"], list):
|
|
38
|
+
parts = []
|
|
39
|
+
for block in content["content"]:
|
|
40
|
+
if isinstance(block, dict) and block.get("type") == "text":
|
|
41
|
+
parts.append(block.get("text", ""))
|
|
42
|
+
return "\n".join(parts)
|
|
43
|
+
# Direct text field
|
|
44
|
+
if "text" in content:
|
|
45
|
+
return str(content["text"])
|
|
46
|
+
return json.dumps(content, indent=2, default=str)
|
|
47
|
+
if isinstance(content, list):
|
|
48
|
+
parts = []
|
|
49
|
+
for item in content:
|
|
50
|
+
parts.append(_extract_text(item))
|
|
51
|
+
return "\n".join(parts)
|
|
52
|
+
return str(content)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class SubstrateMemoryConfig:
|
|
57
|
+
"""Immutable configuration for SubstrateMemory.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
api_key: SUBSTRATE API key. Falls back to ``SUBSTRATE_API_KEY`` env var.
|
|
61
|
+
mcp_url: MCP server URL. Falls back to ``SUBSTRATE_MCP_URL`` env var.
|
|
62
|
+
name: Display name for this memory provider.
|
|
63
|
+
search_top_k: Default number of results for hybrid search.
|
|
64
|
+
include_emotion: Whether to include emotional state in queries and context.
|
|
65
|
+
include_identity: Whether to include identity verification in context.
|
|
66
|
+
include_values: Whether to include core values in context.
|
|
67
|
+
timeout_seconds: HTTP request timeout.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
api_key: str = ""
|
|
71
|
+
mcp_url: str = ""
|
|
72
|
+
name: str = "substrate"
|
|
73
|
+
search_top_k: int = 5
|
|
74
|
+
include_emotion: bool = True
|
|
75
|
+
include_identity: bool = True
|
|
76
|
+
include_values: bool = True
|
|
77
|
+
timeout_seconds: float = 30.0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class SubstrateMemory(Memory):
|
|
81
|
+
"""SUBSTRATE-backed memory for AutoGen agents.
|
|
82
|
+
|
|
83
|
+
Provides cognitive memory, emotional state, identity verification, and
|
|
84
|
+
value-aligned context injection for AutoGen agent conversations.
|
|
85
|
+
|
|
86
|
+
Usage::
|
|
87
|
+
|
|
88
|
+
from autogen_substrate import SubstrateMemory
|
|
89
|
+
|
|
90
|
+
memory = SubstrateMemory(api_key="sk_sub_...")
|
|
91
|
+
await memory.add("User prefers concise responses")
|
|
92
|
+
result = await memory.query("What does the user prefer?")
|
|
93
|
+
print(result.results)
|
|
94
|
+
|
|
95
|
+
Or with full config::
|
|
96
|
+
|
|
97
|
+
from autogen_substrate.memory import SubstrateMemory, SubstrateMemoryConfig
|
|
98
|
+
|
|
99
|
+
config = SubstrateMemoryConfig(
|
|
100
|
+
api_key="sk_sub_...",
|
|
101
|
+
search_top_k=10,
|
|
102
|
+
include_emotion=True,
|
|
103
|
+
)
|
|
104
|
+
memory = SubstrateMemory(config=config)
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
*,
|
|
110
|
+
api_key: str = "",
|
|
111
|
+
config: SubstrateMemoryConfig | None = None,
|
|
112
|
+
) -> None:
|
|
113
|
+
resolved_config = config or SubstrateMemoryConfig(api_key=api_key)
|
|
114
|
+
|
|
115
|
+
resolved_key = (
|
|
116
|
+
resolved_config.api_key
|
|
117
|
+
or os.environ.get("SUBSTRATE_API_KEY", "")
|
|
118
|
+
)
|
|
119
|
+
resolved_url = (
|
|
120
|
+
resolved_config.mcp_url
|
|
121
|
+
or os.environ.get("SUBSTRATE_MCP_URL", "")
|
|
122
|
+
or "https://substrate.garmolabs.com/mcp-server/mcp"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self._config = SubstrateMemoryConfig(
|
|
126
|
+
api_key=resolved_key,
|
|
127
|
+
mcp_url=resolved_url,
|
|
128
|
+
name=resolved_config.name,
|
|
129
|
+
search_top_k=resolved_config.search_top_k,
|
|
130
|
+
include_emotion=resolved_config.include_emotion,
|
|
131
|
+
include_identity=resolved_config.include_identity,
|
|
132
|
+
include_values=resolved_config.include_values,
|
|
133
|
+
timeout_seconds=resolved_config.timeout_seconds,
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self._client_config = SubstrateClientConfig(
|
|
137
|
+
api_key=self._config.api_key,
|
|
138
|
+
mcp_url=self._config.mcp_url,
|
|
139
|
+
timeout_seconds=self._config.timeout_seconds,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def name(self) -> str:
|
|
144
|
+
"""Display name for this memory provider."""
|
|
145
|
+
return self._config.name
|
|
146
|
+
|
|
147
|
+
async def query(
|
|
148
|
+
self,
|
|
149
|
+
query: str,
|
|
150
|
+
*,
|
|
151
|
+
cancellation_token: Any | None = None,
|
|
152
|
+
**kwargs: Any,
|
|
153
|
+
) -> MemoryQueryResult:
|
|
154
|
+
"""Query SUBSTRATE for relevant memories and emotional context.
|
|
155
|
+
|
|
156
|
+
Performs a hybrid search (semantic + keyword) across the entity's memory
|
|
157
|
+
and knowledge stores. Optionally includes the current emotional state
|
|
158
|
+
as additional context.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
query: Natural language search query.
|
|
162
|
+
cancellation_token: Optional cancellation token (unused, kept for protocol).
|
|
163
|
+
**kwargs: Additional arguments. Supports ``top_k`` override.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
MemoryQueryResult with matching memory contents.
|
|
167
|
+
"""
|
|
168
|
+
top_k = kwargs.get("top_k", self._config.search_top_k)
|
|
169
|
+
results: list[MemoryContent] = []
|
|
170
|
+
|
|
171
|
+
async with SubstrateClient(self._client_config) as client:
|
|
172
|
+
# Hybrid search for relevant memories
|
|
173
|
+
try:
|
|
174
|
+
search_result = await client.call_tool(
|
|
175
|
+
"hybrid_search",
|
|
176
|
+
{"query": query, "top_k": top_k},
|
|
177
|
+
)
|
|
178
|
+
search_text = _extract_text(search_result)
|
|
179
|
+
if search_text.strip():
|
|
180
|
+
results.append(
|
|
181
|
+
MemoryContent(
|
|
182
|
+
content=search_text,
|
|
183
|
+
mime_type=MemoryMimeType.TEXT,
|
|
184
|
+
metadata={"source": "substrate_hybrid_search", "query": query},
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
except (SubstrateRPCError, SubstrateClientError) as exc:
|
|
188
|
+
logger.warning("hybrid_search failed, falling back to memory_search: %s", exc)
|
|
189
|
+
try:
|
|
190
|
+
fallback = await client.call_tool("memory_search", {"query": query})
|
|
191
|
+
fallback_text = _extract_text(fallback)
|
|
192
|
+
if fallback_text.strip():
|
|
193
|
+
results.append(
|
|
194
|
+
MemoryContent(
|
|
195
|
+
content=fallback_text,
|
|
196
|
+
mime_type=MemoryMimeType.TEXT,
|
|
197
|
+
metadata={"source": "substrate_memory_search", "query": query},
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
except (SubstrateRPCError, SubstrateClientError) as fallback_exc:
|
|
201
|
+
logger.error("memory_search also failed: %s", fallback_exc)
|
|
202
|
+
|
|
203
|
+
# Include emotional state if configured
|
|
204
|
+
if self._config.include_emotion:
|
|
205
|
+
try:
|
|
206
|
+
emotion_result = await client.call_tool("get_emotion_state")
|
|
207
|
+
emotion_text = _extract_text(emotion_result)
|
|
208
|
+
if emotion_text.strip():
|
|
209
|
+
results.append(
|
|
210
|
+
MemoryContent(
|
|
211
|
+
content=f"[Emotional State]\n{emotion_text}",
|
|
212
|
+
mime_type=MemoryMimeType.TEXT,
|
|
213
|
+
metadata={"source": "substrate_emotion"},
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
except (SubstrateRPCError, SubstrateClientError) as exc:
|
|
217
|
+
logger.debug("get_emotion_state unavailable: %s", exc)
|
|
218
|
+
|
|
219
|
+
return MemoryQueryResult(results=results)
|
|
220
|
+
|
|
221
|
+
async def update_context(
|
|
222
|
+
self,
|
|
223
|
+
model_context: ChatCompletionContext,
|
|
224
|
+
) -> None:
|
|
225
|
+
"""Inject SUBSTRATE cognitive context into the agent's model context.
|
|
226
|
+
|
|
227
|
+
Prepends a system message containing the entity's identity status,
|
|
228
|
+
emotional state, core values, and any recent relevant memories.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
model_context: The agent's current chat completion context.
|
|
232
|
+
"""
|
|
233
|
+
sections: list[str] = []
|
|
234
|
+
|
|
235
|
+
async with SubstrateClient(self._client_config) as client:
|
|
236
|
+
# Identity verification
|
|
237
|
+
if self._config.include_identity:
|
|
238
|
+
try:
|
|
239
|
+
identity = await client.call_tool("verify_identity")
|
|
240
|
+
identity_text = _extract_text(identity)
|
|
241
|
+
if identity_text.strip():
|
|
242
|
+
sections.append(f"## Identity Continuity\n{identity_text}")
|
|
243
|
+
except (SubstrateRPCError, SubstrateClientError) as exc:
|
|
244
|
+
logger.debug("verify_identity unavailable: %s", exc)
|
|
245
|
+
|
|
246
|
+
# Emotional state
|
|
247
|
+
if self._config.include_emotion:
|
|
248
|
+
try:
|
|
249
|
+
emotion = await client.call_tool("get_emotion_state")
|
|
250
|
+
emotion_text = _extract_text(emotion)
|
|
251
|
+
if emotion_text.strip():
|
|
252
|
+
sections.append(f"## Emotional State\n{emotion_text}")
|
|
253
|
+
except (SubstrateRPCError, SubstrateClientError) as exc:
|
|
254
|
+
logger.debug("get_emotion_state unavailable: %s", exc)
|
|
255
|
+
|
|
256
|
+
# Core values
|
|
257
|
+
if self._config.include_values:
|
|
258
|
+
try:
|
|
259
|
+
values = await client.call_tool("get_values")
|
|
260
|
+
values_text = _extract_text(values)
|
|
261
|
+
if values_text.strip():
|
|
262
|
+
sections.append(f"## Core Values\n{values_text}")
|
|
263
|
+
except (SubstrateRPCError, SubstrateClientError) as exc:
|
|
264
|
+
logger.debug("get_values unavailable: %s", exc)
|
|
265
|
+
|
|
266
|
+
# Recent context from conversation messages
|
|
267
|
+
existing = await model_context.get_messages()
|
|
268
|
+
context_query = _build_context_query(existing)
|
|
269
|
+
if context_query:
|
|
270
|
+
try:
|
|
271
|
+
memories = await client.call_tool(
|
|
272
|
+
"hybrid_search",
|
|
273
|
+
{"query": context_query, "top_k": 3},
|
|
274
|
+
)
|
|
275
|
+
mem_text = _extract_text(memories)
|
|
276
|
+
if mem_text.strip():
|
|
277
|
+
sections.append(f"## Relevant Memories\n{mem_text}")
|
|
278
|
+
except (SubstrateRPCError, SubstrateClientError) as exc:
|
|
279
|
+
logger.debug("Context memory search unavailable: %s", exc)
|
|
280
|
+
|
|
281
|
+
if sections:
|
|
282
|
+
system_text = (
|
|
283
|
+
"# SUBSTRATE Cognitive Context\n\n"
|
|
284
|
+
+ "\n\n".join(sections)
|
|
285
|
+
)
|
|
286
|
+
await model_context.add_message(SystemMessage(content=system_text))
|
|
287
|
+
|
|
288
|
+
async def add(
|
|
289
|
+
self,
|
|
290
|
+
content: str,
|
|
291
|
+
*,
|
|
292
|
+
cancellation_token: Any | None = None,
|
|
293
|
+
**kwargs: Any,
|
|
294
|
+
) -> None:
|
|
295
|
+
"""Store a memory in SUBSTRATE via the respond tool.
|
|
296
|
+
|
|
297
|
+
Sends the content with a ``[memory-store]`` prefix so the entity
|
|
298
|
+
processes it as an explicit memory rather than conversational input.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
content: The memory content to store.
|
|
302
|
+
cancellation_token: Optional cancellation token (unused).
|
|
303
|
+
**kwargs: Reserved for future use.
|
|
304
|
+
"""
|
|
305
|
+
async with SubstrateClient(self._client_config) as client:
|
|
306
|
+
try:
|
|
307
|
+
await client.call_tool(
|
|
308
|
+
"respond",
|
|
309
|
+
{"message": f"[memory-store] {content}"},
|
|
310
|
+
)
|
|
311
|
+
except (SubstrateRPCError, SubstrateClientError) as exc:
|
|
312
|
+
logger.error("Failed to store memory: %s", exc)
|
|
313
|
+
raise
|
|
314
|
+
|
|
315
|
+
async def clear(self) -> None:
|
|
316
|
+
"""No-op. SUBSTRATE manages its own memory lifecycle.
|
|
317
|
+
|
|
318
|
+
SUBSTRATE entities maintain causal memory with built-in consolidation
|
|
319
|
+
and decay. Explicit clearing is not supported to preserve identity
|
|
320
|
+
continuity.
|
|
321
|
+
"""
|
|
322
|
+
logger.debug("clear() called — SUBSTRATE manages its own memory lifecycle")
|
|
323
|
+
|
|
324
|
+
async def close(self) -> None:
|
|
325
|
+
"""No-op. Each operation creates its own HTTP connection.
|
|
326
|
+
|
|
327
|
+
The client uses context managers per-call, so there is no persistent
|
|
328
|
+
connection to close.
|
|
329
|
+
"""
|
|
330
|
+
logger.debug("close() called — no persistent resources to release")
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _build_context_query(messages: Sequence[Any]) -> str:
|
|
334
|
+
"""Build a search query from the most recent user messages.
|
|
335
|
+
|
|
336
|
+
Extracts text from the last few messages to form a contextual query
|
|
337
|
+
for memory retrieval.
|
|
338
|
+
"""
|
|
339
|
+
texts: list[str] = []
|
|
340
|
+
for msg in messages[-5:]:
|
|
341
|
+
content = getattr(msg, "content", None)
|
|
342
|
+
if isinstance(content, str) and content.strip():
|
|
343
|
+
texts.append(content.strip())
|
|
344
|
+
combined = " ".join(texts)
|
|
345
|
+
# Truncate to a reasonable query length
|
|
346
|
+
return combined[:500] if combined else ""
|
|
File without changes
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: autogen-substrate-memory
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: SUBSTRATE cognitive memory provider for Microsoft AutoGen agents
|
|
5
|
+
Project-URL: Homepage, https://garmolabs.com
|
|
6
|
+
Project-URL: Documentation, https://garmolabs.com/substrate/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/GarmoLabs/autogen-substrate-memory
|
|
8
|
+
Author-email: Garmo Labs <hello@garmolabs.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai-agents,autogen,cognitive,memory,substrate
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: autogen-agentchat>=0.4
|
|
23
|
+
Requires-Dist: autogen-core>=0.4
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# autogen-substrate-memory
|
|
35
|
+
|
|
36
|
+
SUBSTRATE cognitive memory provider for [Microsoft AutoGen](https://github.com/microsoft/autogen) agents.
|
|
37
|
+
|
|
38
|
+
Give your AutoGen agents persistent identity, emotional awareness, and causal memory powered by the [SUBSTRATE](https://garmolabs.com) cognitive entity framework.
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install autogen-substrate-memory
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or install from source:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install -e ".[dev]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Quick Start
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
import asyncio
|
|
56
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
57
|
+
from autogen_ext.models.openai import OpenAIChatCompletionClient
|
|
58
|
+
from autogen_substrate import SubstrateMemory
|
|
59
|
+
|
|
60
|
+
async def main():
|
|
61
|
+
# Create SUBSTRATE memory provider
|
|
62
|
+
memory = SubstrateMemory(api_key="sk_sub_your_key_here")
|
|
63
|
+
|
|
64
|
+
# Create an AutoGen agent with SUBSTRATE memory
|
|
65
|
+
agent = AssistantAgent(
|
|
66
|
+
name="kai",
|
|
67
|
+
model_client=OpenAIChatCompletionClient(model="gpt-4o"),
|
|
68
|
+
memory=[memory],
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# The agent now has access to persistent cognitive memory,
|
|
72
|
+
# emotional state, and identity verification.
|
|
73
|
+
response = await agent.run(task="What do you remember about our last conversation?")
|
|
74
|
+
print(response)
|
|
75
|
+
|
|
76
|
+
asyncio.run(main())
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
### Environment Variables
|
|
82
|
+
|
|
83
|
+
| Variable | Description | Default |
|
|
84
|
+
|---|---|---|
|
|
85
|
+
| `SUBSTRATE_API_KEY` | Your SUBSTRATE API key | (required) |
|
|
86
|
+
| `SUBSTRATE_MCP_URL` | MCP server URL | `https://substrate.garmolabs.com/mcp-server/mcp` |
|
|
87
|
+
|
|
88
|
+
### Full Configuration
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from autogen_substrate.memory import SubstrateMemory, SubstrateMemoryConfig
|
|
92
|
+
|
|
93
|
+
config = SubstrateMemoryConfig(
|
|
94
|
+
api_key="sk_sub_...",
|
|
95
|
+
mcp_url="https://substrate.garmolabs.com/mcp-server/mcp",
|
|
96
|
+
search_top_k=10, # Number of memory results per query
|
|
97
|
+
include_emotion=True, # Include emotional state in queries
|
|
98
|
+
include_identity=True, # Include identity verification in context
|
|
99
|
+
include_values=True, # Include core values in context
|
|
100
|
+
timeout_seconds=30.0, # HTTP request timeout
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
memory = SubstrateMemory(config=config)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## How It Works
|
|
107
|
+
|
|
108
|
+
### Memory Protocol
|
|
109
|
+
|
|
110
|
+
`SubstrateMemory` implements AutoGen's `Memory` protocol:
|
|
111
|
+
|
|
112
|
+
| Method | Behavior |
|
|
113
|
+
|---|---|
|
|
114
|
+
| `query(query)` | Hybrid search (semantic + keyword) across entity memory. Optionally includes emotional state. |
|
|
115
|
+
| `update_context(model_context)` | Injects identity, emotion, values, and relevant memories as a SystemMessage. |
|
|
116
|
+
| `add(content)` | Stores content via the SUBSTRATE `respond` tool with a `[memory-store]` prefix. |
|
|
117
|
+
| `clear()` | No-op. SUBSTRATE manages its own memory lifecycle with causal consolidation. |
|
|
118
|
+
| `close()` | No-op. Each operation creates its own HTTP connection. |
|
|
119
|
+
|
|
120
|
+
### SUBSTRATE MCP Tools Used
|
|
121
|
+
|
|
122
|
+
| Tool | Used In | Purpose |
|
|
123
|
+
|---|---|---|
|
|
124
|
+
| `hybrid_search` | `query()`, `update_context()` | Semantic + keyword memory retrieval |
|
|
125
|
+
| `memory_search` | `query()` (fallback) | Keyword-only memory retrieval |
|
|
126
|
+
| `get_emotion_state` | `query()`, `update_context()` | Current affective state (valence, arousal, dominance) |
|
|
127
|
+
| `verify_identity` | `update_context()` | Cryptographic identity continuity check |
|
|
128
|
+
| `get_values` | `update_context()` | Core value architecture |
|
|
129
|
+
| `respond` | `add()` | Store new memories via conversational input |
|
|
130
|
+
|
|
131
|
+
## Get an API Key
|
|
132
|
+
|
|
133
|
+
Sign up at [garmolabs.com](https://garmolabs.com) to get a SUBSTRATE API key.
|
|
134
|
+
|
|
135
|
+
## License
|
|
136
|
+
|
|
137
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
autogen_substrate/__init__.py,sha256=Z8GhWwdJAxB8nzDxqDOalpXXSimzejSJGZC-i2mAdus,179
|
|
2
|
+
autogen_substrate/client.py,sha256=5Fcy32-vLUmM4UG3mZL9HRwKa7GMDzl4wN7PN8qPKNo,5858
|
|
3
|
+
autogen_substrate/memory.py,sha256=V0f1t24WDVYvm97eG6hhsstK6-hnDxBRt3wLXxpY_QI,13330
|
|
4
|
+
autogen_substrate/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
autogen_substrate_memory-0.1.0.dist-info/METADATA,sha256=cPNxxRm6_G329J2YLVa6AMHiHBxf4_9Kqr1nCX1p6TE,4756
|
|
6
|
+
autogen_substrate_memory-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
autogen_substrate_memory-0.1.0.dist-info/RECORD,,
|