sovant 1.0.7__py3-none-any.whl → 1.1.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 CHANGED
@@ -1,4 +1,4 @@
1
1
  from .client import Sovant, SovantError
2
2
  from .models import MemoryCreate, MemoryResult, SearchQuery
3
3
 
4
- __version__ = "1.0.7"
4
+ __version__ = "0.1.0"
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__(self, api_key: str | None = None, base_url: str | None = None, timeout: float = 30.0):
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 _handle(self, r: httpx.Response):
30
- if r.status_code >= 400:
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:
31
51
  try:
32
- body = r.json()
33
- except Exception:
34
- body = {"message": r.text}
35
- msg = body.get("message") or r.reason_phrase
36
- code = body.get("code") or f"HTTP_{r.status_code}"
37
- raise SovantError(msg, code, r.status_code, body)
38
- if not r.text:
39
- return None
40
- try:
41
- return r.json()
42
- except Exception:
43
- return r.text
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):
63
+ try:
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
- r = self._client.post(f"{self.base_url}/api/v1/memory", content=json.dumps(body))
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
- r = self._client.get(f"{self.base_url}/api/v1/memories/{id}")
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,43 @@ 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
- r = self._client.get(f"{self.base_url}/api/v1/memory/search", params=params)
79
- return self._handle(r)
163
+ return self._request("GET", f"{self.base_url}/api/v1/memory/search", params=params)
164
+
165
+ def memory_recall(self, q: SearchQuery):
166
+ """Semantic search alias for memory_search()"""
167
+ return self.memory_search(q)
80
168
 
81
169
  def memory_update(self, id: str, patch: Dict[str, Any]):
82
170
  # Convert data field to content field if present
83
171
  if 'data' in patch:
84
172
  patch['content'] = json.dumps(patch.pop('data')) if not isinstance(patch.get('data'), str) else patch.pop('data')
85
- r = self._client.patch(f"{self.base_url}/api/v1/memories/{id}", content=json.dumps(patch))
86
- return self._handle(r)
173
+ return self._request("PATCH", f"{self.base_url}/api/v1/memories/{id}", json=patch)
87
174
 
88
175
  def memory_delete(self, id: str):
89
- r = self._client.delete(f"{self.base_url}/api/v1/memories/{id}")
90
- return self._handle(r)
176
+ return self._request("DELETE", f"{self.base_url}/api/v1/memories/{id}")
177
+
178
+ def memory_create_batch(self, memories: list[Dict[str, Any]]):
179
+ """
180
+ Batch create multiple memories in a single request
181
+
182
+ Args:
183
+ memories: List of memory objects (max 100)
184
+
185
+ Returns:
186
+ BatchResponse with individual results
187
+ """
188
+ operations = []
189
+ for mem in memories:
190
+ data = mem.copy()
191
+ # Convert data field to content field
192
+ if 'data' in data:
193
+ data['content'] = json.dumps(data.pop('data')) if not isinstance(data.get('data'), str) else data.pop('data')
194
+ # Ensure type has default
195
+ if 'type' not in data or data['type'] is None:
196
+ data['type'] = 'journal'
197
+ operations.append({
198
+ "operation": "create",
199
+ "data": data
200
+ })
201
+
202
+ 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.7
3
+ Version: 1.1.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
@@ -470,6 +473,13 @@ The API implements rate limiting. The SDK automatically handles rate limit respo
470
473
 
471
474
  See [CHANGELOG.md](./CHANGELOG.md) for a detailed history of changes.
472
475
 
476
+ ## License & Use
477
+
478
+ - This SDK is MIT-licensed for integration convenience.
479
+ - The Sovant API and platform are proprietary to Sovant Technologies Sdn. Bhd.
480
+ - You may use this SDK to integrate with Sovant's hosted API.
481
+ - Hosting/redistributing the Sovant backend or any proprietary components is not permitted.
482
+
473
483
  ## License
474
484
 
475
485
  MIT - See [LICENSE](LICENSE) file for details.
@@ -1,14 +1,14 @@
1
- sovant/__init__.py,sha256=hmpXJBx3oFfXlSlmJrTh79Dy6LwvpI11jdaix-fyt88,122
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=B1SlHvUKKRSf1NYOOEkfukBmxhpsop4lHY2X_-EBFek,3372
3
+ sovant/client.py,sha256=0aM5q6L4fvni7rvwiQVX0sYhVOnqo4rUmoJB_yEKMeQ,7557
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.7.dist-info/licenses/LICENSE,sha256=rnNP6-elrMIlQ9jf2aqnHZMyyQ_wvF3y1hTpVUusCWU,1062
11
- sovant-1.0.7.dist-info/METADATA,sha256=PLokxYhdgIjz8eEFRx3epAIYwIngF-EFUf-Lu6OXzXk,11407
12
- sovant-1.0.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- sovant-1.0.7.dist-info/top_level.txt,sha256=za6eVEsYd_ZQQs8vrmEWNcAR58r1wCDge_jA60e4CvQ,7
14
- sovant-1.0.7.dist-info/RECORD,,
10
+ sovant-1.1.0.dist-info/licenses/LICENSE,sha256=rnNP6-elrMIlQ9jf2aqnHZMyyQ_wvF3y1hTpVUusCWU,1062
11
+ sovant-1.1.0.dist-info/METADATA,sha256=A1TrKV2ZZss0rHi9TbU7avu4PnItlRainXeWZAu8MdA,11892
12
+ sovant-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
+ sovant-1.1.0.dist-info/top_level.txt,sha256=za6eVEsYd_ZQQs8vrmEWNcAR58r1wCDge_jA60e4CvQ,7
14
+ sovant-1.1.0.dist-info/RECORD,,
File without changes