contex-python 0.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.
- contex/__init__.py +49 -0
- contex/client.py +412 -0
- contex/exceptions.py +44 -0
- contex/models.py +70 -0
- contex_python-0.1.0.dist-info/METADATA +277 -0
- contex_python-0.1.0.dist-info/RECORD +9 -0
- contex_python-0.1.0.dist-info/WHEEL +5 -0
- contex_python-0.1.0.dist-info/licenses/LICENSE +21 -0
- contex_python-0.1.0.dist-info/top_level.txt +1 -0
contex/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Contex Python SDK
|
|
3
|
+
~~~~~~~~~~~~~~~~~
|
|
4
|
+
|
|
5
|
+
Official Python client for Contex - Semantic context routing for AI agents.
|
|
6
|
+
|
|
7
|
+
Basic usage:
|
|
8
|
+
|
|
9
|
+
>>> from contex import ContexClient
|
|
10
|
+
>>> client = ContexClient(url="http://localhost:8001", api_key="ck_...")
|
|
11
|
+
>>> await client.publish(project_id="my-app", data_key="config", data={"env": "prod"})
|
|
12
|
+
|
|
13
|
+
Full documentation: https://github.com/cahoots-org/contex
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "0.2.0"
|
|
17
|
+
__author__ = "Contex Team"
|
|
18
|
+
__license__ = "MIT"
|
|
19
|
+
|
|
20
|
+
from .client import ContexClient, ContexAsyncClient
|
|
21
|
+
from .models import (
|
|
22
|
+
DataEvent,
|
|
23
|
+
AgentRegistration,
|
|
24
|
+
RegistrationResponse,
|
|
25
|
+
MatchedData,
|
|
26
|
+
)
|
|
27
|
+
from .exceptions import (
|
|
28
|
+
ContexError,
|
|
29
|
+
AuthenticationError,
|
|
30
|
+
RateLimitError,
|
|
31
|
+
ValidationError,
|
|
32
|
+
NotFoundError,
|
|
33
|
+
ServerError,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"ContexClient",
|
|
38
|
+
"ContexAsyncClient",
|
|
39
|
+
"DataEvent",
|
|
40
|
+
"AgentRegistration",
|
|
41
|
+
"RegistrationResponse",
|
|
42
|
+
"MatchedData",
|
|
43
|
+
"ContexError",
|
|
44
|
+
"AuthenticationError",
|
|
45
|
+
"RateLimitError",
|
|
46
|
+
"ValidationError",
|
|
47
|
+
"NotFoundError",
|
|
48
|
+
"ServerError",
|
|
49
|
+
]
|
contex/client.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Contex Python SDK client implementation"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any, Dict, List, Optional, AsyncIterator
|
|
5
|
+
import httpx
|
|
6
|
+
from .models import (
|
|
7
|
+
DataEvent,
|
|
8
|
+
AgentRegistration,
|
|
9
|
+
RegistrationResponse,
|
|
10
|
+
QueryRequest,
|
|
11
|
+
QueryResponse,
|
|
12
|
+
APIKeyResponse,
|
|
13
|
+
RateLimitInfo,
|
|
14
|
+
)
|
|
15
|
+
from .exceptions import (
|
|
16
|
+
AuthenticationError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
ValidationError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
ServerError,
|
|
21
|
+
NetworkError,
|
|
22
|
+
TimeoutError as ContexTimeoutError,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContexAsyncClient:
|
|
27
|
+
"""
|
|
28
|
+
Async Contex client for Python.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> client = ContexAsyncClient(url="http://localhost:8001", api_key="ck_...")
|
|
32
|
+
>>> await client.publish(project_id="my-app", data_key="config", data={"env": "prod"})
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
url: str = "http://localhost:8001",
|
|
38
|
+
api_key: Optional[str] = None,
|
|
39
|
+
timeout: float = 30.0,
|
|
40
|
+
max_retries: int = 3,
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
Initialize Contex client.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
url: Contex server URL
|
|
47
|
+
api_key: API key for authentication
|
|
48
|
+
timeout: Request timeout in seconds
|
|
49
|
+
max_retries: Maximum number of retries for failed requests
|
|
50
|
+
"""
|
|
51
|
+
self.url = url.rstrip("/")
|
|
52
|
+
self.api_key = api_key
|
|
53
|
+
self.timeout = timeout
|
|
54
|
+
self.max_retries = max_retries
|
|
55
|
+
self._client: Optional[httpx.AsyncClient] = None
|
|
56
|
+
|
|
57
|
+
async def __aenter__(self):
|
|
58
|
+
"""Async context manager entry"""
|
|
59
|
+
self._client = httpx.AsyncClient(timeout=self.timeout)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
63
|
+
"""Async context manager exit"""
|
|
64
|
+
if self._client:
|
|
65
|
+
await self._client.aclose()
|
|
66
|
+
|
|
67
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
68
|
+
"""Get request headers"""
|
|
69
|
+
headers = {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"User-Agent": "contex-python/0.2.0",
|
|
72
|
+
}
|
|
73
|
+
if self.api_key:
|
|
74
|
+
headers["X-API-Key"] = self.api_key
|
|
75
|
+
return headers
|
|
76
|
+
|
|
77
|
+
async def _request(
|
|
78
|
+
self,
|
|
79
|
+
method: str,
|
|
80
|
+
path: str,
|
|
81
|
+
json: Optional[Dict] = None,
|
|
82
|
+
params: Optional[Dict] = None,
|
|
83
|
+
) -> Any:
|
|
84
|
+
"""Make HTTP request with error handling and retries"""
|
|
85
|
+
url = f"{self.url}{path}"
|
|
86
|
+
headers = self._get_headers()
|
|
87
|
+
|
|
88
|
+
# Create client if not in context manager
|
|
89
|
+
if self._client is None:
|
|
90
|
+
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
|
91
|
+
return await self._do_request(client, method, url, headers, json, params)
|
|
92
|
+
else:
|
|
93
|
+
return await self._do_request(self._client, method, url, headers, json, params)
|
|
94
|
+
|
|
95
|
+
async def _do_request(
|
|
96
|
+
self,
|
|
97
|
+
client: httpx.AsyncClient,
|
|
98
|
+
method: str,
|
|
99
|
+
url: str,
|
|
100
|
+
headers: Dict[str, str],
|
|
101
|
+
json: Optional[Dict] = None,
|
|
102
|
+
params: Optional[Dict] = None,
|
|
103
|
+
) -> Any:
|
|
104
|
+
"""Execute HTTP request with retries"""
|
|
105
|
+
last_exception = None
|
|
106
|
+
|
|
107
|
+
for attempt in range(self.max_retries):
|
|
108
|
+
try:
|
|
109
|
+
response = await client.request(
|
|
110
|
+
method=method,
|
|
111
|
+
url=url,
|
|
112
|
+
headers=headers,
|
|
113
|
+
json=json,
|
|
114
|
+
params=params,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Handle response
|
|
118
|
+
if response.status_code == 200 or response.status_code == 201:
|
|
119
|
+
return response.json()
|
|
120
|
+
|
|
121
|
+
elif response.status_code == 401:
|
|
122
|
+
raise AuthenticationError("Invalid or missing API key")
|
|
123
|
+
|
|
124
|
+
elif response.status_code == 403:
|
|
125
|
+
raise AuthenticationError("Insufficient permissions")
|
|
126
|
+
|
|
127
|
+
elif response.status_code == 404:
|
|
128
|
+
raise NotFoundError(f"Resource not found: {url}")
|
|
129
|
+
|
|
130
|
+
elif response.status_code == 422:
|
|
131
|
+
error_detail = response.json().get("detail", "Validation error")
|
|
132
|
+
raise ValidationError(f"Validation error: {error_detail}")
|
|
133
|
+
|
|
134
|
+
elif response.status_code == 429:
|
|
135
|
+
retry_after = response.headers.get("Retry-After")
|
|
136
|
+
raise RateLimitError(
|
|
137
|
+
"Rate limit exceeded",
|
|
138
|
+
retry_after=int(retry_after) if retry_after else None
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
elif response.status_code >= 500:
|
|
142
|
+
error_msg = response.json().get("detail", "Server error")
|
|
143
|
+
raise ServerError(f"Server error: {error_msg}")
|
|
144
|
+
|
|
145
|
+
else:
|
|
146
|
+
raise ServerError(f"Unexpected status code: {response.status_code}")
|
|
147
|
+
|
|
148
|
+
except httpx.TimeoutException as e:
|
|
149
|
+
last_exception = ContexTimeoutError(f"Request timed out: {e}")
|
|
150
|
+
if attempt < self.max_retries - 1:
|
|
151
|
+
await asyncio.sleep(2 ** attempt) # Exponential backoff
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
except httpx.RequestError as e:
|
|
155
|
+
last_exception = NetworkError(f"Network error: {e}")
|
|
156
|
+
if attempt < self.max_retries - 1:
|
|
157
|
+
await asyncio.sleep(2 ** attempt)
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
# All retries failed
|
|
161
|
+
if last_exception:
|
|
162
|
+
raise last_exception
|
|
163
|
+
|
|
164
|
+
# ========================================================================
|
|
165
|
+
# Data Publishing
|
|
166
|
+
# ========================================================================
|
|
167
|
+
|
|
168
|
+
async def publish(
|
|
169
|
+
self,
|
|
170
|
+
project_id: str,
|
|
171
|
+
data_key: str,
|
|
172
|
+
data: Any,
|
|
173
|
+
data_format: str = "json",
|
|
174
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
175
|
+
) -> Dict[str, Any]:
|
|
176
|
+
"""
|
|
177
|
+
Publish data to Contex.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
project_id: Project identifier
|
|
181
|
+
data_key: Unique key for this data
|
|
182
|
+
data: Data payload (any JSON-serializable type)
|
|
183
|
+
data_format: Data format (json, yaml, toml, text)
|
|
184
|
+
metadata: Optional metadata
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Response with status and sequence number
|
|
188
|
+
|
|
189
|
+
Example:
|
|
190
|
+
>>> await client.publish(
|
|
191
|
+
... project_id="my-app",
|
|
192
|
+
... data_key="config",
|
|
193
|
+
... data={"env": "prod", "debug": False}
|
|
194
|
+
... )
|
|
195
|
+
"""
|
|
196
|
+
event = DataEvent(
|
|
197
|
+
project_id=project_id,
|
|
198
|
+
data_key=data_key,
|
|
199
|
+
data=data,
|
|
200
|
+
data_format=data_format,
|
|
201
|
+
metadata=metadata,
|
|
202
|
+
)
|
|
203
|
+
return await self._request("POST", "/api/data/publish", json=event.model_dump())
|
|
204
|
+
|
|
205
|
+
# ========================================================================
|
|
206
|
+
# Agent Management
|
|
207
|
+
# ========================================================================
|
|
208
|
+
|
|
209
|
+
async def register_agent(
|
|
210
|
+
self,
|
|
211
|
+
agent_id: str,
|
|
212
|
+
project_id: str,
|
|
213
|
+
data_needs: List[str],
|
|
214
|
+
notification_method: str = "redis",
|
|
215
|
+
webhook_url: Optional[str] = None,
|
|
216
|
+
webhook_secret: Optional[str] = None,
|
|
217
|
+
last_seen_sequence: str = "0",
|
|
218
|
+
) -> RegistrationResponse:
|
|
219
|
+
"""
|
|
220
|
+
Register an agent with Contex.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
agent_id: Unique agent identifier
|
|
224
|
+
project_id: Project identifier
|
|
225
|
+
data_needs: List of data needs in natural language
|
|
226
|
+
notification_method: Notification method (redis or webhook)
|
|
227
|
+
webhook_url: Webhook URL (if using webhook notifications)
|
|
228
|
+
webhook_secret: Webhook secret for HMAC verification
|
|
229
|
+
last_seen_sequence: Last seen sequence number
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Registration response with matched data
|
|
233
|
+
|
|
234
|
+
Example:
|
|
235
|
+
>>> response = await client.register_agent(
|
|
236
|
+
... agent_id="code-reviewer",
|
|
237
|
+
... project_id="my-app",
|
|
238
|
+
... data_needs=["coding standards", "test requirements"]
|
|
239
|
+
... )
|
|
240
|
+
>>> print(f"Matched {len(response.matched_data)} items")
|
|
241
|
+
"""
|
|
242
|
+
registration = AgentRegistration(
|
|
243
|
+
agent_id=agent_id,
|
|
244
|
+
project_id=project_id,
|
|
245
|
+
data_needs=data_needs,
|
|
246
|
+
notification_method=notification_method,
|
|
247
|
+
webhook_url=webhook_url,
|
|
248
|
+
webhook_secret=webhook_secret,
|
|
249
|
+
last_seen_sequence=last_seen_sequence,
|
|
250
|
+
)
|
|
251
|
+
result = await self._request("POST", "/api/agents/register", json=registration.model_dump())
|
|
252
|
+
return RegistrationResponse(**result)
|
|
253
|
+
|
|
254
|
+
async def unregister_agent(self, agent_id: str) -> Dict[str, Any]:
|
|
255
|
+
"""
|
|
256
|
+
Unregister an agent.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
agent_id: Agent identifier
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Response confirming unregistration
|
|
263
|
+
"""
|
|
264
|
+
return await self._request("DELETE", f"/api/agents/{agent_id}")
|
|
265
|
+
|
|
266
|
+
async def get_agent_status(self, agent_id: str) -> Dict[str, Any]:
|
|
267
|
+
"""
|
|
268
|
+
Get agent status.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
agent_id: Agent identifier
|
|
272
|
+
|
|
273
|
+
Returns:
|
|
274
|
+
Agent status information
|
|
275
|
+
"""
|
|
276
|
+
return await self._request("GET", f"/api/agents/{agent_id}/status")
|
|
277
|
+
|
|
278
|
+
# ========================================================================
|
|
279
|
+
# Querying
|
|
280
|
+
# ========================================================================
|
|
281
|
+
|
|
282
|
+
async def query(
|
|
283
|
+
self,
|
|
284
|
+
project_id: str,
|
|
285
|
+
query: str,
|
|
286
|
+
max_results: int = 10,
|
|
287
|
+
) -> QueryResponse:
|
|
288
|
+
"""
|
|
289
|
+
Query for relevant data.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
project_id: Project identifier
|
|
293
|
+
query: Query string in natural language
|
|
294
|
+
max_results: Maximum number of results
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Query response with matched data
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
>>> response = await client.query(
|
|
301
|
+
... project_id="my-app",
|
|
302
|
+
... query="authentication configuration"
|
|
303
|
+
... )
|
|
304
|
+
>>> for result in response.results:
|
|
305
|
+
... print(f"{result.data_key}: {result.similarity_score}")
|
|
306
|
+
"""
|
|
307
|
+
request = QueryRequest(
|
|
308
|
+
project_id=project_id,
|
|
309
|
+
query=query,
|
|
310
|
+
max_results=max_results,
|
|
311
|
+
)
|
|
312
|
+
result = await self._request("POST", "/api/query", json=request.model_dump())
|
|
313
|
+
return QueryResponse(**result)
|
|
314
|
+
|
|
315
|
+
# ========================================================================
|
|
316
|
+
# API Key Management
|
|
317
|
+
# ========================================================================
|
|
318
|
+
|
|
319
|
+
async def create_api_key(self, name: str) -> APIKeyResponse:
|
|
320
|
+
"""
|
|
321
|
+
Create a new API key.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
name: Name for the API key
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
API key response with key details
|
|
328
|
+
|
|
329
|
+
Note:
|
|
330
|
+
The API key is only returned once. Store it securely!
|
|
331
|
+
"""
|
|
332
|
+
result = await self._request("POST", f"/api/auth/keys?name={name}")
|
|
333
|
+
return APIKeyResponse(**result)
|
|
334
|
+
|
|
335
|
+
async def list_api_keys(self) -> List[Dict[str, Any]]:
|
|
336
|
+
"""List all API keys (without the actual key values)"""
|
|
337
|
+
return await self._request("GET", "/api/auth/keys")
|
|
338
|
+
|
|
339
|
+
async def revoke_api_key(self, key_id: str) -> Dict[str, Any]:
|
|
340
|
+
"""
|
|
341
|
+
Revoke an API key.
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
key_id: Key identifier
|
|
345
|
+
"""
|
|
346
|
+
return await self._request("DELETE", f"/api/auth/keys/{key_id}")
|
|
347
|
+
|
|
348
|
+
# ========================================================================
|
|
349
|
+
# Health & Status
|
|
350
|
+
# ========================================================================
|
|
351
|
+
|
|
352
|
+
async def health(self) -> Dict[str, Any]:
|
|
353
|
+
"""Get comprehensive health status"""
|
|
354
|
+
return await self._request("GET", "/api/health")
|
|
355
|
+
|
|
356
|
+
async def ready(self) -> Dict[str, Any]:
|
|
357
|
+
"""Get readiness status"""
|
|
358
|
+
return await self._request("GET", "/api/health/ready")
|
|
359
|
+
|
|
360
|
+
async def rate_limit_status(self) -> RateLimitInfo:
|
|
361
|
+
"""Get current rate limit status"""
|
|
362
|
+
result = await self._request("GET", "/api/rate-limit/status")
|
|
363
|
+
return RateLimitInfo(**result)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class ContexClient(ContexAsyncClient):
|
|
367
|
+
"""
|
|
368
|
+
Synchronous Contex client (wrapper around async client).
|
|
369
|
+
|
|
370
|
+
Example:
|
|
371
|
+
>>> client = ContexClient(url="http://localhost:8001", api_key="ck_...")
|
|
372
|
+
>>> client.publish(project_id="my-app", data_key="config", data={"env": "prod"})
|
|
373
|
+
"""
|
|
374
|
+
|
|
375
|
+
def __init__(self, *args, **kwargs):
|
|
376
|
+
super().__init__(*args, **kwargs)
|
|
377
|
+
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
|
378
|
+
|
|
379
|
+
def _run_async(self, coro):
|
|
380
|
+
"""Run async coroutine in sync context"""
|
|
381
|
+
if self._loop is None:
|
|
382
|
+
self._loop = asyncio.new_event_loop()
|
|
383
|
+
return self._loop.run_until_complete(coro)
|
|
384
|
+
|
|
385
|
+
def publish(self, *args, **kwargs):
|
|
386
|
+
"""Synchronous publish"""
|
|
387
|
+
return self._run_async(super().publish(*args, **kwargs))
|
|
388
|
+
|
|
389
|
+
def register_agent(self, *args, **kwargs):
|
|
390
|
+
"""Synchronous register_agent"""
|
|
391
|
+
return self._run_async(super().register_agent(*args, **kwargs))
|
|
392
|
+
|
|
393
|
+
def unregister_agent(self, *args, **kwargs):
|
|
394
|
+
"""Synchronous unregister_agent"""
|
|
395
|
+
return self._run_async(super().unregister_agent(*args, **kwargs))
|
|
396
|
+
|
|
397
|
+
def query(self, *args, **kwargs):
|
|
398
|
+
"""Synchronous query"""
|
|
399
|
+
return self._run_async(super().query(*args, **kwargs))
|
|
400
|
+
|
|
401
|
+
def health(self, *args, **kwargs):
|
|
402
|
+
"""Synchronous health"""
|
|
403
|
+
return self._run_async(super().health(*args, **kwargs))
|
|
404
|
+
|
|
405
|
+
def create_api_key(self, *args, **kwargs):
|
|
406
|
+
"""Synchronous create_api_key"""
|
|
407
|
+
return self._run_async(super().create_api_key(*args, **kwargs))
|
|
408
|
+
|
|
409
|
+
def __del__(self):
|
|
410
|
+
"""Cleanup event loop"""
|
|
411
|
+
if self._loop:
|
|
412
|
+
self._loop.close()
|
contex/exceptions.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Contex SDK exceptions"""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ContexError(Exception):
|
|
5
|
+
"""Base exception for all Contex SDK errors"""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthenticationError(ContexError):
|
|
10
|
+
"""Raised when authentication fails"""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RateLimitError(ContexError):
|
|
15
|
+
"""Raised when rate limit is exceeded"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, message: str, retry_after: int = None):
|
|
18
|
+
super().__init__(message)
|
|
19
|
+
self.retry_after = retry_after
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ValidationError(ContexError):
|
|
23
|
+
"""Raised when request validation fails"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NotFoundError(ContexError):
|
|
28
|
+
"""Raised when resource is not found"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ServerError(ContexError):
|
|
33
|
+
"""Raised when server returns 5xx error"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class NetworkError(ContexError):
|
|
38
|
+
"""Raised when network request fails"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TimeoutError(ContexError):
|
|
43
|
+
"""Raised when request times out"""
|
|
44
|
+
pass
|
contex/models.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Contex SDK data models"""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class DataEvent(BaseModel):
|
|
8
|
+
"""Data event to publish"""
|
|
9
|
+
project_id: str = Field(..., description="Project identifier")
|
|
10
|
+
data_key: str = Field(..., description="Unique key for this data")
|
|
11
|
+
data: Any = Field(..., description="Data payload (any JSON-serializable type)")
|
|
12
|
+
data_format: Optional[str] = Field(default="json", description="Data format: json, yaml, toml, text")
|
|
13
|
+
metadata: Optional[Dict[str, Any]] = Field(default=None, description="Optional metadata")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AgentRegistration(BaseModel):
|
|
17
|
+
"""Agent registration request"""
|
|
18
|
+
agent_id: str = Field(..., description="Unique agent identifier")
|
|
19
|
+
project_id: str = Field(..., description="Project identifier")
|
|
20
|
+
data_needs: List[str] = Field(..., description="List of data needs in natural language")
|
|
21
|
+
notification_method: Optional[str] = Field(default="redis", description="Notification method: redis or webhook")
|
|
22
|
+
webhook_url: Optional[str] = Field(default=None, description="Webhook URL (if notification_method=webhook)")
|
|
23
|
+
webhook_secret: Optional[str] = Field(default=None, description="Webhook secret for HMAC verification")
|
|
24
|
+
last_seen_sequence: Optional[str] = Field(default="0", description="Last seen sequence number")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MatchedData(BaseModel):
|
|
28
|
+
"""Matched data returned to agent"""
|
|
29
|
+
data_key: str
|
|
30
|
+
data: Any
|
|
31
|
+
similarity_score: float
|
|
32
|
+
sequence: str
|
|
33
|
+
timestamp: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RegistrationResponse(BaseModel):
|
|
37
|
+
"""Agent registration response"""
|
|
38
|
+
agent_id: str
|
|
39
|
+
project_id: str
|
|
40
|
+
notification_channel: Optional[str] = None
|
|
41
|
+
matched_data: List[MatchedData]
|
|
42
|
+
last_seen_sequence: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class QueryRequest(BaseModel):
|
|
46
|
+
"""Query request"""
|
|
47
|
+
project_id: str
|
|
48
|
+
query: str
|
|
49
|
+
max_results: Optional[int] = Field(default=10, ge=1, le=100)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class QueryResponse(BaseModel):
|
|
53
|
+
"""Query response"""
|
|
54
|
+
results: List[MatchedData]
|
|
55
|
+
total: int
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class APIKeyResponse(BaseModel):
|
|
59
|
+
"""API key creation response"""
|
|
60
|
+
key_id: str
|
|
61
|
+
key: str
|
|
62
|
+
name: str
|
|
63
|
+
created_at: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RateLimitInfo(BaseModel):
|
|
67
|
+
"""Rate limit information"""
|
|
68
|
+
limit: int
|
|
69
|
+
remaining: int
|
|
70
|
+
reset_at: str
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: contex-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for Contex - Semantic context routing for AI agents
|
|
5
|
+
Author-email: Cahoots <admin@cahoots.cc>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cahoots-org/contex
|
|
8
|
+
Project-URL: Repository, https://github.com/cahoots-org/contex
|
|
9
|
+
Project-URL: Issues, https://github.com/cahoots-org/contex/issues
|
|
10
|
+
Keywords: ai,agents,context,semantic-search,machine-learning
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: httpx>=0.25.0
|
|
23
|
+
Requires-Dist: pydantic>=2.0.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.4.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
|
28
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.5.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# Contex Python SDK
|
|
34
|
+
|
|
35
|
+
Official Python client for [Contex](https://github.com/cahoots-org/contex) - Semantic context routing for AI agents.
|
|
36
|
+
|
|
37
|
+
## Installation
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install contex-python
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
### Async Client (Recommended)
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from contex import ContexAsyncClient
|
|
49
|
+
|
|
50
|
+
async def main():
|
|
51
|
+
async with ContexAsyncClient(
|
|
52
|
+
url="http://localhost:8001",
|
|
53
|
+
api_key="ck_your_api_key_here"
|
|
54
|
+
) as client:
|
|
55
|
+
# Publish data
|
|
56
|
+
await client.publish(
|
|
57
|
+
project_id="my-app",
|
|
58
|
+
data_key="coding_standards",
|
|
59
|
+
data={
|
|
60
|
+
"style": "PEP 8",
|
|
61
|
+
"max_line_length": 100,
|
|
62
|
+
"quotes": "double"
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Register agent
|
|
67
|
+
response = await client.register_agent(
|
|
68
|
+
agent_id="code-reviewer",
|
|
69
|
+
project_id="my-app",
|
|
70
|
+
data_needs=[
|
|
71
|
+
"coding standards and style guidelines",
|
|
72
|
+
"testing requirements and coverage goals"
|
|
73
|
+
]
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
print(f"Matched {len(response.matched_data)} items")
|
|
77
|
+
for match in response.matched_data:
|
|
78
|
+
print(f" {match.data_key}: {match.similarity_score:.2f}")
|
|
79
|
+
|
|
80
|
+
# Query for data
|
|
81
|
+
results = await client.query(
|
|
82
|
+
project_id="my-app",
|
|
83
|
+
query="authentication configuration"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
for result in results.results:
|
|
87
|
+
print(f"{result.data_key}: {result.data}")
|
|
88
|
+
|
|
89
|
+
import asyncio
|
|
90
|
+
asyncio.run(main())
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Sync Client
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from contex import ContexClient
|
|
97
|
+
|
|
98
|
+
client = ContexClient(
|
|
99
|
+
url="http://localhost:8001",
|
|
100
|
+
api_key="ck_your_api_key_here"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Publish data
|
|
104
|
+
client.publish(
|
|
105
|
+
project_id="my-app",
|
|
106
|
+
data_key="config",
|
|
107
|
+
data={"env": "prod", "debug": False}
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Register agent
|
|
111
|
+
response = client.register_agent(
|
|
112
|
+
agent_id="my-agent",
|
|
113
|
+
project_id="my-app",
|
|
114
|
+
data_needs=["configuration", "secrets"]
|
|
115
|
+
)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Features
|
|
119
|
+
|
|
120
|
+
- ✅ **Async & Sync**: Both async and synchronous interfaces
|
|
121
|
+
- ✅ **Type Hints**: Full type annotations with Pydantic models
|
|
122
|
+
- ✅ **Error Handling**: Comprehensive exception hierarchy
|
|
123
|
+
- ✅ **Retry Logic**: Automatic retries with exponential backoff
|
|
124
|
+
- ✅ **Rate Limiting**: Built-in rate limit handling
|
|
125
|
+
- ✅ **Authentication**: API key authentication support
|
|
126
|
+
|
|
127
|
+
## API Reference
|
|
128
|
+
|
|
129
|
+
### Client Initialization
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
client = ContexAsyncClient(
|
|
133
|
+
url="http://localhost:8001", # Contex server URL
|
|
134
|
+
api_key="ck_...", # API key for authentication
|
|
135
|
+
timeout=30.0, # Request timeout in seconds
|
|
136
|
+
max_retries=3, # Maximum number of retries
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Publishing Data
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
await client.publish(
|
|
144
|
+
project_id="my-app", # Project identifier
|
|
145
|
+
data_key="unique-key", # Unique key for this data
|
|
146
|
+
data={"any": "json"}, # Data payload
|
|
147
|
+
data_format="json", # Format: json, yaml, toml, text
|
|
148
|
+
metadata={"tags": ["prod"]}, # Optional metadata
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Registering Agents
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
response = await client.register_agent(
|
|
156
|
+
agent_id="agent-1", # Unique agent ID
|
|
157
|
+
project_id="my-app", # Project ID
|
|
158
|
+
data_needs=["config", "secrets"], # Data needs (natural language)
|
|
159
|
+
notification_method="redis", # redis or webhook
|
|
160
|
+
webhook_url="https://...", # Optional webhook URL
|
|
161
|
+
webhook_secret="secret", # Optional webhook secret
|
|
162
|
+
last_seen_sequence="0", # Last seen sequence
|
|
163
|
+
)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Querying Data
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
results = await client.query(
|
|
170
|
+
project_id="my-app",
|
|
171
|
+
query="authentication settings",
|
|
172
|
+
max_results=10,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
for result in results.results:
|
|
176
|
+
print(f"{result.data_key}: {result.similarity_score}")
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### API Key Management
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
# Create API key
|
|
183
|
+
key_response = await client.create_api_key(name="production-key")
|
|
184
|
+
print(f"API Key: {key_response.key}") # Store this securely!
|
|
185
|
+
|
|
186
|
+
# List keys
|
|
187
|
+
keys = await client.list_api_keys()
|
|
188
|
+
|
|
189
|
+
# Revoke key
|
|
190
|
+
await client.revoke_api_key(key_id="key-123")
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Health Checks
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
# Comprehensive health
|
|
197
|
+
health = await client.health()
|
|
198
|
+
|
|
199
|
+
# Readiness check
|
|
200
|
+
ready = await client.ready()
|
|
201
|
+
|
|
202
|
+
# Rate limit status
|
|
203
|
+
rate_limit = await client.rate_limit_status()
|
|
204
|
+
print(f"Remaining: {rate_limit.remaining}/{rate_limit.limit}")
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Exception Handling
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from contex import (
|
|
211
|
+
ContexError,
|
|
212
|
+
AuthenticationError,
|
|
213
|
+
RateLimitError,
|
|
214
|
+
ValidationError,
|
|
215
|
+
NotFoundError,
|
|
216
|
+
ServerError,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
try:
|
|
220
|
+
await client.publish(...)
|
|
221
|
+
except AuthenticationError:
|
|
222
|
+
print("Invalid API key")
|
|
223
|
+
except RateLimitError as e:
|
|
224
|
+
print(f"Rate limited. Retry after {e.retry_after} seconds")
|
|
225
|
+
except ValidationError as e:
|
|
226
|
+
print(f"Validation error: {e}")
|
|
227
|
+
except NotFoundError:
|
|
228
|
+
print("Resource not found")
|
|
229
|
+
except ServerError:
|
|
230
|
+
print("Server error")
|
|
231
|
+
except ContexError as e:
|
|
232
|
+
print(f"Contex error: {e}")
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Development
|
|
236
|
+
|
|
237
|
+
### Setup
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
cd sdk/python
|
|
241
|
+
pip install -e ".[dev]"
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Running Tests
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
pytest
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Code Formatting
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
black contex/
|
|
254
|
+
ruff check contex/
|
|
255
|
+
mypy contex/
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Examples
|
|
259
|
+
|
|
260
|
+
See the [examples](examples/) directory for more usage examples:
|
|
261
|
+
|
|
262
|
+
- `basic_usage.py` - Basic publish and query
|
|
263
|
+
- `agent_registration.py` - Agent registration and updates
|
|
264
|
+
- `webhook_agent.py` - Webhook-based agent
|
|
265
|
+
- `error_handling.py` - Error handling patterns
|
|
266
|
+
- `batch_operations.py` - Batch publishing
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
271
|
+
|
|
272
|
+
## Links
|
|
273
|
+
|
|
274
|
+
- [Documentation](https://contex.readthedocs.io)
|
|
275
|
+
- [GitHub](https://github.com/cahoots-org/contex)
|
|
276
|
+
- [PyPI](https://pypi.org/project/contex-python/)
|
|
277
|
+
- [Issues](https://github.com/cahoots-org/contex/issues)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
contex/__init__.py,sha256=bx7OgH5qs3fANEQ1NGTeRq6J2feBkEZ-lwKAzccZfTk,1058
|
|
2
|
+
contex/client.py,sha256=Mdw_5Ez22wFJTlKEptw-5dgSVfhwYjCmhnjmAq2EoNA,14106
|
|
3
|
+
contex/exceptions.py,sha256=pqzfumyLkMvRDELsE5TmI7d1P9NT-ZRJyMB7X-SWHHE,882
|
|
4
|
+
contex/models.py,sha256=TdLNEOvUqpAM_9L3mpWDR2mCCTVDlYNCOyT1519r2K4,2253
|
|
5
|
+
contex_python-0.1.0.dist-info/licenses/LICENSE,sha256=ocKsEnLoJ4FUPpfVkwLpK__UOQ6UrKCOGVWopyCOM7g,1068
|
|
6
|
+
contex_python-0.1.0.dist-info/METADATA,sha256=dGaJ0PLkyrHemoHrD7eVS577GGZZ1NuLerWavDI7ylU,7056
|
|
7
|
+
contex_python-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
contex_python-0.1.0.dist-info/top_level.txt,sha256=Jk2ojGh-mGli0p8efzIi5IM8OEc4Et-0TAiBfNtGaTo,7
|
|
9
|
+
contex_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Contex Team
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
contex
|