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.
- seqoria_agent/__init__.py +17 -0
- seqoria_agent/client/__init__.py +11 -0
- seqoria_agent/client/cache/__init__.py +178 -0
- seqoria_agent/client/client.py +1267 -0
- seqoria_agent/client/core.py +22 -0
- seqoria_agent/client/parameters.py +39 -0
- seqoria_agent/client/simple_http_transport.py +126 -0
- seqoria_agent/feature-implementation.md +846 -0
- seqoria_agent/models/__init__.py +17 -0
- seqoria_agent/models/chunk.py +27 -0
- seqoria_agent/models/envelope.py +70 -0
- seqoria_agent/models/errors.py +75 -0
- seqoria_agent/providers/__init__.py +76 -0
- seqoria_agent/providers/anthropic_provider.py +398 -0
- seqoria_agent/providers/base_provider.py +139 -0
- seqoria_agent/providers/errors.py +175 -0
- seqoria_agent/providers/openai_provider.py +402 -0
- seqoria_agent/providers/schemas.py +43 -0
- seqoria_agent/server/__init__.py +8 -0
- seqoria_agent/server/fastxmcp/__init__.py +15 -0
- seqoria_agent/server/fastxmcp/app.py +491 -0
- seqoria_agent/server/formatters.py +192 -0
- seqoria_agent-0.1.0.dist-info/METADATA +185 -0
- seqoria_agent-0.1.0.dist-info/RECORD +26 -0
- seqoria_agent-0.1.0.dist-info/WHEEL +4 -0
- seqoria_agent-0.1.0.dist-info/licenses/LICENSE.txt +201 -0
|
@@ -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}"
|