hipcortex 0.2.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.
- hipcortex/__init__.py +15 -0
- hipcortex/adapters/__init__.py +1 -0
- hipcortex/adapters/autogen.py +156 -0
- hipcortex/adapters/crewai.py +153 -0
- hipcortex/async_client.py +255 -0
- hipcortex/cli.py +480 -0
- hipcortex/client.py +223 -0
- hipcortex/install/SKILL.md +114 -0
- hipcortex/install/__init__.py +1 -0
- hipcortex/langchain_memory.py +238 -0
- hipcortex/llamaindex_storage.py +107 -0
- hipcortex-0.2.0.dist-info/METADATA +108 -0
- hipcortex-0.2.0.dist-info/RECORD +16 -0
- hipcortex-0.2.0.dist-info/WHEEL +5 -0
- hipcortex-0.2.0.dist-info/entry_points.txt +2 -0
- hipcortex-0.2.0.dist-info/top_level.txt +1 -0
hipcortex/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""HipCortex Python SDK — AI memory engine client."""
|
|
2
|
+
|
|
3
|
+
from .client import HipCortexClient
|
|
4
|
+
from .async_client import AsyncHipCortexClient
|
|
5
|
+
from .langchain_memory import HipCortexMemory, AsyncHipCortexMemory
|
|
6
|
+
from .llamaindex_storage import HipCortexStorageContext
|
|
7
|
+
|
|
8
|
+
__version__ = "0.2.0"
|
|
9
|
+
__all__ = [
|
|
10
|
+
"HipCortexClient",
|
|
11
|
+
"AsyncHipCortexClient",
|
|
12
|
+
"HipCortexMemory",
|
|
13
|
+
"AsyncHipCortexMemory",
|
|
14
|
+
"HipCortexStorageContext",
|
|
15
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Framework adapters for HipCortex — AutoGen and CrewAI."""
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""HipCortex AutoGen adapter — targets AutoGen 0.4+ (autogen-agentchat >= 0.4).
|
|
2
|
+
|
|
3
|
+
AutoGen 0.4 changed from register_hook() to the Memory protocol.
|
|
4
|
+
|
|
5
|
+
Usage (AutoGen 0.4+):
|
|
6
|
+
from autogen_agentchat.agents import AssistantAgent
|
|
7
|
+
from hipcortex import HipCortexClient
|
|
8
|
+
from hipcortex.adapters.autogen import HipCortexAutoGenMemory
|
|
9
|
+
|
|
10
|
+
client = HipCortexClient(base_url="http://localhost:3030")
|
|
11
|
+
memory = HipCortexAutoGenMemory(client=client, agent_id="researcher")
|
|
12
|
+
agent = AssistantAgent(name="researcher", model_client=..., memory=[memory])
|
|
13
|
+
|
|
14
|
+
Usage (AutoGen 0.3 — legacy):
|
|
15
|
+
agent.register_hook("process_message_before_send", memory.on_message_sent_v03)
|
|
16
|
+
agent.register_hook("process_all_messages_before_reply", memory.on_messages_received_v03)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
from ..client import HipCortexClient
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from autogen_core.memory import Memory, MemoryContent, MemoryMimeType, MemoryQueryResult
|
|
27
|
+
from autogen_core import CancellationToken
|
|
28
|
+
_AUTOGEN4_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
_AUTOGEN4_AVAILABLE = False
|
|
31
|
+
class Memory: # type: ignore[no-redef]
|
|
32
|
+
pass
|
|
33
|
+
class CancellationToken: # type: ignore[no-redef]
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HipCortexAutoGenMemory(Memory): # type: ignore[misc]
|
|
38
|
+
"""AutoGen 0.4 Memory protocol backed by HipCortex."""
|
|
39
|
+
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
client: HipCortexClient,
|
|
43
|
+
agent_id: str = "autogen-agent",
|
|
44
|
+
top_k: int = 10,
|
|
45
|
+
) -> None:
|
|
46
|
+
self._client = client
|
|
47
|
+
self._agent_id = agent_id
|
|
48
|
+
self._top_k = top_k
|
|
49
|
+
|
|
50
|
+
# AutoGen 0.4 Memory protocol -------------------------------------------
|
|
51
|
+
|
|
52
|
+
async def add(
|
|
53
|
+
self,
|
|
54
|
+
content: Any,
|
|
55
|
+
cancellation_token: Optional[CancellationToken] = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
if _AUTOGEN4_AVAILABLE and hasattr(content, "content"):
|
|
58
|
+
text = str(content.content)
|
|
59
|
+
role = getattr(content, "role", None)
|
|
60
|
+
mime = getattr(content, "mime_type", None)
|
|
61
|
+
action = "ai_message" if (
|
|
62
|
+
(role and role in ("assistant", "ai")) or
|
|
63
|
+
(mime and "assistant" in str(mime).lower())
|
|
64
|
+
) else "observation"
|
|
65
|
+
else:
|
|
66
|
+
text = str(content)
|
|
67
|
+
action = "observation"
|
|
68
|
+
self._client.add_memory(
|
|
69
|
+
actor=self._agent_id, action=action,
|
|
70
|
+
target=text, record_type="Temporal",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
async def query(
|
|
74
|
+
self,
|
|
75
|
+
query: Any,
|
|
76
|
+
cancellation_token: Optional[CancellationToken] = None,
|
|
77
|
+
) -> Any:
|
|
78
|
+
query_text = str(getattr(query, "content", query))
|
|
79
|
+
try:
|
|
80
|
+
search_results = self._client.search(query=query_text, limit=self._top_k)
|
|
81
|
+
except Exception:
|
|
82
|
+
search_results = [
|
|
83
|
+
{"record": r, "score": 1.0}
|
|
84
|
+
for r in self._client.get_conversation_history(self._agent_id, limit=self._top_k)
|
|
85
|
+
]
|
|
86
|
+
if not _AUTOGEN4_AVAILABLE:
|
|
87
|
+
return search_results
|
|
88
|
+
memories = []
|
|
89
|
+
for item in search_results:
|
|
90
|
+
rec = item.get("record", item)
|
|
91
|
+
memories.append(MemoryContent(
|
|
92
|
+
content=rec.get("target", ""),
|
|
93
|
+
mime_type=MemoryMimeType.TEXT,
|
|
94
|
+
metadata={"score": item.get("score", 1.0), "actor": rec.get("actor", ""),
|
|
95
|
+
"action": rec.get("action", ""), "timestamp": rec.get("timestamp", "")},
|
|
96
|
+
))
|
|
97
|
+
return MemoryQueryResult(results=memories)
|
|
98
|
+
|
|
99
|
+
async def update_context(
|
|
100
|
+
self,
|
|
101
|
+
model_context: Any,
|
|
102
|
+
cancellation_token: Optional[CancellationToken] = None,
|
|
103
|
+
) -> Any:
|
|
104
|
+
records = self._client.get_conversation_history(self._agent_id, limit=self._top_k)
|
|
105
|
+
records.sort(key=lambda r: r.get("timestamp", ""))
|
|
106
|
+
if not records:
|
|
107
|
+
return {"memories_added": 0}
|
|
108
|
+
lines = [f"[{r.get('action','memory')}] {r.get('target','')}" for r in records]
|
|
109
|
+
system_msg = {"role": "system", "content": "[HipCortex memory]\n" + "\n".join(lines)}
|
|
110
|
+
if hasattr(model_context, "add_message"):
|
|
111
|
+
await model_context.add_message(system_msg)
|
|
112
|
+
elif hasattr(model_context, "messages"):
|
|
113
|
+
model_context.messages.insert(0, system_msg)
|
|
114
|
+
return {"memories_added": len(records)}
|
|
115
|
+
|
|
116
|
+
async def clear(self) -> None:
|
|
117
|
+
self._client.forget(self._agent_id)
|
|
118
|
+
|
|
119
|
+
# AutoGen 0.3 legacy hooks -----------------------------------------------
|
|
120
|
+
|
|
121
|
+
def on_message_sent_v03(self, message: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
|
|
122
|
+
"""AutoGen 0.3 hook: process_message_before_send."""
|
|
123
|
+
content = message.get("content", "")
|
|
124
|
+
role = message.get("role", "assistant")
|
|
125
|
+
action = "ai_message" if role == "assistant" else "human_message"
|
|
126
|
+
self._client.add_memory(actor=self._agent_id, action=action, target=str(content),
|
|
127
|
+
record_type="Reflexion" if role == "assistant" else "Temporal")
|
|
128
|
+
return message
|
|
129
|
+
|
|
130
|
+
def on_messages_received_v03(
|
|
131
|
+
self, messages: List[Dict[str, Any]], **kwargs: Any
|
|
132
|
+
) -> List[Dict[str, Any]]:
|
|
133
|
+
"""AutoGen 0.3 hook: process_all_messages_before_reply."""
|
|
134
|
+
history = self._client.get_conversation_history(self._agent_id, limit=20)
|
|
135
|
+
if not history:
|
|
136
|
+
return messages
|
|
137
|
+
history.sort(key=lambda r: r.get("timestamp", ""))
|
|
138
|
+
lines = [
|
|
139
|
+
f"{'AI' if r.get('action') == 'ai_message' else 'Human'}: {r.get('target','')}"
|
|
140
|
+
for r in history
|
|
141
|
+
]
|
|
142
|
+
return [{"role": "system", "content": "[HipCortex memory]\n" + "\n".join(lines)}] + list(messages)
|
|
143
|
+
|
|
144
|
+
# Sync helpers (no AutoGen needed) ---------------------------------------
|
|
145
|
+
|
|
146
|
+
def store(self, content: str, action: str = "observation") -> Dict[str, Any]:
|
|
147
|
+
return self._client.add_memory(actor=self._agent_id, action=action,
|
|
148
|
+
target=content, record_type="Temporal")
|
|
149
|
+
|
|
150
|
+
def recall(self, limit: int = 20) -> str:
|
|
151
|
+
records = self._client.get_conversation_history(self._agent_id, limit=limit)
|
|
152
|
+
records.sort(key=lambda r: r.get("timestamp", ""))
|
|
153
|
+
return "\n".join(f"[{r.get('action','?')}] {r.get('target','')}" for r in records)
|
|
154
|
+
|
|
155
|
+
def forget_all(self) -> Dict[str, Any]:
|
|
156
|
+
return self._client.forget(self._agent_id)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""CrewAI tool adapter for HipCortex.
|
|
2
|
+
|
|
3
|
+
Exposes HipCortex memory operations as CrewAI ``BaseTool`` subclasses
|
|
4
|
+
so agents can store and retrieve memories as tool calls during task execution.
|
|
5
|
+
|
|
6
|
+
Usage::
|
|
7
|
+
|
|
8
|
+
from crewai import Agent, Task, Crew
|
|
9
|
+
from hipcortex import HipCortexClient
|
|
10
|
+
from hipcortex.adapters.crewai import (
|
|
11
|
+
HipCortexRememberTool,
|
|
12
|
+
HipCortexRecallTool,
|
|
13
|
+
HipCortexForgetTool,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
client = HipCortexClient(base_url="http://localhost:3000")
|
|
17
|
+
tools = [
|
|
18
|
+
HipCortexRememberTool(client=client, agent_id="researcher"),
|
|
19
|
+
HipCortexRecallTool(client=client, agent_id="researcher"),
|
|
20
|
+
HipCortexForgetTool(client=client, agent_id="researcher"),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
researcher = Agent(
|
|
24
|
+
role="Senior Researcher",
|
|
25
|
+
goal="Research and remember findings",
|
|
26
|
+
tools=tools,
|
|
27
|
+
...
|
|
28
|
+
)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
from typing import Any, Optional, Type
|
|
34
|
+
|
|
35
|
+
from ..client import HipCortexClient
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
from crewai_tools import BaseTool
|
|
39
|
+
from pydantic import BaseModel, Field
|
|
40
|
+
_CREWAI_AVAILABLE = True
|
|
41
|
+
except ImportError:
|
|
42
|
+
_CREWAI_AVAILABLE = False
|
|
43
|
+
|
|
44
|
+
class BaseModel: # type: ignore[no-redef]
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
class Field: # type: ignore[no-redef]
|
|
48
|
+
def __init__(self, *a: Any, **kw: Any) -> None: ...
|
|
49
|
+
|
|
50
|
+
class BaseTool: # type: ignore[no-redef]
|
|
51
|
+
name: str = ""
|
|
52
|
+
description: str = ""
|
|
53
|
+
args_schema: Any = None
|
|
54
|
+
def _run(self, *a: Any, **kw: Any) -> str: ...
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Input schemas
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
if _CREWAI_AVAILABLE:
|
|
62
|
+
class RememberInput(BaseModel):
|
|
63
|
+
content: str = Field(..., description="Observation or fact to remember.")
|
|
64
|
+
action: str = Field(default="observation", description="Action tag (e.g. 'finding', 'decision').")
|
|
65
|
+
|
|
66
|
+
class RecallInput(BaseModel):
|
|
67
|
+
limit: int = Field(default=20, description="Max number of memories to return.")
|
|
68
|
+
|
|
69
|
+
class ForgetInput(BaseModel):
|
|
70
|
+
confirm: bool = Field(default=False, description="Set to True to confirm deletion.")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Tools
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
class HipCortexRememberTool(BaseTool): # type: ignore[misc]
|
|
78
|
+
"""CrewAI tool: store an observation in HipCortex memory."""
|
|
79
|
+
|
|
80
|
+
name: str = "hipcortex_remember"
|
|
81
|
+
description: str = (
|
|
82
|
+
"Store an important observation, finding, or decision in persistent memory. "
|
|
83
|
+
"Use this whenever you discover information worth remembering for future tasks."
|
|
84
|
+
)
|
|
85
|
+
if _CREWAI_AVAILABLE:
|
|
86
|
+
args_schema: Type[BaseModel] = RememberInput
|
|
87
|
+
|
|
88
|
+
def __init__(self, client: HipCortexClient, agent_id: str = "crewai-agent") -> None:
|
|
89
|
+
super().__init__()
|
|
90
|
+
self._client = client
|
|
91
|
+
self._agent_id = agent_id
|
|
92
|
+
|
|
93
|
+
def _run(self, content: str, action: str = "observation") -> str:
|
|
94
|
+
result = self._client.add_memory(
|
|
95
|
+
actor=self._agent_id,
|
|
96
|
+
action=action,
|
|
97
|
+
target=content,
|
|
98
|
+
record_type="Temporal",
|
|
99
|
+
)
|
|
100
|
+
if result.get("success"):
|
|
101
|
+
return f"Stored memory with id={result.get('record_id', '?')}"
|
|
102
|
+
return f"Failed to store memory: {result.get('error', 'unknown error')}"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class HipCortexRecallTool(BaseTool): # type: ignore[misc]
|
|
106
|
+
"""CrewAI tool: retrieve recent memories from HipCortex."""
|
|
107
|
+
|
|
108
|
+
name: str = "hipcortex_recall"
|
|
109
|
+
description: str = (
|
|
110
|
+
"Retrieve recent memories and observations stored in HipCortex. "
|
|
111
|
+
"Use this to recall past findings before starting a new research task."
|
|
112
|
+
)
|
|
113
|
+
if _CREWAI_AVAILABLE:
|
|
114
|
+
args_schema: Type[BaseModel] = RecallInput
|
|
115
|
+
|
|
116
|
+
def __init__(self, client: HipCortexClient, agent_id: str = "crewai-agent") -> None:
|
|
117
|
+
super().__init__()
|
|
118
|
+
self._client = client
|
|
119
|
+
self._agent_id = agent_id
|
|
120
|
+
|
|
121
|
+
def _run(self, limit: int = 20) -> str:
|
|
122
|
+
records = self._client.get_conversation_history(self._agent_id, limit=limit)
|
|
123
|
+
if not records:
|
|
124
|
+
return "No memories found."
|
|
125
|
+
records.sort(key=lambda r: r.get("timestamp", ""))
|
|
126
|
+
lines = []
|
|
127
|
+
for i, rec in enumerate(records, 1):
|
|
128
|
+
lines.append(f"{i}. [{rec.get('action', '?')}] {rec.get('target', '')}")
|
|
129
|
+
return "\n".join(lines)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class HipCortexForgetTool(BaseTool): # type: ignore[misc]
|
|
133
|
+
"""CrewAI tool: delete all memories for this agent (GDPR / session reset)."""
|
|
134
|
+
|
|
135
|
+
name: str = "hipcortex_forget"
|
|
136
|
+
description: str = (
|
|
137
|
+
"Delete all memories stored for this agent. "
|
|
138
|
+
"Only use this when explicitly asked to clear memory or start fresh."
|
|
139
|
+
)
|
|
140
|
+
if _CREWAI_AVAILABLE:
|
|
141
|
+
args_schema: Type[BaseModel] = ForgetInput
|
|
142
|
+
|
|
143
|
+
def __init__(self, client: HipCortexClient, agent_id: str = "crewai-agent") -> None:
|
|
144
|
+
super().__init__()
|
|
145
|
+
self._client = client
|
|
146
|
+
self._agent_id = agent_id
|
|
147
|
+
|
|
148
|
+
def _run(self, confirm: bool = False) -> str:
|
|
149
|
+
if not confirm:
|
|
150
|
+
return "Deletion not confirmed. Pass confirm=True to proceed."
|
|
151
|
+
result = self._client.forget(self._agent_id)
|
|
152
|
+
deleted = result.get("records_deleted", 0)
|
|
153
|
+
return f"Deleted {deleted} memory records for agent '{self._agent_id}'."
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""HipCortex async HTTP client — thin async wrapper around the REST API using httpx."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AsyncHipCortexClient:
|
|
12
|
+
"""Asynchronous HTTP client for the HipCortex memory server.
|
|
13
|
+
|
|
14
|
+
Mirrors :class:`HipCortexClient` with coroutine-based methods powered by
|
|
15
|
+
``httpx.AsyncClient``. Compatible with FastAPI, Django async views,
|
|
16
|
+
LangChain 0.3+ async chains, and any asyncio / anyio event loop.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
base_url: Root URL of the running hipcortex web-server binary.
|
|
20
|
+
timeout: Per-request timeout in seconds.
|
|
21
|
+
api_key: Optional Bearer token sent as ``Authorization: Bearer <key>``.
|
|
22
|
+
|
|
23
|
+
Usage::
|
|
24
|
+
|
|
25
|
+
# One-shot (opens and closes the underlying httpx.AsyncClient per call)
|
|
26
|
+
client = AsyncHipCortexClient()
|
|
27
|
+
result = await client.add_memory("alice", "said", "hello")
|
|
28
|
+
|
|
29
|
+
# Context-manager (recommended for sustained use)
|
|
30
|
+
async with AsyncHipCortexClient() as client:
|
|
31
|
+
result = await client.add_memory("alice", "said", "hello")
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
base_url: str = "http://localhost:3030",
|
|
37
|
+
timeout: float = 10.0,
|
|
38
|
+
api_key: Optional[str] = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.base_url = base_url.rstrip("/")
|
|
41
|
+
self.timeout = timeout
|
|
42
|
+
self._api_key = api_key
|
|
43
|
+
headers: Dict[str, str] = {}
|
|
44
|
+
if api_key:
|
|
45
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
46
|
+
self._client = httpx.AsyncClient(headers=headers, timeout=timeout)
|
|
47
|
+
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
# Async context-manager support
|
|
50
|
+
# ------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
async def __aenter__(self) -> "AsyncHipCortexClient":
|
|
53
|
+
return self
|
|
54
|
+
|
|
55
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
56
|
+
await self.close()
|
|
57
|
+
|
|
58
|
+
async def close(self) -> None:
|
|
59
|
+
"""Close the underlying :class:`httpx.AsyncClient`."""
|
|
60
|
+
await self._client.aclose()
|
|
61
|
+
|
|
62
|
+
# ------------------------------------------------------------------
|
|
63
|
+
# Core memory operations
|
|
64
|
+
# ------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
async def add_memory(
|
|
67
|
+
self,
|
|
68
|
+
actor: str,
|
|
69
|
+
action: str,
|
|
70
|
+
target: str,
|
|
71
|
+
record_type: str = "Temporal",
|
|
72
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
73
|
+
ttl_seconds: Optional[int] = None,
|
|
74
|
+
) -> Dict[str, Any]:
|
|
75
|
+
"""Store a memory record.
|
|
76
|
+
|
|
77
|
+
Returns the server response dict with ``success`` and ``record_id``.
|
|
78
|
+
"""
|
|
79
|
+
payload: Dict[str, Any] = {
|
|
80
|
+
"actor": actor,
|
|
81
|
+
"action": action,
|
|
82
|
+
"target": target,
|
|
83
|
+
"record_type": record_type,
|
|
84
|
+
"metadata": metadata or {},
|
|
85
|
+
}
|
|
86
|
+
if ttl_seconds is not None:
|
|
87
|
+
payload["ttl_seconds"] = ttl_seconds
|
|
88
|
+
resp = await self._client.post(f"{self.base_url}/memory/add", json=payload)
|
|
89
|
+
resp.raise_for_status()
|
|
90
|
+
return resp.json()
|
|
91
|
+
|
|
92
|
+
async def query_memory(
|
|
93
|
+
self,
|
|
94
|
+
actor: Optional[str] = None,
|
|
95
|
+
action: Optional[str] = None,
|
|
96
|
+
record_type: Optional[str] = None,
|
|
97
|
+
limit: int = 100,
|
|
98
|
+
) -> List[Dict[str, Any]]:
|
|
99
|
+
"""Query memory records. Returns a list of record dicts."""
|
|
100
|
+
params: Dict[str, Any] = {"limit": limit}
|
|
101
|
+
if actor is not None:
|
|
102
|
+
params["actor"] = actor
|
|
103
|
+
if action is not None:
|
|
104
|
+
params["action"] = action
|
|
105
|
+
if record_type is not None:
|
|
106
|
+
params["record_type"] = record_type
|
|
107
|
+
resp = await self._client.get(f"{self.base_url}/memory/query", params=params)
|
|
108
|
+
resp.raise_for_status()
|
|
109
|
+
return resp.json().get("records", [])
|
|
110
|
+
|
|
111
|
+
async def search(
|
|
112
|
+
self,
|
|
113
|
+
query: str,
|
|
114
|
+
embedding: Optional[List[float]] = None,
|
|
115
|
+
limit: int = 10,
|
|
116
|
+
) -> List[Dict[str, Any]]:
|
|
117
|
+
"""Semantic + keyword search over stored memory records.
|
|
118
|
+
|
|
119
|
+
If ``embedding`` is provided, ranks results by cosine similarity
|
|
120
|
+
against records that carry a ``metadata.embedding`` float array.
|
|
121
|
+
Falls back to keyword matching when embeddings are absent.
|
|
122
|
+
|
|
123
|
+
Returns a list of ``{"score": float, "record": {...}}`` dicts,
|
|
124
|
+
sorted by descending score.
|
|
125
|
+
"""
|
|
126
|
+
payload: Dict[str, Any] = {"query": query, "limit": limit}
|
|
127
|
+
if embedding is not None:
|
|
128
|
+
payload["embedding"] = embedding
|
|
129
|
+
resp = await self._client.post(f"{self.base_url}/memory/search", json=payload)
|
|
130
|
+
resp.raise_for_status()
|
|
131
|
+
return resp.json().get("results", [])
|
|
132
|
+
|
|
133
|
+
async def bulk_add(self, records: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
134
|
+
"""Bulk-insert multiple memory records in a single request.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
records: List of record dicts, each with at minimum
|
|
138
|
+
``actor``, ``action``, and ``target`` keys.
|
|
139
|
+
|
|
140
|
+
Returns the server response with ``inserted`` count and any errors.
|
|
141
|
+
"""
|
|
142
|
+
resp = await self._client.post(
|
|
143
|
+
f"{self.base_url}/memory/bulk", json={"records": records}
|
|
144
|
+
)
|
|
145
|
+
resp.raise_for_status()
|
|
146
|
+
return resp.json()
|
|
147
|
+
|
|
148
|
+
async def embed_and_add(
|
|
149
|
+
self,
|
|
150
|
+
actor: str,
|
|
151
|
+
action: str,
|
|
152
|
+
target: str,
|
|
153
|
+
embedding_model: str,
|
|
154
|
+
record_type: str = "Temporal",
|
|
155
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
156
|
+
) -> Dict[str, Any]:
|
|
157
|
+
"""Store a memory record and generate an embedding server-side.
|
|
158
|
+
|
|
159
|
+
The server calls the specified ``embedding_model`` (e.g. an Ollama
|
|
160
|
+
or OpenAI model) and attaches the resulting vector to the record.
|
|
161
|
+
|
|
162
|
+
Returns the server response dict with ``success`` and ``record_id``.
|
|
163
|
+
"""
|
|
164
|
+
payload: Dict[str, Any] = {
|
|
165
|
+
"actor": actor,
|
|
166
|
+
"action": action,
|
|
167
|
+
"target": target,
|
|
168
|
+
"record_type": record_type,
|
|
169
|
+
"embedding_model": embedding_model,
|
|
170
|
+
"metadata": metadata or {},
|
|
171
|
+
}
|
|
172
|
+
resp = await self._client.post(f"{self.base_url}/memory/embed", json=payload)
|
|
173
|
+
resp.raise_for_status()
|
|
174
|
+
return resp.json()
|
|
175
|
+
|
|
176
|
+
async def forget(self, actor: str) -> Dict[str, Any]:
|
|
177
|
+
"""GDPR right-to-forget: delete all records for ``actor``.
|
|
178
|
+
|
|
179
|
+
Returns ``{"success": bool, "records_deleted": int, "symbolic_nodes_deleted": int}``.
|
|
180
|
+
"""
|
|
181
|
+
resp = await self._client.delete(
|
|
182
|
+
f"{self.base_url}/memory/forget/{actor}"
|
|
183
|
+
)
|
|
184
|
+
resp.raise_for_status()
|
|
185
|
+
return resp.json()
|
|
186
|
+
|
|
187
|
+
# ------------------------------------------------------------------
|
|
188
|
+
# System
|
|
189
|
+
# ------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
async def health(self) -> bool:
|
|
192
|
+
"""Return True if the server is reachable and healthy."""
|
|
193
|
+
try:
|
|
194
|
+
resp = await self._client.get(f"{self.base_url}/health")
|
|
195
|
+
return resp.status_code == 200
|
|
196
|
+
except httpx.RequestError:
|
|
197
|
+
return False
|
|
198
|
+
|
|
199
|
+
async def stats(self) -> Dict[str, Any]:
|
|
200
|
+
"""Return live server statistics: record counts, type breakdown, metering state."""
|
|
201
|
+
resp = await self._client.get(f"{self.base_url}/stats")
|
|
202
|
+
resp.raise_for_status()
|
|
203
|
+
return resp.json()
|
|
204
|
+
|
|
205
|
+
async def coherence_status(self) -> Dict[str, Any]:
|
|
206
|
+
"""Return the current coherence metrics from the server."""
|
|
207
|
+
resp = await self._client.get(f"{self.base_url}/coherence/status")
|
|
208
|
+
resp.raise_for_status()
|
|
209
|
+
return resp.json()
|
|
210
|
+
|
|
211
|
+
async def graph(self) -> Dict[str, Any]:
|
|
212
|
+
"""Return the full symbolic graph (nodes + edges)."""
|
|
213
|
+
resp = await self._client.get(f"{self.base_url}/graph")
|
|
214
|
+
resp.raise_for_status()
|
|
215
|
+
return resp.json()
|
|
216
|
+
|
|
217
|
+
async def get_node(self, node_id: str) -> Optional[Dict[str, Any]]:
|
|
218
|
+
"""Fetch a single symbolic node by UUID."""
|
|
219
|
+
resp = await self._client.get(f"{self.base_url}/node/{node_id}")
|
|
220
|
+
resp.raise_for_status()
|
|
221
|
+
return resp.json()
|
|
222
|
+
|
|
223
|
+
# ------------------------------------------------------------------
|
|
224
|
+
# Convenience helpers used by framework adapters
|
|
225
|
+
# ------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
async def add_human_message(self, session_id: str, content: str) -> Dict[str, Any]:
|
|
228
|
+
"""Record a human turn in a conversation session."""
|
|
229
|
+
return await self.add_memory(
|
|
230
|
+
actor=session_id,
|
|
231
|
+
action="human_message",
|
|
232
|
+
target=content,
|
|
233
|
+
record_type="Temporal",
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
async def add_ai_message(self, session_id: str, content: str) -> Dict[str, Any]:
|
|
237
|
+
"""Record an AI turn in a conversation session."""
|
|
238
|
+
return await self.add_memory(
|
|
239
|
+
actor=session_id,
|
|
240
|
+
action="ai_message",
|
|
241
|
+
target=content,
|
|
242
|
+
record_type="Reflexion",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
async def get_conversation_history(
|
|
246
|
+
self, session_id: str, limit: int = 50
|
|
247
|
+
) -> List[Dict[str, Any]]:
|
|
248
|
+
"""Retrieve the message history for a conversation session."""
|
|
249
|
+
return await self.query_memory(actor=session_id, limit=limit)
|
|
250
|
+
|
|
251
|
+
async def ping_latency_ms(self) -> float:
|
|
252
|
+
"""Return round-trip latency to /health in milliseconds."""
|
|
253
|
+
t0 = time.perf_counter()
|
|
254
|
+
await self.health()
|
|
255
|
+
return (time.perf_counter() - t0) * 1000
|