acis-memory 1.0.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.
- acis/__init__.py +40 -0
- acis/client.py +369 -0
- acis/exceptions.py +40 -0
- acis_memory-1.0.0.dist-info/METADATA +52 -0
- acis_memory-1.0.0.dist-info/RECORD +7 -0
- acis_memory-1.0.0.dist-info/WHEEL +5 -0
- acis_memory-1.0.0.dist-info/top_level.txt +1 -0
acis/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""ACIS SDK — AI Context & Intelligent System.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from acis import MemoryClient
|
|
5
|
+
|
|
6
|
+
client = MemoryClient(tenant_id="your-tenant", base_url="http://acis-server:8000")
|
|
7
|
+
"""
|
|
8
|
+
from acis.client import (
|
|
9
|
+
MemoryClient,
|
|
10
|
+
SessionResult,
|
|
11
|
+
CloseResult,
|
|
12
|
+
WriteResult,
|
|
13
|
+
ContextResult,
|
|
14
|
+
SearchResult,
|
|
15
|
+
MemoryHistoryEntry,
|
|
16
|
+
)
|
|
17
|
+
from acis.exceptions import (
|
|
18
|
+
ACISError,
|
|
19
|
+
ACISRateLimitError,
|
|
20
|
+
ACISSessionNotFoundError,
|
|
21
|
+
ACISCircuitOpenError,
|
|
22
|
+
ACISValidationError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__version__ = "1.0.0"
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"MemoryClient",
|
|
29
|
+
"SessionResult",
|
|
30
|
+
"CloseResult",
|
|
31
|
+
"WriteResult",
|
|
32
|
+
"ContextResult",
|
|
33
|
+
"SearchResult",
|
|
34
|
+
"MemoryHistoryEntry",
|
|
35
|
+
"ACISError",
|
|
36
|
+
"ACISRateLimitError",
|
|
37
|
+
"ACISSessionNotFoundError",
|
|
38
|
+
"ACISCircuitOpenError",
|
|
39
|
+
"ACISValidationError",
|
|
40
|
+
]
|
acis/client.py
ADDED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""ACIS MemoryClient — primary SDK entry point (HTTP-backed).
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from acis import MemoryClient
|
|
5
|
+
|
|
6
|
+
client = MemoryClient(tenant_id="abc-123", base_url="http://acis-server:8000")
|
|
7
|
+
session = client.create_session(user_id="user-1")
|
|
8
|
+
client.add_event(session_id=session.session_id, role="user", content="Hello")
|
|
9
|
+
ctx = client.get_context(session_id=session.session_id, query="Hello")
|
|
10
|
+
# ctx.messages → pass directly to any LLM
|
|
11
|
+
"""
|
|
12
|
+
import logging
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from acis.exceptions import (
|
|
19
|
+
ACISError,
|
|
20
|
+
ACISRateLimitError,
|
|
21
|
+
ACISSessionNotFoundError,
|
|
22
|
+
ACISCircuitOpenError,
|
|
23
|
+
ACISValidationError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
DEFAULT_BASE_URL = "http://localhost:8000"
|
|
29
|
+
API_PREFIX = "/api/v1"
|
|
30
|
+
DEFAULT_TIMEOUT = httpx.Timeout(connect=5.0, read=30.0, write=10.0, pool=5.0)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class SessionResult:
|
|
35
|
+
session_id: str
|
|
36
|
+
tenant_id: str
|
|
37
|
+
user_id: str
|
|
38
|
+
agent_id: Optional[str] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class CloseResult:
|
|
43
|
+
session_id: str
|
|
44
|
+
closed: bool
|
|
45
|
+
close_reason: str = "user_ended"
|
|
46
|
+
closed_at: Optional[str] = None
|
|
47
|
+
already_closed: bool = False
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class WriteResult:
|
|
52
|
+
event_id: str
|
|
53
|
+
seq: int
|
|
54
|
+
created: bool
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class ContextResult:
|
|
59
|
+
messages: list
|
|
60
|
+
context_payload: list
|
|
61
|
+
events_injected: int
|
|
62
|
+
memories_injected: int
|
|
63
|
+
summary_injected: bool
|
|
64
|
+
rules_injected: int
|
|
65
|
+
total_tokens: int
|
|
66
|
+
token_budget: int
|
|
67
|
+
remaining_budget: int
|
|
68
|
+
memory_ids_used: list
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class SearchResult:
|
|
73
|
+
memory_id: str
|
|
74
|
+
content: str
|
|
75
|
+
category: str
|
|
76
|
+
importance: float
|
|
77
|
+
agent_id: Optional[str] = None
|
|
78
|
+
entities: Optional[dict] = None
|
|
79
|
+
rrf_score: Optional[float] = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class MemoryHistoryEntry:
|
|
84
|
+
memory_id: str
|
|
85
|
+
content: str
|
|
86
|
+
category: str
|
|
87
|
+
importance: float
|
|
88
|
+
created_at: Optional[str] = None
|
|
89
|
+
valid_to: Optional[str] = None
|
|
90
|
+
superseded_by: Optional[str] = None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class MemoryClient:
|
|
94
|
+
"""ACIS Python SDK — single entry point for all memory operations.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
tenant_id: Tenant UUID (required)
|
|
98
|
+
user_id: Default user ID (optional, can override per-call)
|
|
99
|
+
agent_id: Default agent ID for scoped operations (optional)
|
|
100
|
+
base_url: ACIS API base URL (e.g. http://acis-server:8000)
|
|
101
|
+
timeout: HTTP timeout configuration (optional)
|
|
102
|
+
|
|
103
|
+
Example:
|
|
104
|
+
from acis import MemoryClient
|
|
105
|
+
|
|
106
|
+
client = MemoryClient(
|
|
107
|
+
tenant_id="abc-123",
|
|
108
|
+
base_url="http://10.0.1.50:8000"
|
|
109
|
+
)
|
|
110
|
+
session = client.create_session(user_id="user-1")
|
|
111
|
+
client.add_event(session_id=session.session_id, role="user", content="Hello")
|
|
112
|
+
ctx = client.get_context(session_id=session.session_id, query="Hello")
|
|
113
|
+
response = your_llm(messages=ctx.messages)
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
tenant_id: str,
|
|
119
|
+
user_id: Optional[str] = None,
|
|
120
|
+
agent_id: Optional[str] = None,
|
|
121
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
122
|
+
timeout: Optional[httpx.Timeout] = None,
|
|
123
|
+
):
|
|
124
|
+
if not tenant_id or not tenant_id.strip():
|
|
125
|
+
raise ACISValidationError("tenant_id is required and cannot be empty")
|
|
126
|
+
|
|
127
|
+
self.tenant_id = tenant_id.strip()
|
|
128
|
+
self.user_id = user_id
|
|
129
|
+
self.agent_id = agent_id
|
|
130
|
+
self.base_url = base_url.rstrip("/")
|
|
131
|
+
self._api_url = f"{self.base_url}{API_PREFIX}"
|
|
132
|
+
|
|
133
|
+
self._http = httpx.Client(
|
|
134
|
+
base_url=self._api_url,
|
|
135
|
+
headers={"X-Tenant-Id": self.tenant_id},
|
|
136
|
+
timeout=timeout or DEFAULT_TIMEOUT,
|
|
137
|
+
)
|
|
138
|
+
self._resolved_session_id: Optional[str] = None
|
|
139
|
+
logger.info(f"[ACIS SDK] Initialized: tenant={self.tenant_id}, api={self._api_url}")
|
|
140
|
+
|
|
141
|
+
def close(self):
|
|
142
|
+
"""Close the underlying HTTP client."""
|
|
143
|
+
self._http.close()
|
|
144
|
+
|
|
145
|
+
def __enter__(self):
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def __exit__(self, *args):
|
|
149
|
+
self.close()
|
|
150
|
+
|
|
151
|
+
# ─── HTTP Helpers ─────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
def _request(self, method: str, path: str, **kwargs) -> dict:
|
|
154
|
+
try:
|
|
155
|
+
response = self._http.request(method, path, **kwargs)
|
|
156
|
+
except httpx.ConnectError as e:
|
|
157
|
+
raise ACISError(f"Cannot connect to ACIS at {self.base_url}: {e}", code="CONNECTION_ERROR") from e
|
|
158
|
+
except httpx.TimeoutException as e:
|
|
159
|
+
raise ACISError(f"Request to ACIS timed out: {e}", code="TIMEOUT") from e
|
|
160
|
+
|
|
161
|
+
if response.status_code in (200, 201, 202):
|
|
162
|
+
return response.json()
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
body = response.json()
|
|
166
|
+
except Exception:
|
|
167
|
+
body = {"error": response.text}
|
|
168
|
+
|
|
169
|
+
detail = body.get("detail") or body.get("error", "Unknown error")
|
|
170
|
+
|
|
171
|
+
if response.status_code == 400:
|
|
172
|
+
raise ACISValidationError(detail)
|
|
173
|
+
elif response.status_code == 404:
|
|
174
|
+
raise ACISSessionNotFoundError(detail)
|
|
175
|
+
elif response.status_code == 429:
|
|
176
|
+
raise ACISRateLimitError(detail, retry_after=float(response.headers.get("Retry-After", 0)))
|
|
177
|
+
elif response.status_code == 503:
|
|
178
|
+
raise ACISCircuitOpenError()
|
|
179
|
+
else:
|
|
180
|
+
raise ACISError(detail, code=body.get("code", "UNKNOWN"))
|
|
181
|
+
|
|
182
|
+
def _post(self, path: str, json: dict) -> dict:
|
|
183
|
+
return self._request("POST", path, json=json)
|
|
184
|
+
|
|
185
|
+
def _get(self, path: str, params: dict = None) -> dict:
|
|
186
|
+
return self._request("GET", path, params=params)
|
|
187
|
+
|
|
188
|
+
def _delete(self, path: str) -> dict:
|
|
189
|
+
return self._request("DELETE", path)
|
|
190
|
+
|
|
191
|
+
# ─── Session Resolution ───────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
def _resolve_session(self, session_id=None, user_id=None, agent_id=None):
|
|
194
|
+
if session_id:
|
|
195
|
+
return session_id
|
|
196
|
+
uid = user_id or self.user_id
|
|
197
|
+
if not uid:
|
|
198
|
+
raise ACISValidationError("user_id is required for automatic session resolution")
|
|
199
|
+
if self._resolved_session_id:
|
|
200
|
+
return self._resolved_session_id
|
|
201
|
+
result = self.create_session(user_id=uid, agent_id=agent_id)
|
|
202
|
+
self._resolved_session_id = result.session_id
|
|
203
|
+
return result.session_id
|
|
204
|
+
|
|
205
|
+
# ─── Session Lifecycle ────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
def create_session(self, user_id=None, agent_id=None, llm_model_id=None, token_budget=None, meta=None) -> SessionResult:
|
|
208
|
+
"""Create a new conversation session."""
|
|
209
|
+
uid = user_id or self.user_id
|
|
210
|
+
aid = agent_id or self.agent_id
|
|
211
|
+
if not uid:
|
|
212
|
+
raise ACISValidationError("user_id is required for create_session")
|
|
213
|
+
self._resolved_session_id = None
|
|
214
|
+
body = {"user_id": uid}
|
|
215
|
+
if aid: body["agent_id"] = aid
|
|
216
|
+
if llm_model_id: body["llm_model_id"] = llm_model_id
|
|
217
|
+
if token_budget is not None: body["token_budget"] = token_budget
|
|
218
|
+
if meta: body["meta"] = meta
|
|
219
|
+
data = self._post("/sessions", json=body)
|
|
220
|
+
return SessionResult(session_id=data["session_id"], tenant_id=data.get("tenant_id", self.tenant_id), user_id=uid, agent_id=data.get("agent_id", aid))
|
|
221
|
+
|
|
222
|
+
def close_session(self, session_id=None, reason="user_ended") -> CloseResult:
|
|
223
|
+
"""Close a session (triggers async memory extraction)."""
|
|
224
|
+
sid = session_id or self._resolved_session_id
|
|
225
|
+
if not sid:
|
|
226
|
+
raise ACISValidationError("session_id is required")
|
|
227
|
+
data = self._post(f"/sessions/{sid}/close", json={"close_reason": reason})
|
|
228
|
+
if sid == self._resolved_session_id:
|
|
229
|
+
self._resolved_session_id = None
|
|
230
|
+
return CloseResult(session_id=data.get("session_id", sid), closed=True, close_reason=data.get("close_reason", reason), closed_at=data.get("closed_at"), already_closed=data.get("already_closed", False))
|
|
231
|
+
|
|
232
|
+
# ─── Write Events ─────────────────────────────────────────────
|
|
233
|
+
|
|
234
|
+
def add_event(self, session_id=None, role="user", content="", event_type="message", user_id=None, agent_id=None, token_count=None, tool_call_id=None, idempotency_key=None, attachments=None) -> WriteResult:
|
|
235
|
+
"""Write a single event (message, tool_call, tool_result, etc.)."""
|
|
236
|
+
if not content and event_type == "message":
|
|
237
|
+
raise ACISValidationError("content is required for message events")
|
|
238
|
+
uid = user_id or self.user_id
|
|
239
|
+
if not uid:
|
|
240
|
+
raise ACISValidationError("user_id is required")
|
|
241
|
+
resolved_sid = self._resolve_session(session_id, uid, agent_id or self.agent_id)
|
|
242
|
+
body = {"session_id": resolved_sid, "user_id": uid, "event_type": event_type, "role": role, "content": content}
|
|
243
|
+
if agent_id or self.agent_id: body["agent_id"] = agent_id or self.agent_id
|
|
244
|
+
if token_count is not None: body["token_count"] = token_count
|
|
245
|
+
if tool_call_id: body["tool_call_id"] = tool_call_id
|
|
246
|
+
if idempotency_key: body["idempotency_key"] = idempotency_key
|
|
247
|
+
if attachments: body["attachments"] = attachments
|
|
248
|
+
data = self._post("/events", json=body)
|
|
249
|
+
return WriteResult(event_id=data["event_id"], seq=data["seq"], created=data["created"])
|
|
250
|
+
|
|
251
|
+
def write_batch(self, session_id=None, events=None, user_id=None, agent_id=None) -> list:
|
|
252
|
+
"""Batch write multiple events atomically."""
|
|
253
|
+
if not events or not isinstance(events, list):
|
|
254
|
+
raise ACISValidationError("events must be a non-empty list")
|
|
255
|
+
if len(events) > 50:
|
|
256
|
+
raise ACISValidationError("write_batch limited to 50 events per call")
|
|
257
|
+
uid = user_id or self.user_id
|
|
258
|
+
if not uid:
|
|
259
|
+
raise ACISValidationError("user_id is required")
|
|
260
|
+
resolved_sid = self._resolve_session(session_id, uid, agent_id or self.agent_id)
|
|
261
|
+
body = {
|
|
262
|
+
"session_id": resolved_sid, "user_id": uid,
|
|
263
|
+
"events": [{"user_id": uid, "event_type": e.get("event_type", "message"), "role": e.get("role", "user"), "content": e.get("content", ""), "token_count": e.get("token_count"), "tool_call_id": e.get("tool_call_id"), "idempotency_key": e.get("idempotency_key")} for e in events],
|
|
264
|
+
}
|
|
265
|
+
if agent_id or self.agent_id: body["agent_id"] = agent_id or self.agent_id
|
|
266
|
+
data = self._post("/events/batch", json=body)
|
|
267
|
+
return [WriteResult(event_id=e["event_id"], seq=e["seq"], created=e["created"]) for e in data.get("events", [])]
|
|
268
|
+
|
|
269
|
+
# ─── Context Assembly ─────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
def get_context(self, session_id=None, query="", user_id=None, agent_id=None, model_id="gpt-4o") -> ContextResult:
|
|
272
|
+
"""Get token-budgeted context for your LLM call. Returns ctx.messages ready for any LLM."""
|
|
273
|
+
if not query:
|
|
274
|
+
raise ACISValidationError("query is required")
|
|
275
|
+
uid = user_id or self.user_id
|
|
276
|
+
if not uid:
|
|
277
|
+
raise ACISValidationError("user_id is required")
|
|
278
|
+
resolved_sid = self._resolve_session(session_id, uid, agent_id or self.agent_id)
|
|
279
|
+
body = {"session_id": resolved_sid, "user_id": uid, "query": query}
|
|
280
|
+
if model_id: body["model_id"] = model_id
|
|
281
|
+
if agent_id or self.agent_id: body["agent_id"] = agent_id or self.agent_id
|
|
282
|
+
data = self._post("/context", json=body)
|
|
283
|
+
raw_payload = data.get("context_payload", [])
|
|
284
|
+
messages = self._build_messages(raw_payload, query)
|
|
285
|
+
return ContextResult(messages=messages, context_payload=raw_payload, events_injected=data.get("events_injected", 0), memories_injected=data.get("memories_injected", 0), summary_injected=data.get("summary_injected", False), rules_injected=data.get("rules_injected", 0), total_tokens=data.get("total_tokens", 0), token_budget=data.get("token_budget", 0), remaining_budget=data.get("remaining_budget", 0), memory_ids_used=data.get("memory_ids_used", []))
|
|
286
|
+
|
|
287
|
+
# ─── Knowledge Search ─────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
def search(self, query: str, user_id=None, agent_id=None, top_k=10) -> list:
|
|
290
|
+
"""Search user memories (hybrid ANN + BM25)."""
|
|
291
|
+
if not query:
|
|
292
|
+
raise ACISValidationError("query is required for search")
|
|
293
|
+
uid = user_id or self.user_id
|
|
294
|
+
if not uid:
|
|
295
|
+
raise ACISValidationError("user_id is required for search")
|
|
296
|
+
body = {"user_id": uid, "query": query, "top_k": top_k}
|
|
297
|
+
if agent_id or self.agent_id: body["agent_id"] = agent_id or self.agent_id
|
|
298
|
+
data = self._post("/search", json=body)
|
|
299
|
+
return [SearchResult(memory_id=r["memory_id"], content=r["content"], category=r["category"], importance=r["importance"], agent_id=r.get("agent_id"), entities=r.get("entities"), rrf_score=r.get("rrf_score")) for r in data.get("results", [])]
|
|
300
|
+
|
|
301
|
+
# ─── Memory History ───────────────────────────────────────────
|
|
302
|
+
|
|
303
|
+
def memory_history(self, memory_id: str) -> list:
|
|
304
|
+
"""Get temporal evolution chain for a memory."""
|
|
305
|
+
if not memory_id:
|
|
306
|
+
raise ACISValidationError("memory_id is required")
|
|
307
|
+
data = self._get(f"/memories/{memory_id}/history")
|
|
308
|
+
return [MemoryHistoryEntry(memory_id=i["memory_id"], content=i["content"], category=i["category"], importance=i["importance"], created_at=i.get("created_at"), valid_to=i.get("valid_to"), superseded_by=i.get("superseded_by")) for i in data.get("chain", [])]
|
|
309
|
+
|
|
310
|
+
# ─── Feedback ─────────────────────────────────────────────────
|
|
311
|
+
|
|
312
|
+
def feedback(self, memory_ids: list, user_id=None, useful=True) -> dict:
|
|
313
|
+
"""Submit retrieval usefulness feedback (best-effort, never raises)."""
|
|
314
|
+
if not memory_ids:
|
|
315
|
+
raise ACISValidationError("memory_ids is required")
|
|
316
|
+
uid = user_id or self.user_id
|
|
317
|
+
if not uid:
|
|
318
|
+
raise ACISValidationError("user_id is required for feedback")
|
|
319
|
+
try:
|
|
320
|
+
return self._post("/feedback", json={"memory_ids": memory_ids, "user_id": uid, "useful": useful})
|
|
321
|
+
except ACISError:
|
|
322
|
+
return {"status": "error", "count": 0}
|
|
323
|
+
|
|
324
|
+
# ─── GDPR ─────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
def delete_user(self, user_id=None) -> dict:
|
|
327
|
+
"""Hard-delete all user data (GDPR right-to-erasure)."""
|
|
328
|
+
uid = user_id or self.user_id
|
|
329
|
+
if not uid:
|
|
330
|
+
raise ACISValidationError("user_id is required")
|
|
331
|
+
return self._delete(f"/users/{uid}")
|
|
332
|
+
|
|
333
|
+
def export_user(self, user_id=None) -> dict:
|
|
334
|
+
"""Export all user data (GDPR data portability)."""
|
|
335
|
+
uid = user_id or self.user_id
|
|
336
|
+
if not uid:
|
|
337
|
+
raise ACISValidationError("user_id is required")
|
|
338
|
+
return self._get(f"/users/{uid}/export")
|
|
339
|
+
|
|
340
|
+
# ─── Message Builder ──────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
def _build_messages(self, context_payload: list, query: str) -> list:
|
|
343
|
+
messages = []
|
|
344
|
+
system_parts = []
|
|
345
|
+
for block in context_payload:
|
|
346
|
+
t = block.get("type")
|
|
347
|
+
if t == "rules":
|
|
348
|
+
system_parts.append(f"[RULES]\n{block['content']}")
|
|
349
|
+
elif t == "profile_summary":
|
|
350
|
+
system_parts.append(block["content"])
|
|
351
|
+
elif t == "entity_graph":
|
|
352
|
+
system_parts.append(block["content"])
|
|
353
|
+
elif t == "memories":
|
|
354
|
+
inst = block.get("system_instruction", "")
|
|
355
|
+
if inst: system_parts.append(inst)
|
|
356
|
+
system_parts.append(block["content"])
|
|
357
|
+
elif t == "summary":
|
|
358
|
+
system_parts.append(f"[CONVERSATION SUMMARY]\n{block['content']}")
|
|
359
|
+
elif t == "events":
|
|
360
|
+
evts = block.get("content", [])
|
|
361
|
+
if isinstance(evts, list):
|
|
362
|
+
for e in evts:
|
|
363
|
+
if e.get("content"):
|
|
364
|
+
messages.append({"role": e.get("role", "user"), "content": e["content"]})
|
|
365
|
+
if system_parts:
|
|
366
|
+
messages.insert(0, {"role": "system", "content": "\n\n".join(system_parts)})
|
|
367
|
+
if not messages or messages[-1].get("content") != query:
|
|
368
|
+
messages.append({"role": "user", "content": query})
|
|
369
|
+
return messages
|
acis/exceptions.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""ACIS SDK exceptions — typed error hierarchy."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ACISError(Exception):
|
|
5
|
+
"""Base exception for all ACIS SDK errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, code: str = "ACIS_ERROR"):
|
|
8
|
+
self.message = message
|
|
9
|
+
self.code = code
|
|
10
|
+
super().__init__(message)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ACISRateLimitError(ACISError):
|
|
14
|
+
"""Raised when a rate limit is exceeded (HTTP 429 equivalent)."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str, retry_after: float = 0):
|
|
17
|
+
self.retry_after = retry_after
|
|
18
|
+
super().__init__(message, code="RATE_LIMIT_EXCEEDED")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ACISSessionNotFoundError(ACISError):
|
|
22
|
+
"""Raised when a session does not exist."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, session_id: str):
|
|
25
|
+
self.session_id = session_id
|
|
26
|
+
super().__init__(f"Session not found: {session_id}", code="SESSION_NOT_FOUND")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ACISCircuitOpenError(ACISError):
|
|
30
|
+
"""Raised when the write circuit breaker is open."""
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__("Service temporarily unavailable (circuit open)", code="CIRCUIT_OPEN")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ACISValidationError(ACISError):
|
|
37
|
+
"""Raised on invalid input parameters."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str):
|
|
40
|
+
super().__init__(message, code="VALIDATION_ERROR")
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: acis-memory
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: ACIS — AI Context & Intelligent System SDK
|
|
5
|
+
Requires-Python: >=3.9
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: httpx>=0.25.0
|
|
8
|
+
|
|
9
|
+
# ACIS SDK
|
|
10
|
+
|
|
11
|
+
> AI Context & Intelligent System — Memory for LLM Applications
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install "git+https://oauth2:<your-gitlab-token>@git.skaleup.tech/genai-projects/chatbot/genai-hc-stm-ltm.git@UAT#subdirectory=sdk_pkg"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> **Note:** Use a GitLab Personal Access Token with `read_repository` scope. Prefix with `oauth2:` as username.
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from acis import MemoryClient
|
|
25
|
+
|
|
26
|
+
# Point to your ACIS deployment
|
|
27
|
+
client = MemoryClient(
|
|
28
|
+
tenant_id="your-tenant-uuid",
|
|
29
|
+
base_url="http://10.0.1.50:8000", # ACIS server IP
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Create session
|
|
33
|
+
session = client.create_session(user_id="user-123")
|
|
34
|
+
|
|
35
|
+
# Every turn:
|
|
36
|
+
client.add_event(session_id=session.session_id, role="user", content="Hello")
|
|
37
|
+
ctx = client.get_context(session_id=session.session_id, query="Hello")
|
|
38
|
+
|
|
39
|
+
# ctx.messages → pass directly to any LLM (OpenAI, Azure, Anthropic, etc.)
|
|
40
|
+
response = your_llm(messages=ctx.messages)
|
|
41
|
+
|
|
42
|
+
client.add_event(session_id=session.session_id, role="assistant", content=response)
|
|
43
|
+
|
|
44
|
+
# End conversation
|
|
45
|
+
client.close_session(session_id=session.session_id)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Only Dependency
|
|
49
|
+
|
|
50
|
+
- `httpx` (HTTP client)
|
|
51
|
+
|
|
52
|
+
That's it. No PostgreSQL, Redis, Celery, or Django needed on the app server.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
acis/__init__.py,sha256=E0pWCXLKPSK27Ve9Cq4tNIrj1kBTFQRVxbXkaht_S6Y,809
|
|
2
|
+
acis/client.py,sha256=Mo5-rYrbFdcnaXn0aqPZXe7AGn1UUDglsfTQHCaLg7I,16767
|
|
3
|
+
acis/exceptions.py,sha256=KjTsYZzNai1tP-md2zsxwASiRC6xzaV-Xf-MOJ8QtPM,1223
|
|
4
|
+
acis_memory-1.0.0.dist-info/METADATA,sha256=jkvfTeQ0_mIA5KsS0ehX2hsWzTTwdS-YXxnGSGtTug0,1406
|
|
5
|
+
acis_memory-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
6
|
+
acis_memory-1.0.0.dist-info/top_level.txt,sha256=5JZS4JbNKpVOV9r10ksTpDcGge0pB0NluBCO-O_wgQY,5
|
|
7
|
+
acis_memory-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
acis
|