sovant 1.0.7__tar.gz → 1.2.0__tar.gz

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.
@@ -1,9 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sovant
3
- Version: 1.0.7
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 Memory-as-a-Service: a durable, queryable memory layer for AI apps with cross-model recall, hybrid (semantic + deterministic) retrieval, and simple SDKs.
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
  [![PyPI version](https://img.shields.io/pypi/v/sovant.svg)](https://pypi.org/project/sovant/)
19
23
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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,19 +1,7 @@
1
- Metadata-Version: 2.4
2
- Name: sovant
3
- Version: 1.0.7
4
- Summary: Sovant Memory-as-a-Service Python SDK
5
- Author: Sovant
6
- License: MIT
7
- Requires-Python: >=3.10
8
- Description-Content-Type: text/markdown
9
- License-File: LICENSE
10
- Requires-Dist: httpx>=0.27.0
11
- Requires-Dist: pydantic>=2.8.2
12
- Dynamic: license-file
13
-
14
1
  # Sovant Python SDK
15
2
 
16
- Sovant is Memory-as-a-Service: a durable, queryable memory layer for AI apps with cross-model recall, hybrid (semantic + deterministic) retrieval, and simple SDKs.
3
+ **Sovant is a governed AI memory layer for AI agents and applications.**
4
+ Use it to store, search, and recall memories with profile awareness and enterprise-grade control over how memory is captured and used.
17
5
 
18
6
  [![PyPI version](https://img.shields.io/pypi/v/sovant.svg)](https://pypi.org/project/sovant/)
19
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -54,6 +42,32 @@ updated = client.memory.update(mem["id"], {
54
42
  client.memory.delete(mem["id"])
55
43
  ```
56
44
 
45
+ ## Recall vs Search
46
+
47
+ Sovant provides two ways to query memories:
48
+
49
+ - **`memory_recall()`** – Hybrid recall, profile-aware
50
+ Use for conversational queries: "What do you know about me?", "What happened on Project X?".
51
+ Uses Sovant's hybrid pipeline (profile fast-path + thread-scoped lexical + vector semantic search) and prioritizes profile facts (name/age/location) when available.
52
+
53
+ - **`memory_search()`** – Semantic search
54
+ Use for topic lookup and discovery. Pure vector similarity search without profile logic.
55
+ Behavior unchanged from previous versions.
56
+
57
+ ```python
58
+ # Recall for conversational queries
59
+ context = client.memory_recall(
60
+ query="what do you know about me?",
61
+ limit=10
62
+ )
63
+
64
+ # Search for topic discovery
65
+ topics = client.memory_search({
66
+ "query": "project updates",
67
+ "limit": 5
68
+ })
69
+ ```
70
+
57
71
  ## Chat in 60 Seconds
58
72
 
59
73
  Stream real-time chat responses with memory context:
@@ -470,6 +484,13 @@ The API implements rate limiting. The SDK automatically handles rate limit respo
470
484
 
471
485
  See [CHANGELOG.md](./CHANGELOG.md) for a detailed history of changes.
472
486
 
487
+ ## License & Use
488
+
489
+ - This SDK is MIT-licensed for integration convenience.
490
+ - The Sovant API and platform are proprietary to Sovant Technologies Sdn. Bhd.
491
+ - You may use this SDK to integrate with Sovant's hosted API.
492
+ - Hosting/redistributing the Sovant backend or any proprietary components is not permitted.
493
+
473
494
  ## License
474
495
 
475
- MIT - See [LICENSE](LICENSE) file for details.
496
+ MIT - See [LICENSE](LICENSE) file for details.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sovant"
3
- version = "1.0.7"
3
+ version = "1.2.0"
4
4
  description = "Sovant Memory-as-a-Service Python SDK"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -8,6 +8,11 @@ license = {text = "MIT"}
8
8
  authors = [{name = "Sovant"}]
9
9
  dependencies = ["httpx>=0.27.0","pydantic>=2.8.2"]
10
10
 
11
+ [project.urls]
12
+ "Documentation" = "https://sovant.ai/docs"
13
+ "Source" = "https://github.com/hechin91/sovant-ai"
14
+ "Tracker" = "https://github.com/hechin91/sovant-ai/issues"
15
+
11
16
  [build-system]
12
17
  requires = ["setuptools>=68","wheel","build"]
13
18
  build-backend = "setuptools.build_meta"
@@ -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"
@@ -0,0 +1,220 @@
1
+ import os
2
+ import json
3
+ import time
4
+ import httpx
5
+ from typing import Any, Dict, Optional, Callable
6
+ from .models import MemoryCreate, SearchQuery
7
+
8
+ class SovantError(Exception):
9
+ def __init__(self, message: str, code: str, status: int | None = None, details: Any | None = None):
10
+ super().__init__(message)
11
+ self.code = code
12
+ self.status = status
13
+ self.details = details
14
+
15
+ class Sovant:
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
+ ):
27
+ self.api_key = api_key or os.getenv("SOVANT_API_KEY")
28
+ if not self.api_key:
29
+ raise ValueError("Missing api_key")
30
+ self.base_url = (base_url or os.getenv("SOVANT_BASE_URL") or "https://sovant.ai").rstrip("/")
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
37
+ self._client = httpx.Client(
38
+ timeout=self.timeout,
39
+ headers={
40
+ "authorization": f"Bearer {self.api_key}",
41
+ "content-type": "application/json"
42
+ }
43
+ )
44
+
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):
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)
131
+
132
+ def memory_create(self, create: MemoryCreate):
133
+ # Convert data field to content field for API
134
+ body = create.model_dump()
135
+ if 'data' in body:
136
+ body['content'] = json.dumps(body.pop('data')) if not isinstance(body.get('data'), str) else body.pop('data')
137
+
138
+ # Ensure type has a default
139
+ if 'type' not in body or body['type'] is None:
140
+ body['type'] = 'journal'
141
+
142
+ return self._request("POST", f"{self.base_url}/api/v1/memory", json=body)
143
+
144
+ def memory_get(self, id: str):
145
+ return self._request("GET", f"{self.base_url}/api/v1/memories/{id}")
146
+
147
+ def memory_search(self, q: SearchQuery):
148
+ params = {}
149
+ if q.query:
150
+ params['query'] = q.query
151
+ if q.type:
152
+ params['type'] = q.type
153
+ if q.tags:
154
+ params['tags'] = ','.join(q.tags)
155
+ if q.thread_id:
156
+ params['thread_id'] = q.thread_id
157
+ if q.limit:
158
+ params['limit'] = str(q.limit)
159
+ if q.from_date:
160
+ params['from_date'] = q.from_date
161
+ if q.to_date:
162
+ params['to_date'] = q.to_date
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)
186
+
187
+ def memory_update(self, id: str, patch: Dict[str, Any]):
188
+ # Convert data field to content field if present
189
+ if 'data' in patch:
190
+ patch['content'] = json.dumps(patch.pop('data')) if not isinstance(patch.get('data'), str) else patch.pop('data')
191
+ return self._request("PATCH", f"{self.base_url}/api/v1/memories/{id}", json=patch)
192
+
193
+ def memory_delete(self, id: str):
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,6 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: sovant
3
+ Version: 1.2.0
4
+ Summary: Sovant Memory-as-a-Service Python SDK
5
+ Author: Sovant
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
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: httpx>=0.27.0
14
+ Requires-Dist: pydantic>=2.8.2
15
+ Dynamic: license-file
16
+
1
17
  # Sovant Python SDK
2
18
 
3
- Sovant is Memory-as-a-Service: a durable, queryable memory layer for AI apps with cross-model recall, hybrid (semantic + deterministic) retrieval, and simple SDKs.
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.
4
21
 
5
22
  [![PyPI version](https://img.shields.io/pypi/v/sovant.svg)](https://pypi.org/project/sovant/)
6
23
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
@@ -41,6 +58,32 @@ updated = client.memory.update(mem["id"], {
41
58
  client.memory.delete(mem["id"])
42
59
  ```
43
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
+
44
87
  ## Chat in 60 Seconds
45
88
 
46
89
  Stream real-time chat responses with memory context:
@@ -457,6 +500,13 @@ The API implements rate limiting. The SDK automatically handles rate limit respo
457
500
 
458
501
  See [CHANGELOG.md](./CHANGELOG.md) for a detailed history of changes.
459
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
+
460
510
  ## License
461
511
 
462
- MIT - See [LICENSE](LICENSE) file for details.
512
+ MIT - See [LICENSE](LICENSE) file for details.
@@ -1,90 +0,0 @@
1
- import os
2
- import json
3
- import httpx
4
- from typing import Any, Dict
5
- from .models import MemoryCreate, SearchQuery
6
-
7
- class SovantError(Exception):
8
- def __init__(self, message: str, code: str, status: int | None = None, details: Any | None = None):
9
- super().__init__(message)
10
- self.code = code
11
- self.status = status
12
- self.details = details
13
-
14
- class Sovant:
15
- def __init__(self, api_key: str | None = None, base_url: str | None = None, timeout: float = 30.0):
16
- self.api_key = api_key or os.getenv("SOVANT_API_KEY")
17
- if not self.api_key:
18
- raise ValueError("Missing api_key")
19
- self.base_url = (base_url or os.getenv("SOVANT_BASE_URL") or "https://sovant.ai").rstrip("/")
20
- self.timeout = timeout
21
- self._client = httpx.Client(
22
- timeout=self.timeout,
23
- headers={
24
- "authorization": f"Bearer {self.api_key}",
25
- "content-type": "application/json"
26
- }
27
- )
28
-
29
- def _handle(self, r: httpx.Response):
30
- if r.status_code >= 400:
31
- 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
44
-
45
- def memory_create(self, create: MemoryCreate):
46
- # Convert data field to content field for API
47
- body = create.model_dump()
48
- if 'data' in body:
49
- body['content'] = json.dumps(body.pop('data')) if not isinstance(body.get('data'), str) else body.pop('data')
50
-
51
- # Ensure type has a default
52
- if 'type' not in body or body['type'] is None:
53
- 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)
57
-
58
- def memory_get(self, id: str):
59
- r = self._client.get(f"{self.base_url}/api/v1/memories/{id}")
60
- return self._handle(r)
61
-
62
- def memory_search(self, q: SearchQuery):
63
- params = {}
64
- if q.query:
65
- params['query'] = q.query
66
- if q.type:
67
- params['type'] = q.type
68
- if q.tags:
69
- params['tags'] = ','.join(q.tags)
70
- if q.thread_id:
71
- params['thread_id'] = q.thread_id
72
- if q.limit:
73
- params['limit'] = str(q.limit)
74
- if q.from_date:
75
- params['from_date'] = q.from_date
76
- if q.to_date:
77
- 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)
80
-
81
- def memory_update(self, id: str, patch: Dict[str, Any]):
82
- # Convert data field to content field if present
83
- if 'data' in patch:
84
- 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)
87
-
88
- def memory_delete(self, id: str):
89
- r = self._client.delete(f"{self.base_url}/api/v1/memories/{id}")
90
- return self._handle(r)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes