seqoria-agent 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.
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ """seqoria_agent - Seqoria Agentic Assistant Framework public API."""
4
+
5
+ from seqoria_agent.client import AIClient, ChatChunk # noqa: F401
6
+
7
+ # The server runtime (FastMCP wrapper)
8
+ from seqoria_agent.server.fastxmcp import FastXMCP # noqa: F401
9
+
10
+ from seqoria_agent.models.envelope import DisplayEnvelope # noqa: F401
11
+
12
+ __all__: list[str] = [
13
+ "AIClient",
14
+ "ChatChunk",
15
+ "DisplayEnvelope",
16
+ "FastXMCP",
17
+ ]
@@ -0,0 +1,11 @@
1
+ """Client-side SDK for interacting with seqoria_agent servers."""
2
+
3
+ from .core import AIClient # noqa: F401
4
+ from seqoria_agent.models.chunk import ChatChunk # noqa: F401
5
+ from .simple_http_transport import SimpleHTTPTransport # noqa: F401
6
+
7
+ __all__: list[str] = [
8
+ "AIClient",
9
+ "ChatChunk",
10
+ "SimpleHTTPTransport",
11
+ ]
@@ -0,0 +1,178 @@
1
+ """Caching utilities for *seqoria_agent* client package.
2
+
3
+ This file is largely copied from the original `seqoria_agent.cache` module but split
4
+ under the ``seqoria_agent.client.cache`` namespace so that different back-ends can live
5
+ side-by-side.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import asyncio
11
+ import hashlib
12
+ import json
13
+ import time
14
+ from typing import Any, Dict, Optional, Protocol
15
+
16
+ __all__: list[str] = [
17
+ "CacheProtocol",
18
+ "NullCache",
19
+ "DictCache",
20
+ "RedisCache",
21
+ "build_cache_key",
22
+ ]
23
+
24
+
25
+ class CacheProtocol(Protocol):
26
+ """Minimal async cache contract accepted by :class:`seqoria_agent.client.core.AIClient`."""
27
+
28
+ async def get(self, key: str) -> Any | None: # noqa: D401 – single-line docstring
29
+ """Return the cached value or ``None`` when missing/expired."""
30
+
31
+ async def set(self, key: str, value: Any, ttl: int | None = None) -> None: # noqa: D401
32
+ """Store *value* under *key* with optional *ttl* (seconds)."""
33
+
34
+ async def delete(self, key: str) -> None: # noqa: D401
35
+ """Remove *key* from cache if present (best-effort)."""
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Fallback / test implementations
40
+ # ---------------------------------------------------------------------------
41
+
42
+
43
+ class NullCache:
44
+ """No-op implementation that fulfils :class:`CacheProtocol`."""
45
+
46
+ async def get(self, key: str): # type: ignore[override]
47
+ return None
48
+
49
+ async def set(self, key: str, value: Any, ttl: int | None = None): # noqa: D401, ANN001
50
+ return None
51
+
52
+ async def delete(self, key: str): # noqa: D401
53
+ return None
54
+
55
+
56
+ class DictCache:
57
+ """Simple in-memory cache with optional TTL support.
58
+
59
+ Thread-safe across coroutines via an ``asyncio.Lock``. **Not** suitable for
60
+ multi-process deployments – use :class:`RedisCache` instead.
61
+ """
62
+
63
+ def __init__(self):
64
+ self._data: Dict[str, tuple[Any, float | None]] = {}
65
+ self._lock = asyncio.Lock()
66
+
67
+ async def get(self, key: str): # noqa: D401
68
+ async with self._lock:
69
+ item = self._data.get(key)
70
+ if item is None:
71
+ return None
72
+ value, expires_at = item
73
+ if expires_at is not None and expires_at < time.time():
74
+ # Expired – remove lazily
75
+ self._data.pop(key, None)
76
+ return None
77
+ return value
78
+
79
+ async def set(self, key: str, value: Any, ttl: int | None = None): # noqa: D401, ANN001
80
+ async with self._lock:
81
+ expires_at: float | None = None
82
+ if ttl is not None and ttl > 0:
83
+ expires_at = time.time() + ttl
84
+ self._data[key] = (value, expires_at)
85
+
86
+ async def delete(self, key: str): # noqa: D401
87
+ async with self._lock:
88
+ self._data.pop(key, None)
89
+
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Redis backend (optional dependency)
93
+ # ---------------------------------------------------------------------------
94
+
95
+
96
+ class RedisCache: # pragma: no cover – optional path
97
+ """Redis-based cache using ``redis.asyncio``.
98
+
99
+ Requires ``pip install redis>=4.5``.
100
+ """
101
+
102
+ _redis_missing_msg = (
103
+ "RedisCache requires the 'redis' package. Install via 'pip install redis' to use this backend."
104
+ )
105
+
106
+ def __init__(self, url: str = "redis://localhost:6379", ttl: int | None = None):
107
+ """Initialize the RedisCache.
108
+
109
+ Parameters
110
+ ----------
111
+ url : str, optional
112
+ The connection URL for the Redis server,
113
+ by default "redis://localhost:6379".
114
+ ttl : int | None, optional
115
+ The default time-to-live in seconds for cache entries. If None,
116
+ keys do not expire by default. Defaults to `None`.
117
+
118
+ Raises
119
+ ------
120
+ ImportError
121
+ If the `redis` package is not installed.
122
+ """
123
+ try:
124
+ from redis import asyncio as redis_asyncio # type: ignore
125
+ except ModuleNotFoundError as exc:
126
+ raise ImportError(self._redis_missing_msg) from exc
127
+
128
+ self._redis = redis_asyncio.from_url(url, encoding="utf-8", decode_responses=False)
129
+ self._default_ttl = ttl
130
+
131
+ async def get(self, key: str): # noqa: D401
132
+ raw = await self._redis.get(key)
133
+ if raw is None:
134
+ return None
135
+ try:
136
+ return json.loads(raw)
137
+ except Exception:
138
+ return raw
139
+
140
+ async def set(self, key: str, value: Any, ttl: int | None = None): # noqa: D401, ANN001
141
+ try:
142
+ raw = json.dumps(value)
143
+ except TypeError:
144
+ raw = str(value)
145
+
146
+ ttl_to_use = ttl if ttl is not None else self._default_ttl
147
+ if ttl_to_use:
148
+ await self._redis.setex(key, ttl_to_use, raw)
149
+ else:
150
+ await self._redis.set(key, raw)
151
+
152
+ async def delete(self, key: str): # noqa: D401
153
+ await self._redis.delete(key)
154
+
155
+
156
+ # ---------------------------------------------------------------------------
157
+ # Helper – canonical cache keys
158
+ # ---------------------------------------------------------------------------
159
+
160
+ def build_cache_key(tool_name: str, arguments: Dict[str, Any], *, tenant_id: str | None = None) -> str: # noqa: D401
161
+ """Return a deterministic cache key for *tool_name* + *arguments*.
162
+
163
+ Parameters
164
+ ----------
165
+ tool_name:
166
+ Name of the tool.
167
+ arguments:
168
+ Input arguments dict (order independent).
169
+ tenant_id:
170
+ Optional namespace prefix for multi-tenant deployments. When given,
171
+ the resulting key is ``tenant:{tenant_id}:tool:{tool_name}:{hash}``.
172
+ """
173
+
174
+ canonical_json = json.dumps(arguments, sort_keys=True, separators=(",", ":"))
175
+ args_hash = hashlib.sha256(canonical_json.encode()).hexdigest()
176
+
177
+ prefix = f"tenant:{tenant_id}:" if tenant_id else ""
178
+ return f"{prefix}tool:{tool_name}:{args_hash}"