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 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