sovant 1.0.7__py3-none-any.whl → 1.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.
- sovant/__init__.py +1 -1
- sovant/client.py +158 -28
- {sovant-1.0.7.dist-info → sovant-1.2.0.dist-info}/METADATA +39 -2
- {sovant-1.0.7.dist-info → sovant-1.2.0.dist-info}/RECORD +7 -7
- {sovant-1.0.7.dist-info → sovant-1.2.0.dist-info}/WHEEL +0 -0
- {sovant-1.0.7.dist-info → sovant-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {sovant-1.0.7.dist-info → sovant-1.2.0.dist-info}/top_level.txt +0 -0
sovant/__init__.py
CHANGED
sovant/client.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
|
+
import time
|
|
3
4
|
import httpx
|
|
4
|
-
from typing import Any, Dict
|
|
5
|
+
from typing import Any, Dict, Optional, Callable
|
|
5
6
|
from .models import MemoryCreate, SearchQuery
|
|
6
7
|
|
|
7
8
|
class SovantError(Exception):
|
|
@@ -12,12 +13,27 @@ class SovantError(Exception):
|
|
|
12
13
|
self.details = details
|
|
13
14
|
|
|
14
15
|
class Sovant:
|
|
15
|
-
def __init__(
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
api_key: str | None = None,
|
|
19
|
+
base_url: str | None = None,
|
|
20
|
+
timeout: float = 30.0,
|
|
21
|
+
max_retries: int = 3,
|
|
22
|
+
retry_delay: float = 1.0,
|
|
23
|
+
on_request: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
24
|
+
on_response: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
25
|
+
on_error: Optional[Callable[[SovantError], None]] = None
|
|
26
|
+
):
|
|
16
27
|
self.api_key = api_key or os.getenv("SOVANT_API_KEY")
|
|
17
28
|
if not self.api_key:
|
|
18
29
|
raise ValueError("Missing api_key")
|
|
19
30
|
self.base_url = (base_url or os.getenv("SOVANT_BASE_URL") or "https://sovant.ai").rstrip("/")
|
|
20
31
|
self.timeout = timeout
|
|
32
|
+
self.max_retries = max_retries
|
|
33
|
+
self.retry_delay = retry_delay
|
|
34
|
+
self.on_request = on_request
|
|
35
|
+
self.on_response = on_response
|
|
36
|
+
self.on_error = on_error
|
|
21
37
|
self._client = httpx.Client(
|
|
22
38
|
timeout=self.timeout,
|
|
23
39
|
headers={
|
|
@@ -26,38 +42,107 @@ class Sovant:
|
|
|
26
42
|
}
|
|
27
43
|
)
|
|
28
44
|
|
|
29
|
-
def
|
|
30
|
-
|
|
45
|
+
def _request(self, method: str, url: str, **kwargs):
|
|
46
|
+
"""Internal request method with retry logic and telemetry"""
|
|
47
|
+
start_time = time.time()
|
|
48
|
+
|
|
49
|
+
# Telemetry: onRequest hook
|
|
50
|
+
if self.on_request:
|
|
51
|
+
try:
|
|
52
|
+
self.on_request({
|
|
53
|
+
"method": method,
|
|
54
|
+
"url": url,
|
|
55
|
+
"body": kwargs.get("json") or kwargs.get("content")
|
|
56
|
+
})
|
|
57
|
+
except:
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
last_error = None
|
|
61
|
+
|
|
62
|
+
for attempt in range(self.max_retries + 1):
|
|
31
63
|
try:
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
64
|
+
r = self._client.request(method, url, **kwargs)
|
|
65
|
+
|
|
66
|
+
if r.status_code >= 400:
|
|
67
|
+
try:
|
|
68
|
+
body = r.json()
|
|
69
|
+
except Exception:
|
|
70
|
+
body = {"message": r.text}
|
|
71
|
+
msg = body.get("message") or str(r.reason_phrase) if hasattr(r, 'reason_phrase') else 'Error'
|
|
72
|
+
code = body.get("code") or f"HTTP_{r.status_code}"
|
|
73
|
+
error = SovantError(msg, code, r.status_code, body)
|
|
74
|
+
|
|
75
|
+
# Retry on 429 (rate limit) or 5xx errors
|
|
76
|
+
if attempt < self.max_retries and (r.status_code == 429 or r.status_code >= 500):
|
|
77
|
+
last_error = error
|
|
78
|
+
delay = self.retry_delay * (2 ** attempt) # Exponential backoff
|
|
79
|
+
time.sleep(delay)
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
# Telemetry: onError hook
|
|
83
|
+
if self.on_error:
|
|
84
|
+
try:
|
|
85
|
+
self.on_error(error)
|
|
86
|
+
except:
|
|
87
|
+
pass
|
|
88
|
+
|
|
89
|
+
raise error
|
|
90
|
+
|
|
91
|
+
# Success - telemetry: onResponse hook
|
|
92
|
+
duration = (time.time() - start_time) * 1000 # Convert to ms
|
|
93
|
+
if self.on_response:
|
|
94
|
+
try:
|
|
95
|
+
self.on_response({
|
|
96
|
+
"method": method,
|
|
97
|
+
"url": url,
|
|
98
|
+
"status": r.status_code,
|
|
99
|
+
"duration": duration
|
|
100
|
+
})
|
|
101
|
+
except:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
if not r.text:
|
|
105
|
+
return None
|
|
106
|
+
try:
|
|
107
|
+
return r.json()
|
|
108
|
+
except Exception:
|
|
109
|
+
return r.text
|
|
110
|
+
|
|
111
|
+
except httpx.TimeoutException as e:
|
|
112
|
+
error = SovantError("Request timeout", "TIMEOUT", 408)
|
|
113
|
+
if attempt < self.max_retries:
|
|
114
|
+
last_error = error
|
|
115
|
+
delay = self.retry_delay * (2 ** attempt)
|
|
116
|
+
time.sleep(delay)
|
|
117
|
+
continue
|
|
118
|
+
raise error
|
|
119
|
+
|
|
120
|
+
except httpx.NetworkError as e:
|
|
121
|
+
error = SovantError(str(e), "NETWORK_ERROR", 0)
|
|
122
|
+
if attempt < self.max_retries:
|
|
123
|
+
last_error = error
|
|
124
|
+
delay = self.retry_delay * (2 ** attempt)
|
|
125
|
+
time.sleep(delay)
|
|
126
|
+
continue
|
|
127
|
+
raise error
|
|
128
|
+
|
|
129
|
+
# If we exhausted retries, raise the last error
|
|
130
|
+
raise last_error or SovantError("Max retries exceeded", "MAX_RETRIES", 0)
|
|
44
131
|
|
|
45
132
|
def memory_create(self, create: MemoryCreate):
|
|
46
133
|
# Convert data field to content field for API
|
|
47
134
|
body = create.model_dump()
|
|
48
135
|
if 'data' in body:
|
|
49
136
|
body['content'] = json.dumps(body.pop('data')) if not isinstance(body.get('data'), str) else body.pop('data')
|
|
50
|
-
|
|
137
|
+
|
|
51
138
|
# Ensure type has a default
|
|
52
139
|
if 'type' not in body or body['type'] is None:
|
|
53
140
|
body['type'] = 'journal'
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return self._handle(r)
|
|
141
|
+
|
|
142
|
+
return self._request("POST", f"{self.base_url}/api/v1/memory", json=body)
|
|
57
143
|
|
|
58
144
|
def memory_get(self, id: str):
|
|
59
|
-
|
|
60
|
-
return self._handle(r)
|
|
145
|
+
return self._request("GET", f"{self.base_url}/api/v1/memories/{id}")
|
|
61
146
|
|
|
62
147
|
def memory_search(self, q: SearchQuery):
|
|
63
148
|
params = {}
|
|
@@ -75,16 +160,61 @@ class Sovant:
|
|
|
75
160
|
params['from_date'] = q.from_date
|
|
76
161
|
if q.to_date:
|
|
77
162
|
params['to_date'] = q.to_date
|
|
78
|
-
|
|
79
|
-
|
|
163
|
+
return self._request("GET", f"{self.base_url}/api/v1/memory/search", params=params)
|
|
164
|
+
|
|
165
|
+
def memory_recall(self, query: str, thread_id: str | None = None, limit: int | None = None):
|
|
166
|
+
"""
|
|
167
|
+
Hybrid recall with profile awareness
|
|
168
|
+
|
|
169
|
+
Uses multi-stage pipeline (profile fast-path + lexical + semantic)
|
|
170
|
+
Guarantees profile facts (name/age/location) when available
|
|
171
|
+
|
|
172
|
+
Use recall() for conversational queries ("who am I?", "what do you know about me?")
|
|
173
|
+
Use memory_search() for pure semantic topic lookup
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
query: The search query (required)
|
|
177
|
+
thread_id: Optional thread context for thread-scoped recall
|
|
178
|
+
limit: Maximum results to return (default 8, max 50)
|
|
179
|
+
"""
|
|
180
|
+
params = {'query': query}
|
|
181
|
+
if thread_id:
|
|
182
|
+
params['thread_id'] = thread_id
|
|
183
|
+
if limit:
|
|
184
|
+
params['limit'] = str(limit)
|
|
185
|
+
return self._request("GET", f"{self.base_url}/api/v1/memory/recall", params=params)
|
|
80
186
|
|
|
81
187
|
def memory_update(self, id: str, patch: Dict[str, Any]):
|
|
82
188
|
# Convert data field to content field if present
|
|
83
189
|
if 'data' in patch:
|
|
84
190
|
patch['content'] = json.dumps(patch.pop('data')) if not isinstance(patch.get('data'), str) else patch.pop('data')
|
|
85
|
-
|
|
86
|
-
return self._handle(r)
|
|
191
|
+
return self._request("PATCH", f"{self.base_url}/api/v1/memories/{id}", json=patch)
|
|
87
192
|
|
|
88
193
|
def memory_delete(self, id: str):
|
|
89
|
-
|
|
90
|
-
|
|
194
|
+
return self._request("DELETE", f"{self.base_url}/api/v1/memories/{id}")
|
|
195
|
+
|
|
196
|
+
def memory_create_batch(self, memories: list[Dict[str, Any]]):
|
|
197
|
+
"""
|
|
198
|
+
Batch create multiple memories in a single request
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
memories: List of memory objects (max 100)
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
BatchResponse with individual results
|
|
205
|
+
"""
|
|
206
|
+
operations = []
|
|
207
|
+
for mem in memories:
|
|
208
|
+
data = mem.copy()
|
|
209
|
+
# Convert data field to content field
|
|
210
|
+
if 'data' in data:
|
|
211
|
+
data['content'] = json.dumps(data.pop('data')) if not isinstance(data.get('data'), str) else data.pop('data')
|
|
212
|
+
# Ensure type has default
|
|
213
|
+
if 'type' not in data or data['type'] is None:
|
|
214
|
+
data['type'] = 'journal'
|
|
215
|
+
operations.append({
|
|
216
|
+
"operation": "create",
|
|
217
|
+
"data": data
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
return self._request("POST", f"{self.base_url}/api/v1/memory/batch", json=operations)
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sovant
|
|
3
|
-
Version: 1.0
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: Sovant Memory-as-a-Service Python SDK
|
|
5
5
|
Author: Sovant
|
|
6
6
|
License: MIT
|
|
7
|
+
Project-URL: Documentation, https://sovant.ai/docs
|
|
8
|
+
Project-URL: Source, https://github.com/hechin91/sovant-ai
|
|
9
|
+
Project-URL: Tracker, https://github.com/hechin91/sovant-ai/issues
|
|
7
10
|
Requires-Python: >=3.10
|
|
8
11
|
Description-Content-Type: text/markdown
|
|
9
12
|
License-File: LICENSE
|
|
@@ -13,7 +16,8 @@ Dynamic: license-file
|
|
|
13
16
|
|
|
14
17
|
# Sovant Python SDK
|
|
15
18
|
|
|
16
|
-
Sovant is
|
|
19
|
+
**Sovant is a governed AI memory layer for AI agents and applications.**
|
|
20
|
+
Use it to store, search, and recall memories with profile awareness and enterprise-grade control over how memory is captured and used.
|
|
17
21
|
|
|
18
22
|
[](https://pypi.org/project/sovant/)
|
|
19
23
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -54,6 +58,32 @@ updated = client.memory.update(mem["id"], {
|
|
|
54
58
|
client.memory.delete(mem["id"])
|
|
55
59
|
```
|
|
56
60
|
|
|
61
|
+
## Recall vs Search
|
|
62
|
+
|
|
63
|
+
Sovant provides two ways to query memories:
|
|
64
|
+
|
|
65
|
+
- **`memory_recall()`** – Hybrid recall, profile-aware
|
|
66
|
+
Use for conversational queries: "What do you know about me?", "What happened on Project X?".
|
|
67
|
+
Uses Sovant's hybrid pipeline (profile fast-path + thread-scoped lexical + vector semantic search) and prioritizes profile facts (name/age/location) when available.
|
|
68
|
+
|
|
69
|
+
- **`memory_search()`** – Semantic search
|
|
70
|
+
Use for topic lookup and discovery. Pure vector similarity search without profile logic.
|
|
71
|
+
Behavior unchanged from previous versions.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# Recall for conversational queries
|
|
75
|
+
context = client.memory_recall(
|
|
76
|
+
query="what do you know about me?",
|
|
77
|
+
limit=10
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Search for topic discovery
|
|
81
|
+
topics = client.memory_search({
|
|
82
|
+
"query": "project updates",
|
|
83
|
+
"limit": 5
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
57
87
|
## Chat in 60 Seconds
|
|
58
88
|
|
|
59
89
|
Stream real-time chat responses with memory context:
|
|
@@ -470,6 +500,13 @@ The API implements rate limiting. The SDK automatically handles rate limit respo
|
|
|
470
500
|
|
|
471
501
|
See [CHANGELOG.md](./CHANGELOG.md) for a detailed history of changes.
|
|
472
502
|
|
|
503
|
+
## License & Use
|
|
504
|
+
|
|
505
|
+
- This SDK is MIT-licensed for integration convenience.
|
|
506
|
+
- The Sovant API and platform are proprietary to Sovant Technologies Sdn. Bhd.
|
|
507
|
+
- You may use this SDK to integrate with Sovant's hosted API.
|
|
508
|
+
- Hosting/redistributing the Sovant backend or any proprietary components is not permitted.
|
|
509
|
+
|
|
473
510
|
## License
|
|
474
511
|
|
|
475
512
|
MIT - See [LICENSE](LICENSE) file for details.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
sovant/__init__.py,sha256=
|
|
1
|
+
sovant/__init__.py,sha256=1GYQzmb1Ni5-y874HOs1J-rYrpx-iBzR4_cCEBFIjpk,122
|
|
2
2
|
sovant/base_client.py,sha256=Vmn6OGywGwLbH5cEeflSjVOFwn5iX_YdfTdUq9pWWxA,8778
|
|
3
|
-
sovant/client.py,sha256=
|
|
3
|
+
sovant/client.py,sha256=KQZ5yi4-5gPc7QxuRzJC0tBSfZ6Cohkdl397yuCCA0I,8331
|
|
4
4
|
sovant/exceptions.py,sha256=MQMSgk7ckXnAbe7hPPpbKOnRBHSPxuCY7WSjqfJAvd0,1557
|
|
5
5
|
sovant/models.py,sha256=avDAITMptDDdDfNH_ed854Q7kF6z_1OzjwJ9Xeft_-8,979
|
|
6
6
|
sovant/types.py,sha256=gnvdXksJt8LObti7nc6eHSBCB7Pz7SNpS5o_HRTq_kA,6098
|
|
7
7
|
sovant/resources/__init__.py,sha256=cPFIM7h8duviDdeHudnVEAmv3F89RHQxdH5BCRWFteQ,193
|
|
8
8
|
sovant/resources/memories.py,sha256=bKKE0uWqFkPa1OEvbK1LrdSD7v6N04RhJ_2VDoPPQBA,11379
|
|
9
9
|
sovant/resources/threads.py,sha256=mN29xP0JODmZBKyfhpeqJyViUWNVMAx3TlYAW-1ruTs,12558
|
|
10
|
-
sovant-1.0.
|
|
11
|
-
sovant-1.0.
|
|
12
|
-
sovant-1.0.
|
|
13
|
-
sovant-1.0.
|
|
14
|
-
sovant-1.0.
|
|
10
|
+
sovant-1.2.0.dist-info/licenses/LICENSE,sha256=rnNP6-elrMIlQ9jf2aqnHZMyyQ_wvF3y1hTpVUusCWU,1062
|
|
11
|
+
sovant-1.2.0.dist-info/METADATA,sha256=cPnJTbYhtsWr77V7AJ8CkWCnZwk3p0Uz0eI7vAxK1KA,12753
|
|
12
|
+
sovant-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
13
|
+
sovant-1.2.0.dist-info/top_level.txt,sha256=za6eVEsYd_ZQQs8vrmEWNcAR58r1wCDge_jA60e4CvQ,7
|
|
14
|
+
sovant-1.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|