From 7afc24d91fb233c646fe8e036e0cb276628e767f Mon Sep 17 00:00:00 2001 From: ApiliumDevTeam Date: Sat, 11 Jul 2026 12:05:43 +0200 Subject: [PATCH] feat: port the SDK to the AIngle Cortex REST API (v0.2.0) The old surface targeted the obsolete distributed-ledger/IoT API (entries, nodes, EntryHash, /api/v1/entries, port 8080). AIngle is now the verifiable memory cortex for AI agents, so this rewrites the SDK as a client of the current Cortex REST API: memory (remember/recall/search), triples, query, and health. Adds typed models, a typed error, a README that repositions AIngle accordingly, and a CI workflow. Bumps to 0.2.0. Verified locally: import and py_compile pass. Dependencies trimmed to httpx. --- .github/workflows/ci.yml | 18 ++ README.md | 149 +++++++------ pyproject.toml | 10 +- src/aingle_sdk/__init__.py | 53 ++++- src/aingle_sdk/client.py | 432 ++++++++++++++++++++++++------------- src/aingle_sdk/types.py | 284 +++++++++++++++++++----- src/aingle_sdk/version.py | 2 +- 7 files changed, 661 insertions(+), 287 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..61b9569 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,18 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install . + - run: python -c "import aingle_sdk; print(aingle_sdk.__version__)" + - run: python -m py_compile src/aingle_sdk/*.py diff --git a/README.md b/README.md index cc7afd3..649af8b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ # AIngle SDK for Python -Official Python SDK for [AIngle](https://apilium.com) - the ultra-light distributed ledger for IoT devices. +Python SDK for [AIngle](https://apilium.com), the verifiable memory cortex for +AI agents. AIngle Cortex is a semantic graph plus vector memory served over a +REST API, so your agents can remember, recall, and reason over durable, +queryable knowledge. ## Installation @@ -11,104 +14,122 @@ pip install aingle-sdk ## Quick Start ```python -import asyncio from aingle_sdk import AIngleClient -async def main(): - async with AIngleClient(node_url="http://localhost:8080") as client: - # Create an entry - hash = await client.create_entry({ - "type": "sensor_reading", - "value": 23.5, - "unit": "celsius", - }) - print(f"Created entry: {hash}") - - # Retrieve an entry - entry = await client.get_entry(hash) - print(entry) - - # Get node info - info = await client.get_node_info() - print(f"Node version: {info.version}") - -asyncio.run(main()) +client = AIngleClient() # defaults to http://127.0.0.1:19090 + +# Remember a note. +saved = client.remember( + "note", + {"text": "Ada prefers dark roast coffee"}, + tags=["preference"], + importance=0.7, +) +print("stored id:", saved.id) + +# Recall it later by semantic text. +hits = client.recall(text="what coffee does Ada like?", limit=5) +for hit in hits: + print(hit.relevance, hit.data) ``` -## Subscribe to Real-time Updates +## Configuration + +| Parameter | Type | Default | Description | +|------------|-----------------|-----------------------------|--------------------------------------| +| `base_url` | `str` | `http://127.0.0.1:19090` | AIngle Cortex base URL. | +| `token` | `str` or `None` | `None` | Optional bearer token for a namespace. | +| `timeout` | `float` | `30.0` | Request timeout in seconds. | + +Pass a token when a namespace requires authentication: ```python -import asyncio -from aingle_sdk import AIngleClient +client = AIngleClient(base_url="https://cortex.example.com", token="my-token") +``` + +## API Reference + +All methods are synchronous and raise `AIngleError(status, message)` on any +non-2xx response. + +### Health and stats -async def main(): - client = AIngleClient() - await client.connect() +| Method | Description | +|------------------|----------------------------------------| +| `health()` | Service health and component status. | +| `stats()` | Graph and server statistics. | - def on_entry(entry): - print(f"New entry: {entry.hash}") +### Memory - unsubscribe = await client.subscribe(on_entry) +| Method | Description | +|---------------------------------------------------------|------------------------------------------| +| `remember(entry_type, data, *, tags, importance, embedding)` | Store a memory, returns `{ id }`. | +| `recall(*, text, tags, entry_type, min_importance, limit)` | Recall memories by text or tags. | +| `search(*, embedding, k, min_similarity, entry_type, tags)` | Vector / semantic search. | +| `memory_stats()` | Short and long term memory counts. | +| `forget(id)` | Delete a memory by id. | - # Keep running for 60 seconds - await asyncio.sleep(60) +### Triples (semantic graph) - unsubscribe() - await client.disconnect() +The triple `object` is an untagged value: `str`, `int`, `float`, `bool`, or a +node reference `{"node": "http://example.org/thing"}`. Use the `node_ref` +helper to build a node reference. -asyncio.run(main()) +```python +from aingle_sdk import node_ref + +client.create_triple("ada", "likes", "coffee") +client.create_triple("ada", "knows", node_ref("http://example.org/grace")) ``` -## API Reference +| Method | Description | +|--------------------------------------------------------------|-----------------------------------| +| `create_triple(subject, predicate, object)` | Insert one triple. | +| `list_triples(*, subject, predicate, object, limit, offset)` | List triples with filters. | +| `get_triple(id)` | Fetch a triple by id. | +| `delete_triple(id)` | Delete a triple by id. | + +### Query -### AIngleClient +| Method | Description | +|-------------------------------------------------|--------------------------------------| +| `query(*, subject, predicate, object, limit)` | Pattern match over triples. | +| `subjects(*, predicate, limit)` | Distinct subjects, optional filter. | +| `predicates(*, subject, limit)` | Distinct predicates, optional filter. | -| Method | Description | -|--------|-------------| -| `connect()` | Connect to the AIngle node | -| `disconnect()` | Disconnect from the node | -| `create_entry(data)` | Create a new entry | -| `get_entry(hash)` | Retrieve an entry by hash | -| `get_node_info()` | Get node information | -| `subscribe(callback)` | Subscribe to real-time updates | +## Error handling -### Configuration +```python +from aingle_sdk import AIngleClient, AIngleError -| Parameter | Type | Default | Description | -|-----------|------|---------|-------------| -| `node_url` | `str` | `http://localhost:8080` | Node URL | -| `ws_url` | `str` | `ws://localhost:8081` | WebSocket URL | -| `timeout` | `float` | `30.0` | Request timeout (seconds) | -| `debug` | `bool` | `False` | Enable debug logging | +client = AIngleClient() +try: + client.get_triple("does-not-exist") +except AIngleError as err: + print(err.status, err.message) +``` ## Development ```bash -# Install dev dependencies +# Install dev dependencies. pip install -e ".[dev]" -# Run tests +# Run tests. pytest -# Run tests with coverage -pytest --cov=aingle_sdk - -# Type checking +# Type checking. mypy src -# Linting +# Linting. ruff check src - -# Format code -black src ``` ## License -Apache-2.0 - see [LICENSE](LICENSE) +Apache-2.0, see [LICENSE](LICENSE). ## Links - [AIngle Core](https://github.com/ApiliumCode/aingle) - [Documentation](https://docs.apilium.com) -- [Discord](https://discord.gg/apilium) diff --git a/pyproject.toml b/pyproject.toml index abb2dc6..cea3d6b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,15 +4,15 @@ build-backend = "hatchling.build" [project] name = "aingle-sdk" -version = "0.1.0" -description = "AIngle SDK for Python - Data science, scripts, backend" +version = "0.2.0" +description = "Python SDK for the AIngle Cortex REST API, the verifiable memory cortex for AI agents." readme = "README.md" license = "Apache-2.0" requires-python = ">=3.9" authors = [ { name = "Apilium Technologies", email = "hello@apilium.com" } ] -keywords = ["aingle", "distributed", "dag", "iot", "blockchain", "p2p", "sdk"] +keywords = ["aingle", "cortex", "memory", "semantic-graph", "ai-agents", "sdk"] classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", @@ -23,12 +23,10 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: System :: Distributed Computing", + "Topic :: Scientific/Engineering :: Artificial Intelligence", ] dependencies = [ "httpx>=0.27.0", - "websockets>=12.0", - "pydantic>=2.0", ] [project.optional-dependencies] diff --git a/src/aingle_sdk/__init__.py b/src/aingle_sdk/__init__.py index a56413e..2c0bd7e 100644 --- a/src/aingle_sdk/__init__.py +++ b/src/aingle_sdk/__init__.py @@ -1,21 +1,54 @@ """ -AIngle SDK for Python +AIngle SDK for Python. -Official Python SDK for AIngle - the ultra-light distributed ledger for IoT devices. +An HTTP client for the AIngle Cortex REST API, the verifiable memory cortex for +AI agents. """ -from .client import AIngleClient, AIngleClientConfig -from .types import Entry, EntryHash, NodeInfo, PeerInfo, AIngleError, ErrorCode +from .client import AIngleClient +from .types import ( + AIngleError, + BatchInsertResult, + ComponentHealth, + CreateTriple, + GraphStats, + Health, + HealthComponents, + MemoryStats, + PredicatesResult, + QueryResult, + RecallResult, + RememberResponse, + ServerStats, + Stats, + SubjectsResult, + Triple, + TripleList, + Value, + node_ref, +) from .version import __version__ __all__ = [ "AIngleClient", - "AIngleClientConfig", - "Entry", - "EntryHash", - "NodeInfo", - "PeerInfo", "AIngleError", - "ErrorCode", + "BatchInsertResult", + "ComponentHealth", + "CreateTriple", + "GraphStats", + "Health", + "HealthComponents", + "MemoryStats", + "PredicatesResult", + "QueryResult", + "RecallResult", + "RememberResponse", + "ServerStats", + "Stats", + "SubjectsResult", + "Triple", + "TripleList", + "Value", + "node_ref", "__version__", ] diff --git a/src/aingle_sdk/client.py b/src/aingle_sdk/client.py index b2dcc3f..d8e26ae 100644 --- a/src/aingle_sdk/client.py +++ b/src/aingle_sdk/client.py @@ -1,188 +1,324 @@ """ -AIngle Client - Main entry point for interacting with AIngle network +AIngle Cortex client. + +A small, synchronous HTTP client for the AIngle Cortex REST API, the verifiable +memory cortex for AI agents. Built on httpx. """ from __future__ import annotations -import asyncio -from dataclasses import dataclass, field -from typing import Any, Callable, Optional +import json as _json +from typing import Any, Dict, List, Optional import httpx -import websockets -from websockets.client import WebSocketClientProtocol - -from .types import Entry, EntryHash, NodeInfo - -@dataclass -class AIngleClientConfig: - """Configuration for AIngle client.""" - - node_url: str = "http://localhost:8080" - ws_url: str = "ws://localhost:8081" - timeout: float = 30.0 - debug: bool = False +from .types import ( + AIngleError, + BatchInsertResult, + CreateTriple, + Health, + MemoryStats, + PredicatesResult, + QueryResult, + RecallResult, + RememberResponse, + Stats, + SubjectsResult, + Triple, + TripleList, + Value, + parse_batch_insert, + parse_health, + parse_memory_stats, + parse_query_result, + parse_recall_result, + parse_stats, + parse_triple, + parse_triple_list, +) + +DEFAULT_BASE_URL = "http://127.0.0.1:19090" +DEFAULT_TIMEOUT = 30.0 class AIngleClient: """ - AIngle Client for interacting with AIngle nodes. + Client for the AIngle Cortex REST API. Example: ```python from aingle_sdk import AIngleClient - async def main(): - client = AIngleClient(node_url="http://localhost:8080") - - # Create an entry - hash = await client.create_entry({"data": "Hello, AIngle!"}) - - # Retrieve an entry - entry = await client.get_entry(hash) - print(entry) - - asyncio.run(main()) + client = AIngleClient() + result = client.remember("note", {"text": "buy milk"}, tags=["todo"]) + hits = client.recall(text="milk") + print(hits[0].data) ``` """ def __init__( self, - node_url: str = "http://localhost:8080", - ws_url: str = "ws://localhost:8081", - timeout: float = 30.0, - debug: bool = False, + base_url: str = DEFAULT_BASE_URL, + token: Optional[str] = None, + timeout: float = DEFAULT_TIMEOUT, ) -> None: - self.config = AIngleClientConfig( - node_url=node_url, - ws_url=ws_url, - timeout=timeout, - debug=debug, - ) - self._http_client: Optional[httpx.AsyncClient] = None - self._ws: Optional[WebSocketClientProtocol] = None - - async def __aenter__(self) -> "AIngleClient": - await self.connect() - return self + self.base_url = base_url.rstrip("/") + self.token = token + self.timeout = timeout - async def __aexit__(self, *args: Any) -> None: - await self.disconnect() + headers = {"Content-Type": "application/json"} + if token: + headers["Authorization"] = f"Bearer {token}" - async def connect(self) -> None: - """Connect to the AIngle node.""" - if self.config.debug: - print(f"Connecting to {self.config.node_url}") - - self._http_client = httpx.AsyncClient( - base_url=self.config.node_url, - timeout=self.config.timeout, - ) - - async def disconnect(self) -> None: - """Disconnect from the AIngle node.""" - if self._http_client: - await self._http_client.aclose() - self._http_client = None - - if self._ws: - await self._ws.close() - self._ws = None - - async def create_entry(self, data: Any) -> EntryHash: - """ - Create a new entry in the DAG. - - Args: - data: Entry payload (will be JSON serialized) - - Returns: - Hash of the created entry - """ - if not self._http_client: - await self.connect() - - assert self._http_client is not None - - response = await self._http_client.post( - "/api/v1/entries", - json={"data": data}, + self._http = httpx.Client( + base_url=self.base_url, + headers=headers, + timeout=timeout, ) - response.raise_for_status() - - result = response.json() - return result["hash"] - async def get_entry(self, hash: EntryHash) -> Optional[Entry]: - """ - Retrieve an entry by hash. - - Args: - hash: Entry hash + def __enter__(self) -> "AIngleClient": + return self - Returns: - Entry if found, None otherwise - """ - if not self._http_client: - await self.connect() + def __exit__(self, *args: Any) -> None: + self.close() - assert self._http_client is not None + def close(self) -> None: + """Close the underlying HTTP connection pool.""" + self._http.close() - response = await self._http_client.get(f"/api/v1/entries/{hash}") + # --- internal helpers ------------------------------------------------- - if response.status_code == 404: + def _request( + self, + method: str, + path: str, + *, + params: Optional[Dict[str, Any]] = None, + json: Optional[Any] = None, + ) -> Any: + clean_params = None + if params is not None: + clean_params = { + k: (_json.dumps(v) if isinstance(v, (dict, bool)) else v) + for k, v in params.items() + if v is not None + } + + try: + response = self._http.request( + method, + path, + params=clean_params, + json=json, + ) + except httpx.HTTPError as exc: # network / transport failure + raise AIngleError(0, str(exc)) from exc + + if response.status_code >= 400: + raise AIngleError(response.status_code, self._error_message(response)) + + if response.status_code == 204 or not response.content: return None - - response.raise_for_status() - return Entry(**response.json()) - - async def get_node_info(self) -> NodeInfo: - """ - Get node information. - - Returns: - Node information - """ - if not self._http_client: - await self.connect() - - assert self._http_client is not None - - response = await self._http_client.get("/api/v1/info") - response.raise_for_status() - - return NodeInfo(**response.json()) - - async def subscribe( + return response.json() + + @staticmethod + def _error_message(response: httpx.Response) -> str: + try: + body = response.json() + except ValueError: + return response.text or response.reason_phrase + if isinstance(body, dict): + for key in ("message", "error", "detail"): + val = body.get(key) + if isinstance(val, str): + return val + return response.text or response.reason_phrase + + # --- health & stats --------------------------------------------------- + + def health(self) -> Health: + """GET /api/v1/health""" + return parse_health(self._request("GET", "/api/v1/health")) + + def stats(self) -> Stats: + """GET /api/v1/stats""" + return parse_stats(self._request("GET", "/api/v1/stats")) + + # --- memory ----------------------------------------------------------- + + def remember( + self, + entry_type: str, + data: Any, + *, + tags: Optional[List[str]] = None, + importance: float = 0.0, + embedding: Optional[List[float]] = None, + ) -> RememberResponse: + """POST /api/v1/memory/remember""" + body: Dict[str, Any] = { + "entry_type": entry_type, + "data": data, + "tags": tags or [], + "importance": importance, + } + if embedding is not None: + body["embedding"] = embedding + raw = self._request("POST", "/api/v1/memory/remember", json=body) + return RememberResponse(id=raw["id"]) + + def recall( self, - callback: Callable[[Entry], None], - ) -> Callable[[], None]: - """ - Subscribe to real-time updates. + *, + text: Optional[str] = None, + tags: Optional[List[str]] = None, + entry_type: Optional[str] = None, + min_importance: Optional[float] = None, + limit: Optional[int] = None, + ) -> List[RecallResult]: + """POST /api/v1/memory/recall""" + body: Dict[str, Any] = {"tags": tags or []} + if text is not None: + body["text"] = text + if entry_type is not None: + body["entry_type"] = entry_type + if min_importance is not None: + body["min_importance"] = min_importance + if limit is not None: + body["limit"] = limit + raw = self._request("POST", "/api/v1/memory/recall", json=body) + return [parse_recall_result(r) for r in raw] + + def search( + self, + *, + embedding: List[float], + k: int, + min_similarity: float = 0.0, + entry_type: Optional[str] = None, + tags: Optional[List[str]] = None, + ) -> List[RecallResult]: + """POST /api/v1/memory/search (vector / semantic search)""" + body: Dict[str, Any] = { + "embedding": embedding, + "k": k, + "min_similarity": min_similarity, + } + if entry_type is not None: + body["entry_type"] = entry_type + if tags is not None: + body["tags"] = tags + raw = self._request("POST", "/api/v1/memory/search", json=body) + return [parse_recall_result(r) for r in raw] + + def memory_stats(self) -> MemoryStats: + """GET /api/v1/memory/stats""" + return parse_memory_stats(self._request("GET", "/api/v1/memory/stats")) + + def forget(self, id: str) -> None: + """DELETE /api/v1/memory/{id}""" + self._request("DELETE", f"/api/v1/memory/{id}") + + # --- triples ---------------------------------------------------------- + + def create_triple( + self, subject: str, predicate: str, object: Value + ) -> Triple: + """POST /api/v1/triples""" + body = {"subject": subject, "predicate": predicate, "object": object} + return parse_triple(self._request("POST", "/api/v1/triples", json=body)) + + def list_triples( + self, + *, + subject: Optional[str] = None, + predicate: Optional[str] = None, + object: Optional[Value] = None, + limit: Optional[int] = None, + offset: Optional[int] = None, + ) -> TripleList: + """GET /api/v1/triples""" + params: Dict[str, Any] = { + "subject": subject, + "predicate": predicate, + "object": object, + "limit": limit, + "offset": offset, + } + return parse_triple_list( + self._request("GET", "/api/v1/triples", params=params) + ) - Args: - callback: Function to call when new entries arrive + def create_triples(self, triples: List[CreateTriple]) -> BatchInsertResult: + """POST /api/v1/triples/batch""" + body = { + "triples": [ + { + "subject": t.subject, + "predicate": t.predicate, + "object": t.object, + } + for t in triples + ] + } + return parse_batch_insert( + self._request("POST", "/api/v1/triples/batch", json=body) + ) - Returns: - Unsubscribe function - """ - self._ws = await websockets.connect(self.config.ws_url) + def get_triple(self, id: str) -> Triple: + """GET /api/v1/triples/{id}""" + return parse_triple(self._request("GET", f"/api/v1/triples/{id}")) - async def listen() -> None: - assert self._ws is not None - async for message in self._ws: - import json + def delete_triple(self, id: str) -> None: + """DELETE /api/v1/triples/{id}""" + self._request("DELETE", f"/api/v1/triples/{id}") - entry_data = json.loads(message) - entry = Entry(**entry_data) - callback(entry) + # --- query ------------------------------------------------------------ - task = asyncio.create_task(listen()) + def query( + self, + *, + subject: Optional[str] = None, + predicate: Optional[str] = None, + object: Optional[Value] = None, + limit: Optional[int] = None, + ) -> QueryResult: + """POST /api/v1/query""" + body: Dict[str, Any] = {} + if subject is not None: + body["subject"] = subject + if predicate is not None: + body["predicate"] = predicate + if object is not None: + body["object"] = object + if limit is not None: + body["limit"] = limit + return parse_query_result( + self._request("POST", "/api/v1/query", json=body) + ) - def unsubscribe() -> None: - task.cancel() - if self._ws: - asyncio.create_task(self._ws.close()) + def subjects( + self, + *, + predicate: Optional[str] = None, + limit: Optional[int] = None, + ) -> SubjectsResult: + """GET /api/v1/query/subjects""" + params = {"predicate": predicate, "limit": limit} + raw = self._request("GET", "/api/v1/query/subjects", params=params) + return SubjectsResult( + subjects=list(raw.get("subjects", [])), total=raw.get("total", 0) + ) - return unsubscribe + def predicates( + self, + *, + subject: Optional[str] = None, + limit: Optional[int] = None, + ) -> PredicatesResult: + """GET /api/v1/query/predicates""" + params = {"subject": subject, "limit": limit} + raw = self._request("GET", "/api/v1/query/predicates", params=params) + return PredicatesResult( + predicates=list(raw.get("predicates", [])), total=raw.get("total", 0) + ) diff --git a/src/aingle_sdk/types.py b/src/aingle_sdk/types.py index fbaa73d..faf10ed 100644 --- a/src/aingle_sdk/types.py +++ b/src/aingle_sdk/types.py @@ -1,87 +1,255 @@ """ -AIngle SDK Type Definitions +AIngle Cortex SDK type definitions. + +Typed request/response models for the AIngle Cortex REST API, implemented with +plain dataclasses to keep the package dependency-light. """ from __future__ import annotations -from dataclasses import dataclass -from enum import Enum -from typing import Any, List, Optional +from dataclasses import dataclass, field +from typing import Any, Dict, List, Optional, Union + +# A triple's ``object`` is an untagged union serialized as the raw JSON value: +# string -> "hello" +# integer -> 42 +# float -> 4.2 +# boolean -> true +# node ref (IRI) -> {"node": "http://example.org/thing"} +Value = Union[str, int, float, bool, Dict[str, str]] + +def node_ref(iri: str) -> Dict[str, str]: + """Build a node-reference (IRI) object Value: ``{"node": iri}``.""" + return {"node": iri} + + +class AIngleError(Exception): + """Typed error raised for non-2xx responses from the AIngle Cortex API.""" -# Type aliases -EntryHash = str -AgentPubKey = str -Timestamp = int + def __init__(self, status: int, message: str) -> None: + super().__init__(f"[{status}] {message}") + self.status = status + self.message = message + + +# --- Health & stats ------------------------------------------------------- @dataclass -class Entry: - """Entry in the AIngle DAG.""" +class ComponentHealth: + status: str + message: Optional[str] = None - hash: EntryHash - author: AgentPubKey - parents: List[EntryHash] - data: Any - timestamp: Timestamp - sequence: int - signature: str + +@dataclass +class HealthComponents: + graph: ComponentHealth + logic: ComponentHealth @dataclass -class NodeInfo: - """Node information.""" +class Health: + status: str + components: HealthComponents - node_id: str + +@dataclass +class GraphStats: + triple_count: int + subject_count: int + predicate_count: int + object_count: int + + +@dataclass +class ServerStats: + connected_clients: int + uptime_seconds: int version: str - uptime: int - entries_count: int - peers_count: int - storage_backend: str - features: List[str] @dataclass -class PeerInfo: - """Peer information.""" +class Stats: + graph: GraphStats + server: ServerStats + - peer_id: str - address: str - quality: int - last_seen: Timestamp - latest_seq: int +# --- Memory --------------------------------------------------------------- @dataclass -class SyncStatus: - """Sync status.""" +class RememberResponse: + id: str - syncing: bool - pending: int - last_sync: Timestamp +@dataclass +class RecallResult: + id: str + entry_type: str + data: Any + tags: List[str] + importance: float + relevance: float + source: str + created_at: str + last_accessed: str + access_count: int -class ErrorCode(Enum): - """Error codes.""" - CONNECTION_FAILED = "CONNECTION_FAILED" - TIMEOUT = "TIMEOUT" - NOT_FOUND = "NOT_FOUND" - INVALID_ENTRY = "INVALID_ENTRY" - STORAGE_ERROR = "STORAGE_ERROR" - NETWORK_ERROR = "NETWORK_ERROR" - AUTH_ERROR = "AUTH_ERROR" +@dataclass +class MemoryStats: + stm_count: int + stm_capacity: int + ltm_entity_count: int + ltm_link_count: int + total_memory_bytes: int -class AIngleError(Exception): - """SDK Error.""" - - def __init__( - self, - code: ErrorCode, - message: str, - cause: Optional[Exception] = None, - ) -> None: - super().__init__(message) - self.code = code - self.cause = cause +# --- Triples -------------------------------------------------------------- + + +@dataclass +class Triple: + subject: str + predicate: str + object: Value + id: Optional[str] = None + created_at: Optional[str] = None + + +@dataclass +class CreateTriple: + subject: str + predicate: str + object: Value + + +@dataclass +class TripleList: + triples: List[Triple] + total: int + limit: int + offset: int + + +@dataclass +class BatchInsertResult: + inserted: List[Triple] + total: int + duplicates: int + + +# --- Query ---------------------------------------------------------------- + + +@dataclass +class QueryResult: + matches: List[Triple] + total: int + pattern: Any + + +@dataclass +class SubjectsResult: + subjects: List[str] + total: int + + +@dataclass +class PredicatesResult: + predicates: List[str] + total: int + + +def _component(raw: Dict[str, Any]) -> ComponentHealth: + return ComponentHealth(status=raw["status"], message=raw.get("message")) + + +def parse_health(raw: Dict[str, Any]) -> Health: + comps = raw.get("components", {}) + return Health( + status=raw["status"], + components=HealthComponents( + graph=_component(comps.get("graph", {})), + logic=_component(comps.get("logic", {})), + ), + ) + + +def parse_stats(raw: Dict[str, Any]) -> Stats: + g = raw.get("graph", {}) + s = raw.get("server", {}) + return Stats( + graph=GraphStats( + triple_count=g.get("triple_count", 0), + subject_count=g.get("subject_count", 0), + predicate_count=g.get("predicate_count", 0), + object_count=g.get("object_count", 0), + ), + server=ServerStats( + connected_clients=s.get("connected_clients", 0), + uptime_seconds=s.get("uptime_seconds", 0), + version=s.get("version", ""), + ), + ) + + +def parse_recall_result(raw: Dict[str, Any]) -> RecallResult: + return RecallResult( + id=raw["id"], + entry_type=raw["entry_type"], + data=raw.get("data"), + tags=list(raw.get("tags", [])), + importance=raw.get("importance", 0.0), + relevance=raw.get("relevance", 0.0), + source=raw.get("source", ""), + created_at=raw.get("created_at", ""), + last_accessed=raw.get("last_accessed", ""), + access_count=raw.get("access_count", 0), + ) + + +def parse_memory_stats(raw: Dict[str, Any]) -> MemoryStats: + return MemoryStats( + stm_count=raw.get("stm_count", 0), + stm_capacity=raw.get("stm_capacity", 0), + ltm_entity_count=raw.get("ltm_entity_count", 0), + ltm_link_count=raw.get("ltm_link_count", 0), + total_memory_bytes=raw.get("total_memory_bytes", 0), + ) + + +def parse_triple(raw: Dict[str, Any]) -> Triple: + return Triple( + subject=raw["subject"], + predicate=raw["predicate"], + object=raw["object"], + id=raw.get("id"), + created_at=raw.get("created_at"), + ) + + +def parse_triple_list(raw: Dict[str, Any]) -> TripleList: + return TripleList( + triples=[parse_triple(t) for t in raw.get("triples", [])], + total=raw.get("total", 0), + limit=raw.get("limit", 0), + offset=raw.get("offset", 0), + ) + + +def parse_batch_insert(raw: Dict[str, Any]) -> BatchInsertResult: + return BatchInsertResult( + inserted=[parse_triple(t) for t in raw.get("inserted", [])], + total=raw.get("total", 0), + duplicates=raw.get("duplicates", 0), + ) + + +def parse_query_result(raw: Dict[str, Any]) -> QueryResult: + return QueryResult( + matches=[parse_triple(t) for t in raw.get("matches", [])], + total=raw.get("total", 0), + pattern=raw.get("pattern"), + ) diff --git a/src/aingle_sdk/version.py b/src/aingle_sdk/version.py index e58a6ce..b5a2cb9 100644 --- a/src/aingle_sdk/version.py +++ b/src/aingle_sdk/version.py @@ -1,3 +1,3 @@ """Version information.""" -__version__ = "0.1.0" +__version__ = "0.2.0"