uams-sdk 1.0.1__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.
- uams_sdk-1.0.1/PKG-INFO +20 -0
- uams_sdk-1.0.1/README.md +93 -0
- uams_sdk-1.0.1/pyproject.toml +37 -0
- uams_sdk-1.0.1/setup.cfg +4 -0
- uams_sdk-1.0.1/tests/test_mcp_identity.py +30 -0
- uams_sdk-1.0.1/uams_sdk/__init__.py +5 -0
- uams_sdk-1.0.1/uams_sdk/cache.py +24 -0
- uams_sdk-1.0.1/uams_sdk/client.py +183 -0
- uams_sdk-1.0.1/uams_sdk/exceptions.py +14 -0
- uams_sdk-1.0.1/uams_sdk/mcp_server.py +249 -0
- uams_sdk-1.0.1/uams_sdk/middleware.py +114 -0
- uams_sdk-1.0.1/uams_sdk.egg-info/PKG-INFO +20 -0
- uams_sdk-1.0.1/uams_sdk.egg-info/SOURCES.txt +15 -0
- uams_sdk-1.0.1/uams_sdk.egg-info/dependency_links.txt +1 -0
- uams_sdk-1.0.1/uams_sdk.egg-info/entry_points.txt +2 -0
- uams_sdk-1.0.1/uams_sdk.egg-info/requires.txt +8 -0
- uams_sdk-1.0.1/uams_sdk.egg-info/top_level.txt +1 -0
uams_sdk-1.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uams-sdk
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Shared Memory SDK for Hermes and OpenClaw targeting the Unified Agent Memory System.
|
|
5
|
+
Author: Shivam Sharma
|
|
6
|
+
Project-URL: Homepage, https://github.com/Shivamsharma6/unified_memory
|
|
7
|
+
Project-URL: Repository, https://github.com/Shivamsharma6/unified_memory.git
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: httpx>=0.27.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Requires-Dist: cachetools>=5.3.0
|
|
17
|
+
Requires-Dist: mcp[cli]<1.13,>=1.12.4
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
uams_sdk-1.0.1/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# UAMS Shared Memory SDK
|
|
2
|
+
|
|
3
|
+
A unified, asynchronous Python SDK for the **Unified Agent Memory System (UAMS)**.
|
|
4
|
+
Designed to be shared natively by both **Hermes** and **OpenClaw** agents.
|
|
5
|
+
|
|
6
|
+
## Features
|
|
7
|
+
- **Unified Client:** One standard interface for all agents.
|
|
8
|
+
- **Async First:** Built on `httpx` for non-blocking operations.
|
|
9
|
+
- **Intelligent Caching:** Built-in TTL caching with automatic invalidation on writes.
|
|
10
|
+
- **Full API Coverage:** Semantic search, graph traversal, context compression, and writes.
|
|
11
|
+
- **MCP Adapter:** Exposes UAMS as standard MCP tools, resources, and prompts for agent tool discovery.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
```bash
|
|
15
|
+
pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
```python
|
|
20
|
+
import asyncio
|
|
21
|
+
from uams_sdk import UAMSClient
|
|
22
|
+
|
|
23
|
+
async def main():
|
|
24
|
+
client = UAMSClient(base_url="http://localhost:8000")
|
|
25
|
+
task = "How do I fix Docker file locks?"
|
|
26
|
+
|
|
27
|
+
# Default preflight before the agent works.
|
|
28
|
+
memory = await client.begin_task(task)
|
|
29
|
+
print(memory["context"])
|
|
30
|
+
|
|
31
|
+
# Default write-back after durable work.
|
|
32
|
+
await client.end_task(
|
|
33
|
+
task=task,
|
|
34
|
+
outcome="Documented the Docker file-lock workaround.",
|
|
35
|
+
entities=["Docker File Locks"],
|
|
36
|
+
tags=["#procedure"],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
asyncio.run(main())
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## MCP Server
|
|
43
|
+
|
|
44
|
+
Run the adapter from the repository root:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
./uams mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Or run the installed console entry point:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
UAMS_API_URL=http://localhost:8000 uams-mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Agent MCP config:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"mcpServers": {
|
|
61
|
+
"uams": {
|
|
62
|
+
"command": "/absolute/path/to/unified_memory/uams",
|
|
63
|
+
"args": ["mcp"],
|
|
64
|
+
"env": {
|
|
65
|
+
"UAMS_API_URL": "http://localhost:8000"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Generate snippets from the repository root:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
./uams mcp-config all
|
|
76
|
+
./uams mcp-config codex
|
|
77
|
+
./uams mcp-config json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Discovered MCP capabilities:
|
|
81
|
+
|
|
82
|
+
- Tools: `begin_task`, `end_task`, `health`, `search_memory`, `get_context`, `get_procedures`, `remember`, `get_related_entities`, `summarize_memory`, `store_fix_summary`
|
|
83
|
+
- Resource: `uams://memory-policy`
|
|
84
|
+
- Prompt: `use_uams_memory`
|
|
85
|
+
|
|
86
|
+
Agent default protocol:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
Before each non-trivial task, call `begin_task`.
|
|
90
|
+
During work, call `search_memory` when more recall is needed.
|
|
91
|
+
After durable work, call `end_task`.
|
|
92
|
+
Never store raw transcripts; store distilled facts, decisions, fixes, and procedures.
|
|
93
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "uams-sdk"
|
|
7
|
+
version = "1.0.1"
|
|
8
|
+
description = "Shared Memory SDK for Hermes and OpenClaw targeting the Unified Agent Memory System."
|
|
9
|
+
authors = [{ name = "Shivam Sharma" }]
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"httpx>=0.27.0",
|
|
13
|
+
"pydantic>=2.0.0",
|
|
14
|
+
"cachetools>=5.3.0",
|
|
15
|
+
"mcp[cli]>=1.12.4,<1.13"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/Shivamsharma6/unified_memory"
|
|
28
|
+
Repository = "https://github.com/Shivamsharma6/unified_memory.git"
|
|
29
|
+
|
|
30
|
+
[project.scripts]
|
|
31
|
+
uams-mcp = "uams_sdk.mcp_server:main"
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest",
|
|
36
|
+
"pytest-asyncio"
|
|
37
|
+
]
|
uams_sdk-1.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from uams_sdk.mcp_server import mcp
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@pytest.mark.asyncio
|
|
6
|
+
async def test_get_identity_tool_exists():
|
|
7
|
+
tools = await mcp.list_tools()
|
|
8
|
+
tool_names = [t.name for t in tools]
|
|
9
|
+
assert "get_identity" in tool_names
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.mark.asyncio
|
|
13
|
+
async def test_inject_identity_tool_exists():
|
|
14
|
+
tools = await mcp.list_tools()
|
|
15
|
+
tool_names = [t.name for t in tools]
|
|
16
|
+
assert "inject_identity" in tool_names
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@pytest.mark.asyncio
|
|
20
|
+
async def test_extract_identity_tool_exists():
|
|
21
|
+
tools = await mcp.list_tools()
|
|
22
|
+
tool_names = [t.name for t in tools]
|
|
23
|
+
assert "extract_identity" in tool_names
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.asyncio
|
|
27
|
+
async def test_memory_quality_tool_exists():
|
|
28
|
+
tools = await mcp.list_tools()
|
|
29
|
+
tool_names = [t.name for t in tools]
|
|
30
|
+
assert "memory_quality" in tool_names
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from cachetools import TTLCache
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
class SDKCache:
|
|
7
|
+
"""In-memory TTL cache for SDK requests to reduce API load."""
|
|
8
|
+
def __init__(self, maxsize: int = 1000, ttl: int = 300):
|
|
9
|
+
self.cache = TTLCache(maxsize=maxsize, ttl=ttl)
|
|
10
|
+
|
|
11
|
+
def _generate_key(self, endpoint: str, params: Dict[str, Any]) -> str:
|
|
12
|
+
param_str = json.dumps(params, sort_keys=True)
|
|
13
|
+
return hashlib.md5(f"{endpoint}:{param_str}".encode()).hexdigest()
|
|
14
|
+
|
|
15
|
+
def get(self, endpoint: str, params: Dict[str, Any]) -> Any:
|
|
16
|
+
key = self._generate_key(endpoint, params)
|
|
17
|
+
return self.cache.get(key)
|
|
18
|
+
|
|
19
|
+
def set(self, endpoint: str, params: Dict[str, Any], data: Any):
|
|
20
|
+
key = self._generate_key(endpoint, params)
|
|
21
|
+
self.cache[key] = data
|
|
22
|
+
|
|
23
|
+
def clear(self):
|
|
24
|
+
self.cache.clear()
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import logging
|
|
3
|
+
import json
|
|
4
|
+
from datetime import date
|
|
5
|
+
from typing import Dict, Any, List, Optional
|
|
6
|
+
from .cache import SDKCache
|
|
7
|
+
from .exceptions import UAMSError, UAMSConnectionError, UAMSAPIError
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
class UAMSClient:
|
|
12
|
+
"""
|
|
13
|
+
Unified Agent Memory System (UAMS) SDK Client.
|
|
14
|
+
Shared across Hermes, OpenClaw, and VoiceAI.
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self, base_url: str = "http://localhost:8000", cache_ttl: int = 300):
|
|
17
|
+
self.base_url = base_url
|
|
18
|
+
self.timeout = httpx.Timeout(15.0, connect=5.0)
|
|
19
|
+
self.cache = SDKCache(ttl=cache_ttl)
|
|
20
|
+
|
|
21
|
+
async def _request(self, method: str, endpoint: str, json_data: Dict[str, Any] = None, use_cache: bool = False) -> Dict[str, Any]:
|
|
22
|
+
if use_cache and method == "POST":
|
|
23
|
+
cached = self.cache.get(endpoint, json_data or {})
|
|
24
|
+
if cached:
|
|
25
|
+
logger.debug(f"Cache hit for {endpoint}")
|
|
26
|
+
return cached
|
|
27
|
+
|
|
28
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
29
|
+
try:
|
|
30
|
+
if method == "POST":
|
|
31
|
+
response = await client.post(f"{self.base_url}{endpoint}", json=json_data or {})
|
|
32
|
+
elif method == "GET":
|
|
33
|
+
response = await client.get(f"{self.base_url}{endpoint}", params=json_data)
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError(f"Unsupported method: {method}")
|
|
36
|
+
|
|
37
|
+
response.raise_for_status()
|
|
38
|
+
data = response.json()
|
|
39
|
+
|
|
40
|
+
if use_cache and method == "POST":
|
|
41
|
+
self.cache.set(endpoint, json_data or {}, data)
|
|
42
|
+
|
|
43
|
+
return data
|
|
44
|
+
|
|
45
|
+
except httpx.HTTPStatusError as e:
|
|
46
|
+
raise UAMSAPIError(f"API Error: {e.response.status_code}", status_code=e.response.status_code, details=e.response.text)
|
|
47
|
+
except httpx.RequestError as e:
|
|
48
|
+
raise UAMSConnectionError(f"Connection error to UAMS {endpoint}: {str(e)}")
|
|
49
|
+
|
|
50
|
+
async def search(self, query: str, limit: int = 5, entities: List[str] = None, compress: bool = True) -> Dict[str, Any]:
|
|
51
|
+
"""Semantic + Graph hybrid retrieval."""
|
|
52
|
+
payload = {
|
|
53
|
+
"query": query,
|
|
54
|
+
"limit": limit,
|
|
55
|
+
"entities": entities or [],
|
|
56
|
+
"compress": compress
|
|
57
|
+
}
|
|
58
|
+
return await self._request("POST", "/search", payload, use_cache=True)
|
|
59
|
+
|
|
60
|
+
async def retrieve_context(self, task: str, max_tokens: int = 2000) -> str:
|
|
61
|
+
"""Highly compressed context assembly for LLM prompting."""
|
|
62
|
+
res = await self._request("POST", "/context", {"task": task, "max_tokens": max_tokens}, use_cache=True)
|
|
63
|
+
return res.get("context", "")
|
|
64
|
+
|
|
65
|
+
async def retrieve_procedures(self, task: str) -> List[str]:
|
|
66
|
+
"""Fetch procedural memories (AGENTS.md / SOPs)."""
|
|
67
|
+
res = await self._request("POST", f"/procedures", {"task": task}, use_cache=True)
|
|
68
|
+
return res.get("procedures", [])
|
|
69
|
+
|
|
70
|
+
async def begin_task(self, task: str, max_tokens: int = 2000) -> Dict[str, Any]:
|
|
71
|
+
"""Default memory preflight agents should call before doing work."""
|
|
72
|
+
procedures = await self.retrieve_procedures(task)
|
|
73
|
+
context = await self.retrieve_context(task, max_tokens=max_tokens)
|
|
74
|
+
return {
|
|
75
|
+
"task": task,
|
|
76
|
+
"status": "ready",
|
|
77
|
+
"procedures": procedures,
|
|
78
|
+
"context": context,
|
|
79
|
+
"max_tokens": max_tokens,
|
|
80
|
+
"memory_policy": (
|
|
81
|
+
"Always call begin_task before non-trivial work. Use the procedures and "
|
|
82
|
+
"context as grounding. Call search_memory when recall is needed during "
|
|
83
|
+
"the task. Call end_task after durable work to store distilled outcomes."
|
|
84
|
+
),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async def store_memory(self, text: str, category: str = "episodic", tags: List[str] = None) -> bool:
|
|
88
|
+
"""Agent memory write support. Clears cache to ensure fresh reads."""
|
|
89
|
+
payload = {"text": text, "category": category, "tags": tags or []}
|
|
90
|
+
try:
|
|
91
|
+
await self._request("POST", "/remember", payload, use_cache=False)
|
|
92
|
+
self.cache.clear() # Invalidate cache on write
|
|
93
|
+
return True
|
|
94
|
+
except UAMSError as e:
|
|
95
|
+
logger.error(f"Failed to store memory: {e}")
|
|
96
|
+
return False
|
|
97
|
+
|
|
98
|
+
async def end_task(
|
|
99
|
+
self,
|
|
100
|
+
task: str,
|
|
101
|
+
outcome: str,
|
|
102
|
+
files: Optional[List[str]] = None,
|
|
103
|
+
decisions: Optional[List[str]] = None,
|
|
104
|
+
fixes: Optional[List[str]] = None,
|
|
105
|
+
entities: Optional[List[str]] = None,
|
|
106
|
+
tags: Optional[List[str]] = None,
|
|
107
|
+
category: str = "episodic",
|
|
108
|
+
) -> Dict[str, Any]:
|
|
109
|
+
"""Store a distilled task outcome after durable work completes."""
|
|
110
|
+
all_tags = list(dict.fromkeys((tags or []) + ["#auto-distilled", "#task-outcome"]))
|
|
111
|
+
entity_links = " ".join(f"[[{entity}]]" for entity in entities or [])
|
|
112
|
+
file_lines = "\n".join(f"- `{path}`" for path in files or []) or "- Not specified"
|
|
113
|
+
decision_lines = "\n".join(f"- {item}" for item in decisions or []) or "- None recorded"
|
|
114
|
+
fix_lines = "\n".join(f"- {item}" for item in fixes or []) or "- None recorded"
|
|
115
|
+
today = date.today().isoformat()
|
|
116
|
+
|
|
117
|
+
tags_json = json.dumps(all_tags)
|
|
118
|
+
text = f"""---
|
|
119
|
+
type: {category}
|
|
120
|
+
date: {today}
|
|
121
|
+
tags: {tags_json}
|
|
122
|
+
---
|
|
123
|
+
# Task Outcome: {task}
|
|
124
|
+
|
|
125
|
+
## TL;DR
|
|
126
|
+
{outcome.strip()}
|
|
127
|
+
|
|
128
|
+
## Entities
|
|
129
|
+
{entity_links or f"[[{task}]]"}
|
|
130
|
+
|
|
131
|
+
## Files
|
|
132
|
+
{file_lines}
|
|
133
|
+
|
|
134
|
+
## Decisions
|
|
135
|
+
{decision_lines}
|
|
136
|
+
|
|
137
|
+
## Fixes
|
|
138
|
+
{fix_lines}
|
|
139
|
+
|
|
140
|
+
## Retrieval Notes
|
|
141
|
+
Future agents should search for [[{task}]], the listed entities, and the listed files before repeating related work.
|
|
142
|
+
"""
|
|
143
|
+
ok = await self.store_memory(text=text, category=category, tags=all_tags)
|
|
144
|
+
return {"ok": ok, "category": category, "tags": all_tags}
|
|
145
|
+
|
|
146
|
+
async def distill_memory(self, topic: str) -> str:
|
|
147
|
+
"""Trigger summarization/distillation of a topic."""
|
|
148
|
+
res = await self._request("POST", "/summarize", {"topic": topic}, use_cache=False)
|
|
149
|
+
return res.get("summary", "")
|
|
150
|
+
|
|
151
|
+
async def related_entities(self, entity: str, radius: int = 1) -> Dict[str, Any]:
|
|
152
|
+
"""Graph retrieval: neighborhood expansion."""
|
|
153
|
+
try:
|
|
154
|
+
return await self._request("GET", f"/graph/neighborhood/{entity}", {"radius": radius}, use_cache=True)
|
|
155
|
+
except UAMSAPIError as e:
|
|
156
|
+
if getattr(e, 'status_code', None) == 404:
|
|
157
|
+
return {"error": f"Entity '{entity}' not found in knowledge graph.", "nodes": [], "links": []}
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
async def get_identity(self, entity_id: str = "default") -> Dict[str, Any]:
|
|
161
|
+
"""Get identity profile."""
|
|
162
|
+
return await self._request("POST", "/identity/profile", {"entity_id": entity_id}, use_cache=True)
|
|
163
|
+
|
|
164
|
+
async def inject_identity(
|
|
165
|
+
self, entity_id: str = "default", query: str = "", task_type: str = "general"
|
|
166
|
+
) -> Dict[str, Any]:
|
|
167
|
+
"""Inject identity into reasoning."""
|
|
168
|
+
return await self._request("POST", "/identity/inject", {
|
|
169
|
+
"entity_id": entity_id, "query": query, "task_type": task_type
|
|
170
|
+
}, use_cache=False)
|
|
171
|
+
|
|
172
|
+
async def extract_identity(
|
|
173
|
+
self, entity_id: str = "default", entity_name: str = "Agent",
|
|
174
|
+
memories: Optional[List[Dict[str, Any]]] = None,
|
|
175
|
+
) -> Dict[str, Any]:
|
|
176
|
+
"""Extract identity from memories."""
|
|
177
|
+
return await self._request("POST", "/identity/extract", {
|
|
178
|
+
"entity_id": entity_id, "entity_name": entity_name, "memories": memories or []
|
|
179
|
+
}, use_cache=False)
|
|
180
|
+
|
|
181
|
+
async def memory_quality(self, path: str) -> Dict[str, Any]:
|
|
182
|
+
"""Score memory quality."""
|
|
183
|
+
return await self._request("POST", "/memory/quality", {"path": path}, use_cache=True)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class UAMSError(Exception):
|
|
2
|
+
"""Base exception for UAMS SDK."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
class UAMSConnectionError(UAMSError):
|
|
6
|
+
"""Raised when the API is unreachable."""
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
class UAMSAPIError(UAMSError):
|
|
10
|
+
"""Raised when the API returns an error response."""
|
|
11
|
+
def __init__(self, message: str, status_code: int = None, details: str = None):
|
|
12
|
+
super().__init__(message)
|
|
13
|
+
self.status_code = status_code
|
|
14
|
+
self.details = details
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import date
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from mcp.server.fastmcp import FastMCP
|
|
6
|
+
|
|
7
|
+
from .client import UAMSClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_BASE_URL = os.getenv("UAMS_API_URL", "http://localhost:8000")
|
|
11
|
+
|
|
12
|
+
mcp = FastMCP(
|
|
13
|
+
"Unified Agent Memory",
|
|
14
|
+
instructions=(
|
|
15
|
+
"Use UAMS as the default shared memory backend. Before coding or answering, "
|
|
16
|
+
"call begin_task for relevant procedures and context. After durable work, "
|
|
17
|
+
"call end_task with distilled, non-transcript outcomes. Use search_memory "
|
|
18
|
+
"during work when additional recall is needed."
|
|
19
|
+
),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _client() -> UAMSClient:
|
|
24
|
+
return UAMSClient(base_url=os.getenv("UAMS_API_URL", DEFAULT_BASE_URL))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@mcp.resource("uams://memory-policy")
|
|
28
|
+
def memory_policy() -> str:
|
|
29
|
+
"""Default operating policy agents should follow when UAMS is available."""
|
|
30
|
+
return """# UAMS Default Memory Policy
|
|
31
|
+
|
|
32
|
+
Before each task:
|
|
33
|
+
- Call `begin_task` for task-specific rules, compressed historical context, and graph context.
|
|
34
|
+
- Use `search_memory` when targeted lookup is needed.
|
|
35
|
+
|
|
36
|
+
After each task:
|
|
37
|
+
- Call `end_task` with distilled outcomes.
|
|
38
|
+
- Store only durable facts, decisions, fixes, and procedures.
|
|
39
|
+
- Never store raw chat transcripts.
|
|
40
|
+
- Prefer wikilinks like `[[Entity Name]]` and tags like `#bugfix`.
|
|
41
|
+
- Use `store_fix_summary` for bug fixes so future retrieval can explain cause and resolution.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mcp.prompt(title="Use UAMS Memory")
|
|
46
|
+
def use_uams_memory(task: str) -> str:
|
|
47
|
+
"""Prompt template that makes an agent default to UAMS for a task."""
|
|
48
|
+
return f"""You have access to Unified Agent Memory System tools.
|
|
49
|
+
|
|
50
|
+
Task: {task}
|
|
51
|
+
|
|
52
|
+
Protocol:
|
|
53
|
+
1. Call `begin_task` with the task.
|
|
54
|
+
2. Use the returned procedures and context as grounding before acting.
|
|
55
|
+
3. Use `search_memory` during work when additional recall is needed.
|
|
56
|
+
4. After completing durable work, call `end_task`.
|
|
57
|
+
5. Store distilled atomic memory only, never raw conversation."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@mcp.tool()
|
|
61
|
+
async def health() -> dict[str, Any]:
|
|
62
|
+
"""Check whether the UAMS Retrieval API is reachable."""
|
|
63
|
+
return await _client()._request("GET", "/health", use_cache=False)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@mcp.tool()
|
|
67
|
+
async def search_memory(
|
|
68
|
+
query: str,
|
|
69
|
+
limit: int = 5,
|
|
70
|
+
entities: list[str] | None = None,
|
|
71
|
+
compress: bool = True,
|
|
72
|
+
) -> dict[str, Any]:
|
|
73
|
+
"""Search UAMS using hybrid semantic and graph-aware retrieval."""
|
|
74
|
+
return await _client().search(
|
|
75
|
+
query=query,
|
|
76
|
+
limit=limit,
|
|
77
|
+
entities=entities or [],
|
|
78
|
+
compress=compress,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@mcp.tool()
|
|
83
|
+
async def begin_task(task: str, max_tokens: int = 2000) -> dict[str, Any]:
|
|
84
|
+
"""Default first call before work: retrieve procedures, context, and memory policy."""
|
|
85
|
+
return await _client().begin_task(task=task, max_tokens=max_tokens)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@mcp.tool()
|
|
89
|
+
async def get_context(task: str, max_tokens: int = 2000) -> dict[str, Any]:
|
|
90
|
+
"""Return compressed memory context for an agent task."""
|
|
91
|
+
context = await _client().retrieve_context(task=task, max_tokens=max_tokens)
|
|
92
|
+
return {"task": task, "context": context, "max_tokens": max_tokens}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@mcp.tool()
|
|
96
|
+
async def get_procedures(task: str) -> dict[str, Any]:
|
|
97
|
+
"""Return procedural memories and operating rules relevant to a task."""
|
|
98
|
+
procedures = await _client().retrieve_procedures(task=task)
|
|
99
|
+
return {"task": task, "procedures": procedures}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@mcp.tool()
|
|
103
|
+
async def remember(
|
|
104
|
+
text: str,
|
|
105
|
+
category: str = "episodic",
|
|
106
|
+
tags: list[str] | None = None,
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
"""Store a distilled memory in UAMS. Do not use for raw transcripts."""
|
|
109
|
+
ok = await _client().store_memory(
|
|
110
|
+
text=text,
|
|
111
|
+
category=category,
|
|
112
|
+
tags=tags or [],
|
|
113
|
+
)
|
|
114
|
+
return {"ok": ok, "category": category, "tags": tags or []}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@mcp.tool()
|
|
118
|
+
async def end_task(
|
|
119
|
+
task: str,
|
|
120
|
+
outcome: str,
|
|
121
|
+
files: list[str] | None = None,
|
|
122
|
+
decisions: list[str] | None = None,
|
|
123
|
+
fixes: list[str] | None = None,
|
|
124
|
+
entities: list[str] | None = None,
|
|
125
|
+
tags: list[str] | None = None,
|
|
126
|
+
category: str = "episodic",
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
"""Default final call after durable work: store distilled task outcome memory."""
|
|
129
|
+
return await _client().end_task(
|
|
130
|
+
task=task,
|
|
131
|
+
outcome=outcome,
|
|
132
|
+
files=files or [],
|
|
133
|
+
decisions=decisions or [],
|
|
134
|
+
fixes=fixes or [],
|
|
135
|
+
entities=entities or [],
|
|
136
|
+
tags=tags or [],
|
|
137
|
+
category=category,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@mcp.tool()
|
|
142
|
+
async def get_related_entities(entity: str, radius: int = 1) -> dict[str, Any]:
|
|
143
|
+
"""Fetch a graph neighborhood around an entity."""
|
|
144
|
+
return await _client().related_entities(entity=entity, radius=radius)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@mcp.tool()
|
|
148
|
+
async def summarize_memory(topic: str) -> dict[str, Any]:
|
|
149
|
+
"""Ask UAMS to generate or retrieve a semantic summary for a topic."""
|
|
150
|
+
summary = await _client().distill_memory(topic=topic)
|
|
151
|
+
return {"topic": topic, "summary": summary}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@mcp.tool()
|
|
155
|
+
async def store_fix_summary(
|
|
156
|
+
issue: str,
|
|
157
|
+
cause: str,
|
|
158
|
+
resolution: str,
|
|
159
|
+
files: list[str] | None = None,
|
|
160
|
+
entities: list[str] | None = None,
|
|
161
|
+
tags: list[str] | None = None,
|
|
162
|
+
) -> dict[str, Any]:
|
|
163
|
+
"""Store a durable bug-fix memory with cause, resolution, files, and entities."""
|
|
164
|
+
all_tags = list(dict.fromkeys((tags or []) + ["#bugfix", "#auto-distilled"]))
|
|
165
|
+
linked_entities = " ".join(f"[[{entity}]]" for entity in entities or [])
|
|
166
|
+
file_list = "\n".join(f"- `{path}`" for path in files or [])
|
|
167
|
+
today = date.today().isoformat()
|
|
168
|
+
|
|
169
|
+
import json
|
|
170
|
+
tags_json = json.dumps(all_tags)
|
|
171
|
+
text = f"""---
|
|
172
|
+
type: procedural
|
|
173
|
+
date: {today}
|
|
174
|
+
tags: {tags_json}
|
|
175
|
+
---
|
|
176
|
+
# Fix Summary: {issue}
|
|
177
|
+
|
|
178
|
+
## TL;DR
|
|
179
|
+
[[{issue}]] was caused by {cause} and resolved by {resolution}.
|
|
180
|
+
|
|
181
|
+
## Entities
|
|
182
|
+
{linked_entities or f"[[{issue}]]"}
|
|
183
|
+
|
|
184
|
+
## Files
|
|
185
|
+
{file_list or "- Not specified"}
|
|
186
|
+
|
|
187
|
+
## Cause
|
|
188
|
+
{cause}
|
|
189
|
+
|
|
190
|
+
## Resolution
|
|
191
|
+
{resolution}
|
|
192
|
+
|
|
193
|
+
## Retrieval Notes
|
|
194
|
+
Future agents should search for [[{issue}]], related files, and the listed entities before re-debugging this class of issue.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
ok = await _client().store_memory(text=text, category="procedural", tags=all_tags)
|
|
198
|
+
return {
|
|
199
|
+
"ok": ok,
|
|
200
|
+
"issue": issue,
|
|
201
|
+
"category": "procedural",
|
|
202
|
+
"tags": all_tags,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@mcp.tool()
|
|
207
|
+
async def get_identity(entity_id: str = "default") -> dict[str, Any]:
|
|
208
|
+
"""Get the identity profile for an entity (traits, confidence, version)."""
|
|
209
|
+
return await _client().get_identity(entity_id=entity_id)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@mcp.tool()
|
|
213
|
+
async def inject_identity(
|
|
214
|
+
entity_id: str = "default",
|
|
215
|
+
query: str = "",
|
|
216
|
+
task_type: str = "general",
|
|
217
|
+
) -> dict[str, Any]:
|
|
218
|
+
"""Inject identity context into agent reasoning for personalized responses."""
|
|
219
|
+
return await _client().inject_identity(
|
|
220
|
+
entity_id=entity_id, query=query, task_type=task_type
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@mcp.tool()
|
|
225
|
+
async def extract_identity(
|
|
226
|
+
entity_id: str = "default",
|
|
227
|
+
entity_name: str = "Agent",
|
|
228
|
+
memories: list[dict[str, Any]] | None = None,
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
"""Extract identity traits from episodic memories."""
|
|
231
|
+
return await _client().extract_identity(
|
|
232
|
+
entity_id=entity_id,
|
|
233
|
+
entity_name=entity_name,
|
|
234
|
+
memories=memories or [],
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@mcp.tool()
|
|
239
|
+
async def memory_quality(path: str) -> dict[str, Any]:
|
|
240
|
+
"""Score a memory note's quality and completeness."""
|
|
241
|
+
return await _client().memory_quality(path=path)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def main() -> None:
|
|
245
|
+
mcp.run()
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
main()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import re
|
|
3
|
+
from typing import Dict, Any, List, Optional
|
|
4
|
+
from .client import UAMSClient
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
class AutonomousMemoryMiddleware:
|
|
9
|
+
"""
|
|
10
|
+
Advanced Automatic Memory Middleware for Hermes & OpenClaw.
|
|
11
|
+
Automatically orchestrates memory injection before tasks and knowledge extraction after tasks.
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self, client: Optional[UAMSClient] = None):
|
|
14
|
+
self.client = client or UAMSClient()
|
|
15
|
+
self.max_context_tokens = 2000
|
|
16
|
+
|
|
17
|
+
def _detect_task_type(self, prompt: str) -> str:
|
|
18
|
+
"""1. Detect task type via heuristic."""
|
|
19
|
+
q = prompt.lower()
|
|
20
|
+
if any(w in q for w in ["code", "script", "debug", "error", "refactor", "build"]):
|
|
21
|
+
return "coding"
|
|
22
|
+
if any(w in q for w in ["explain", "research", "what is", "why", "how does"]):
|
|
23
|
+
return "research"
|
|
24
|
+
return "general"
|
|
25
|
+
|
|
26
|
+
async def pre_task(self, user_prompt: str) -> str:
|
|
27
|
+
"""
|
|
28
|
+
Executed BEFORE the agent sees the prompt.
|
|
29
|
+
1. Detect task type
|
|
30
|
+
2. Retrieve procedures first
|
|
31
|
+
3. Retrieve relevant memory automatically
|
|
32
|
+
4. Compress context
|
|
33
|
+
5. Inject memory into prompt
|
|
34
|
+
"""
|
|
35
|
+
task_type = self._detect_task_type(user_prompt)
|
|
36
|
+
logger.info(f"[UAMS Middleware] Task detected as '{task_type}'. Fetching memory...")
|
|
37
|
+
|
|
38
|
+
memory_packet = await self.client.begin_task(
|
|
39
|
+
task=user_prompt,
|
|
40
|
+
max_tokens=self.max_context_tokens,
|
|
41
|
+
)
|
|
42
|
+
procedures = memory_packet.get("procedures", [])
|
|
43
|
+
proc_text = "\n".join(procedures) if procedures else "No specific operating procedures found."
|
|
44
|
+
context = memory_packet.get("context", "")
|
|
45
|
+
context_text = context if context else "No historical context found."
|
|
46
|
+
|
|
47
|
+
# 5. Inject memory into prompt
|
|
48
|
+
augmented_prompt = f"""<uams_context>
|
|
49
|
+
[System] Task Type: {task_type.capitalize()}
|
|
50
|
+
|
|
51
|
+
[Operating Procedures]
|
|
52
|
+
{proc_text}
|
|
53
|
+
|
|
54
|
+
[Historical Memory & Graph Context]
|
|
55
|
+
{context_text}
|
|
56
|
+
</uams_context>
|
|
57
|
+
|
|
58
|
+
User Request: {user_prompt}"""
|
|
59
|
+
|
|
60
|
+
return augmented_prompt
|
|
61
|
+
|
|
62
|
+
def _extract_insights(self, response: str) -> List[Dict[str, Any]]:
|
|
63
|
+
"""
|
|
64
|
+
Heuristic to extract durable knowledge from the agent's response.
|
|
65
|
+
In production, this could be a lightweight LLM call.
|
|
66
|
+
"""
|
|
67
|
+
insights = []
|
|
68
|
+
|
|
69
|
+
# Look for explicit decisions, fixes, or procedures in the text
|
|
70
|
+
fix_match = re.search(r'(?i)(?:I fixed this by|The solution was|Resolution:)(.*?)(?:\n\n|$)', response, re.DOTALL)
|
|
71
|
+
if fix_match:
|
|
72
|
+
insights.append({"text": f"Fix applied: {fix_match.group(1).strip()}", "category": "procedural"})
|
|
73
|
+
|
|
74
|
+
decision_match = re.search(r'(?i)(?:I have decided to|Decision:)(.*?)(?:\n\n|$)', response, re.DOTALL)
|
|
75
|
+
if decision_match:
|
|
76
|
+
insights.append({"text": f"Decision made: {decision_match.group(1).strip()}", "category": "semantic"})
|
|
77
|
+
|
|
78
|
+
# Fallback to episodic if we just completed a significant task successfully
|
|
79
|
+
if not insights and ("successfully" in response.lower() or "completed" in response.lower()):
|
|
80
|
+
# We'll just take the first sentence as an episodic summary
|
|
81
|
+
first_sentence = response.split('.')[0] + "."
|
|
82
|
+
insights.append({"text": f"Task completed: {first_sentence}", "category": "episodic"})
|
|
83
|
+
|
|
84
|
+
return insights
|
|
85
|
+
|
|
86
|
+
async def post_task(self, user_prompt: str, agent_response: str) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Executed AFTER the agent finishes the task.
|
|
89
|
+
1. Determine if durable knowledge was generated
|
|
90
|
+
2. Distill reusable insights
|
|
91
|
+
3. Store procedural memory
|
|
92
|
+
4. Update graph relationships (Triggered by the store endpoint internally)
|
|
93
|
+
"""
|
|
94
|
+
logger.info("[UAMS Middleware] Evaluating task output for durable knowledge...")
|
|
95
|
+
|
|
96
|
+
insights = self._extract_insights(agent_response)
|
|
97
|
+
|
|
98
|
+
if not insights:
|
|
99
|
+
logger.debug("[UAMS Middleware] No durable knowledge detected in response.")
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
for insight in insights:
|
|
103
|
+
logger.info(f"[UAMS Middleware] Distilled Insight ({insight['category']}): {insight['text'][:50]}...")
|
|
104
|
+
|
|
105
|
+
# 3 & 4. Store memory and update relationships
|
|
106
|
+
# The UAMS watcher handles graph relationship updates upon file creation
|
|
107
|
+
await self.client.end_task(
|
|
108
|
+
task=user_prompt,
|
|
109
|
+
outcome=insight["text"],
|
|
110
|
+
tags=[f"#{self._detect_task_type(user_prompt)}"],
|
|
111
|
+
category=insight["category"],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
logger.info("[UAMS Middleware] Knowledge successfully synchronized to the Unified Memory System.")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: uams-sdk
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Shared Memory SDK for Hermes and OpenClaw targeting the Unified Agent Memory System.
|
|
5
|
+
Author: Shivam Sharma
|
|
6
|
+
Project-URL: Homepage, https://github.com/Shivamsharma6/unified_memory
|
|
7
|
+
Project-URL: Repository, https://github.com/Shivamsharma6/unified_memory.git
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: httpx>=0.27.0
|
|
15
|
+
Requires-Dist: pydantic>=2.0.0
|
|
16
|
+
Requires-Dist: cachetools>=5.3.0
|
|
17
|
+
Requires-Dist: mcp[cli]<1.13,>=1.12.4
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
tests/test_mcp_identity.py
|
|
4
|
+
uams_sdk/__init__.py
|
|
5
|
+
uams_sdk/cache.py
|
|
6
|
+
uams_sdk/client.py
|
|
7
|
+
uams_sdk/exceptions.py
|
|
8
|
+
uams_sdk/mcp_server.py
|
|
9
|
+
uams_sdk/middleware.py
|
|
10
|
+
uams_sdk.egg-info/PKG-INFO
|
|
11
|
+
uams_sdk.egg-info/SOURCES.txt
|
|
12
|
+
uams_sdk.egg-info/dependency_links.txt
|
|
13
|
+
uams_sdk.egg-info/entry_points.txt
|
|
14
|
+
uams_sdk.egg-info/requires.txt
|
|
15
|
+
uams_sdk.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
uams_sdk
|