system-one 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.
- system_one/__init__.py +65 -0
- system_one/agent.py +106 -0
- system_one/backends/__init__.py +94 -0
- system_one/backends/http.py +182 -0
- system_one/backends/onnx.py +410 -0
- system_one/catalog.py +145 -0
- system_one/cli.py +126 -0
- system_one/errors.py +38 -0
- system_one/export.py +252 -0
- system_one/mcp.py +111 -0
- system_one/py.typed +0 -0
- system_one/schemas.py +259 -0
- system_one/settings.py +68 -0
- system_one-0.1.0.dist-info/METADATA +209 -0
- system_one-0.1.0.dist-info/RECORD +17 -0
- system_one-0.1.0.dist-info/WHEEL +4 -0
- system_one-0.1.0.dist-info/entry_points.txt +4 -0
system_one/__init__.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""A vendor-neutral SDK for System One models: `ask(state, questions) -> answers`."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import version
|
|
4
|
+
|
|
5
|
+
from system_one.agent import AsyncSystemOne, SystemOne
|
|
6
|
+
from system_one.errors import (
|
|
7
|
+
RETRY_STATUSES,
|
|
8
|
+
APIConnectionError,
|
|
9
|
+
APIError,
|
|
10
|
+
APITimeoutError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
SystemOneError,
|
|
13
|
+
)
|
|
14
|
+
from system_one.schemas import (
|
|
15
|
+
Answer,
|
|
16
|
+
Choice,
|
|
17
|
+
ChoiceAnswer,
|
|
18
|
+
Noul,
|
|
19
|
+
NoulAnswer,
|
|
20
|
+
NoulCriteria,
|
|
21
|
+
Question,
|
|
22
|
+
Score,
|
|
23
|
+
ScoreAnswer,
|
|
24
|
+
SystemOneInput,
|
|
25
|
+
SystemOneOutput,
|
|
26
|
+
Usage,
|
|
27
|
+
)
|
|
28
|
+
from system_one.settings import (
|
|
29
|
+
HTTPConfig,
|
|
30
|
+
ONNXConfig,
|
|
31
|
+
OpenRouterConfig,
|
|
32
|
+
Settings,
|
|
33
|
+
TypesafeConfig,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__version__ = version("system-one")
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"RETRY_STATUSES",
|
|
40
|
+
"APIConnectionError",
|
|
41
|
+
"APIError",
|
|
42
|
+
"APITimeoutError",
|
|
43
|
+
"Answer",
|
|
44
|
+
"AsyncSystemOne",
|
|
45
|
+
"AuthenticationError",
|
|
46
|
+
"Choice",
|
|
47
|
+
"ChoiceAnswer",
|
|
48
|
+
"HTTPConfig",
|
|
49
|
+
"Noul",
|
|
50
|
+
"NoulAnswer",
|
|
51
|
+
"NoulCriteria",
|
|
52
|
+
"ONNXConfig",
|
|
53
|
+
"OpenRouterConfig",
|
|
54
|
+
"Question",
|
|
55
|
+
"Score",
|
|
56
|
+
"ScoreAnswer",
|
|
57
|
+
"Settings",
|
|
58
|
+
"SystemOne",
|
|
59
|
+
"SystemOneError",
|
|
60
|
+
"SystemOneInput",
|
|
61
|
+
"SystemOneOutput",
|
|
62
|
+
"TypesafeConfig",
|
|
63
|
+
"Usage",
|
|
64
|
+
"__version__",
|
|
65
|
+
]
|
system_one/agent.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""The one public entry point: `SystemOne.ask(state, questions)`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
6
|
+
|
|
7
|
+
from system_one.backends import create_async_backend, create_backend
|
|
8
|
+
from system_one.schemas import SystemOneInput
|
|
9
|
+
from system_one.settings import ONNXConfig, Settings
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from typing_extensions import Self
|
|
13
|
+
|
|
14
|
+
from system_one.backends import AsyncBackend, Backend
|
|
15
|
+
from system_one.schemas import QuestionInput, State, SystemOneOutput
|
|
16
|
+
from system_one.settings import BackendConfig
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseSystemOne:
|
|
20
|
+
"""Settings resolution and input building, shared by the sync and async agents.
|
|
21
|
+
|
|
22
|
+
`config` is the backend's own configuration — `TypesafeConfig()`,
|
|
23
|
+
`OpenRouterConfig()`, `HTTPConfig(base_url=..., model=...)` or `ONNXConfig()` —
|
|
24
|
+
and picks the backend to build; omitted, `SYSTEM_ONE_BACKEND` decides and the
|
|
25
|
+
config is read from the environment. `using` hands over an already-built backend
|
|
26
|
+
instead, which is how tests and custom providers plug in.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
backend: Any # narrowed to `Backend` / `AsyncBackend` by the two subclasses
|
|
30
|
+
|
|
31
|
+
def __init__(self, config: BackendConfig | None = None, **overrides: Any) -> None:
|
|
32
|
+
if config is not None and "backend" not in overrides:
|
|
33
|
+
overrides["backend"] = "onnx" if isinstance(config, ONNXConfig) else "http"
|
|
34
|
+
self.settings = Settings(**overrides)
|
|
35
|
+
self.config = config
|
|
36
|
+
|
|
37
|
+
def _input(
|
|
38
|
+
self, state: State, questions: QuestionInput, model: str | None
|
|
39
|
+
) -> SystemOneInput:
|
|
40
|
+
return SystemOneInput.model_validate(
|
|
41
|
+
{
|
|
42
|
+
"state": state,
|
|
43
|
+
"model": model or self.settings.model or self.backend.model,
|
|
44
|
+
"questions": questions,
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class SystemOne(BaseSystemOne):
|
|
50
|
+
"""Ask typed questions about a state and get calibrated answers back."""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
config: BackendConfig | None = None,
|
|
55
|
+
*,
|
|
56
|
+
using: Backend | None = None,
|
|
57
|
+
**overrides: Any,
|
|
58
|
+
) -> None:
|
|
59
|
+
super().__init__(config, **overrides)
|
|
60
|
+
self.backend: Backend = (
|
|
61
|
+
using if using is not None else create_backend(self.settings, config)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def ask(
|
|
65
|
+
self, state: State, questions: QuestionInput, *, model: str | None = None
|
|
66
|
+
) -> SystemOneOutput:
|
|
67
|
+
return self.backend.ask(self._input(state, questions, model))
|
|
68
|
+
|
|
69
|
+
def close(self) -> None:
|
|
70
|
+
self.backend.close()
|
|
71
|
+
|
|
72
|
+
def __enter__(self) -> Self:
|
|
73
|
+
return self
|
|
74
|
+
|
|
75
|
+
def __exit__(self, *exc: object) -> None:
|
|
76
|
+
self.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class AsyncSystemOne(BaseSystemOne):
|
|
80
|
+
"""The async counterpart of `SystemOne`: the same `ask`, awaited."""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
config: BackendConfig | None = None,
|
|
85
|
+
*,
|
|
86
|
+
using: AsyncBackend | None = None,
|
|
87
|
+
**overrides: Any,
|
|
88
|
+
) -> None:
|
|
89
|
+
super().__init__(config, **overrides)
|
|
90
|
+
self.backend: AsyncBackend = (
|
|
91
|
+
using if using is not None else create_async_backend(self.settings, config)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
async def ask(
|
|
95
|
+
self, state: State, questions: QuestionInput, *, model: str | None = None
|
|
96
|
+
) -> SystemOneOutput:
|
|
97
|
+
return await self.backend.ask(self._input(state, questions, model))
|
|
98
|
+
|
|
99
|
+
async def close(self) -> None:
|
|
100
|
+
await self.backend.close()
|
|
101
|
+
|
|
102
|
+
async def __aenter__(self) -> Self:
|
|
103
|
+
return self
|
|
104
|
+
|
|
105
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
106
|
+
await self.close()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Backend selection. Vendor imports stay inside the factories so the extras stay optional."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from typing import TYPE_CHECKING, Protocol, TypeVar
|
|
7
|
+
|
|
8
|
+
from system_one.errors import SystemOneError
|
|
9
|
+
from system_one.settings import BackendConfig, HTTPConfig, ONNXConfig
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
|
|
14
|
+
from system_one.schemas import SystemOneInput, SystemOneOutput
|
|
15
|
+
from system_one.settings import Settings
|
|
16
|
+
|
|
17
|
+
Config = TypeVar("Config", bound=BackendConfig)
|
|
18
|
+
|
|
19
|
+
EXTRA_HINTS = {
|
|
20
|
+
"http": "The http backend needs httpx2: pip install 'system-one[http]'",
|
|
21
|
+
"onnx": "The onnx backend needs onnxruntime, tokenizers and numpy: "
|
|
22
|
+
"pip install 'system-one[onnx]'",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Backend(Protocol):
|
|
27
|
+
"""What `SystemOne` needs from a backend."""
|
|
28
|
+
|
|
29
|
+
model: str
|
|
30
|
+
"""The model to ask when neither the call nor `SYSTEM_ONE_MODEL` names one."""
|
|
31
|
+
|
|
32
|
+
def ask(self, request: SystemOneInput) -> SystemOneOutput: ...
|
|
33
|
+
|
|
34
|
+
def close(self) -> None: ...
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AsyncBackend(Protocol):
|
|
38
|
+
"""The same two methods as `Backend`, awaited."""
|
|
39
|
+
|
|
40
|
+
model: str
|
|
41
|
+
|
|
42
|
+
async def ask(self, request: SystemOneInput) -> SystemOneOutput: ...
|
|
43
|
+
|
|
44
|
+
async def close(self) -> None: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@contextmanager
|
|
48
|
+
def _hint(name: str) -> Iterator[None]:
|
|
49
|
+
"""Turn a missing vendor dependency — and only that — into its install hint."""
|
|
50
|
+
try:
|
|
51
|
+
yield
|
|
52
|
+
except ImportError as exc:
|
|
53
|
+
raise ImportError(EXTRA_HINTS[name]) from exc
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _resolve(
|
|
57
|
+
settings: Settings, config: BackendConfig | None, default: type[Config]
|
|
58
|
+
) -> Config:
|
|
59
|
+
"""The config to build with: the given one, or one read from the environment."""
|
|
60
|
+
# mypy cannot see that pydantic-settings fills the required fields from env.
|
|
61
|
+
config = default() if config is None else config # type: ignore[call-arg]
|
|
62
|
+
if not isinstance(config, default):
|
|
63
|
+
message = (
|
|
64
|
+
f"{type(config).__name__} does not configure the "
|
|
65
|
+
f"{settings.backend!r} backend."
|
|
66
|
+
)
|
|
67
|
+
raise SystemOneError(message)
|
|
68
|
+
if settings.model is not None:
|
|
69
|
+
config = config.model_copy(update={"model": settings.model})
|
|
70
|
+
return config
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def create_backend(settings: Settings, config: BackendConfig | None = None) -> Backend:
|
|
74
|
+
"""The sync backend named by `settings.backend`."""
|
|
75
|
+
if settings.backend == "onnx":
|
|
76
|
+
with _hint("onnx"):
|
|
77
|
+
from system_one.backends.onnx import ONNXBackend
|
|
78
|
+
return ONNXBackend(_resolve(settings, config, ONNXConfig))
|
|
79
|
+
with _hint("http"):
|
|
80
|
+
from system_one.backends.http import HTTPBackend
|
|
81
|
+
return HTTPBackend(_resolve(settings, config, HTTPConfig))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def create_async_backend(
|
|
85
|
+
settings: Settings, config: BackendConfig | None = None
|
|
86
|
+
) -> AsyncBackend:
|
|
87
|
+
"""The async backend named by `settings.backend`."""
|
|
88
|
+
if settings.backend == "onnx":
|
|
89
|
+
with _hint("onnx"):
|
|
90
|
+
from system_one.backends.onnx import AsyncONNXBackend
|
|
91
|
+
return AsyncONNXBackend(_resolve(settings, config, ONNXConfig))
|
|
92
|
+
with _hint("http"):
|
|
93
|
+
from system_one.backends.http import AsyncHTTPBackend
|
|
94
|
+
return AsyncHTTPBackend(_resolve(settings, config, HTTPConfig))
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""One HTTP backend for every vendor: same JSON body, the config carries the endpoint."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import secrets
|
|
8
|
+
import time
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
import httpx2
|
|
12
|
+
|
|
13
|
+
from system_one import __version__
|
|
14
|
+
from system_one.errors import (
|
|
15
|
+
RETRY_STATUSES,
|
|
16
|
+
APIConnectionError,
|
|
17
|
+
APIError,
|
|
18
|
+
APITimeoutError,
|
|
19
|
+
AuthenticationError,
|
|
20
|
+
SystemOneError,
|
|
21
|
+
)
|
|
22
|
+
from system_one.schemas import SystemOneOutput
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from collections.abc import Mapping
|
|
26
|
+
|
|
27
|
+
from system_one.schemas import SystemOneInput
|
|
28
|
+
from system_one.settings import HTTPConfig
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger("system_one")
|
|
31
|
+
|
|
32
|
+
MAX_BACKOFF = 5.0
|
|
33
|
+
INITIAL_BACKOFF = 0.5
|
|
34
|
+
MAX_ERROR_BODY = 200
|
|
35
|
+
AUTH_STATUSES = frozenset({401, 403})
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def retry_after(headers: Mapping[str, str]) -> float | None:
|
|
39
|
+
"""Read `retry-after-ms` then `retry-after`. HTTP-date forms fall back to backoff."""
|
|
40
|
+
for header, divisor in (("retry-after-ms", 1000.0), ("retry-after", 1.0)):
|
|
41
|
+
raw = headers.get(header)
|
|
42
|
+
if raw is None:
|
|
43
|
+
continue
|
|
44
|
+
try:
|
|
45
|
+
return float(raw) / divisor
|
|
46
|
+
except ValueError:
|
|
47
|
+
continue
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def backoff(attempt: int, after: float | None) -> float:
|
|
52
|
+
if after is not None:
|
|
53
|
+
return min(after, MAX_BACKOFF)
|
|
54
|
+
delay = min(MAX_BACKOFF, INITIAL_BACKOFF * (1 << attempt))
|
|
55
|
+
return delay * (0.5 + secrets.randbelow(500) / 1000)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def api_error(response: httpx2.Response) -> APIError:
|
|
59
|
+
status = response.status_code
|
|
60
|
+
error = AuthenticationError if status in AUTH_STATUSES else APIError
|
|
61
|
+
return error(
|
|
62
|
+
f"System One API returned {status}",
|
|
63
|
+
status=status,
|
|
64
|
+
body=response.text[:MAX_ERROR_BODY],
|
|
65
|
+
retry_after=retry_after(response.headers),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def is_retryable(error: SystemOneError) -> bool:
|
|
70
|
+
if isinstance(error, AuthenticationError):
|
|
71
|
+
return False
|
|
72
|
+
if isinstance(error, APIError):
|
|
73
|
+
return error.status in RETRY_STATUSES
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def transport_error(request: httpx2.Request, exc: httpx2.HTTPError) -> SystemOneError:
|
|
78
|
+
if isinstance(exc, httpx2.TimeoutException):
|
|
79
|
+
return APITimeoutError(f"Request to {request.url} timed out")
|
|
80
|
+
return APIConnectionError(f"Could not reach {request.url}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class BaseHTTPBackend:
|
|
84
|
+
"""Request building, error mapping and retry pacing, shared by both flavours."""
|
|
85
|
+
|
|
86
|
+
def __init__(self, config: HTTPConfig, *, transport: Any = None) -> None:
|
|
87
|
+
self.config = config
|
|
88
|
+
self.model = config.model
|
|
89
|
+
self.base_url = config.base_url.rstrip("/")
|
|
90
|
+
self.path = config.path
|
|
91
|
+
self._transport = transport
|
|
92
|
+
self._headers = {
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
"Accept": "application/json",
|
|
95
|
+
"User-Agent": f"system-one/{__version__}",
|
|
96
|
+
}
|
|
97
|
+
if config.api_key is not None:
|
|
98
|
+
self._headers["Authorization"] = (
|
|
99
|
+
f"Bearer {config.api_key.get_secret_value()}"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _build(self, method: str, path: str, body: str | None = None) -> httpx2.Request:
|
|
103
|
+
url = self.base_url + path
|
|
104
|
+
logger.debug("%s %s", method, url)
|
|
105
|
+
return httpx2.Request(method, url, headers=self._headers, content=body)
|
|
106
|
+
|
|
107
|
+
def _ask_request(self, request: SystemOneInput) -> httpx2.Request:
|
|
108
|
+
return self._build("POST", self.path, request.model_dump_json())
|
|
109
|
+
|
|
110
|
+
def _delay_or_reraise(self, error: SystemOneError, attempt: int) -> float:
|
|
111
|
+
if attempt >= self.config.max_retries or not is_retryable(error):
|
|
112
|
+
raise error
|
|
113
|
+
after = error.retry_after if isinstance(error, APIError) else None
|
|
114
|
+
return backoff(attempt, after)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class HTTPBackend(BaseHTTPBackend):
|
|
118
|
+
"""Talks to any vendor implementing the System One contract over HTTP."""
|
|
119
|
+
|
|
120
|
+
def __init__(self, config: HTTPConfig, *, transport: Any = None) -> None:
|
|
121
|
+
super().__init__(config, transport=transport)
|
|
122
|
+
self._client = httpx2.Client(timeout=config.timeout, transport=transport)
|
|
123
|
+
|
|
124
|
+
def ask(self, request: SystemOneInput) -> SystemOneOutput:
|
|
125
|
+
response = self._send(self._ask_request(request))
|
|
126
|
+
return SystemOneOutput.model_validate_json(response.content)
|
|
127
|
+
|
|
128
|
+
def close(self) -> None:
|
|
129
|
+
self._client.close()
|
|
130
|
+
|
|
131
|
+
def _send(self, request: httpx2.Request) -> httpx2.Response:
|
|
132
|
+
attempt = 0
|
|
133
|
+
while True:
|
|
134
|
+
try:
|
|
135
|
+
return self._attempt(request)
|
|
136
|
+
except SystemOneError as exc:
|
|
137
|
+
delay = self._delay_or_reraise(exc, attempt)
|
|
138
|
+
attempt += 1
|
|
139
|
+
time.sleep(delay)
|
|
140
|
+
|
|
141
|
+
def _attempt(self, request: httpx2.Request) -> httpx2.Response:
|
|
142
|
+
try:
|
|
143
|
+
response = self._client.send(request)
|
|
144
|
+
except httpx2.HTTPError as exc:
|
|
145
|
+
raise transport_error(request, exc) from exc
|
|
146
|
+
if response.is_success:
|
|
147
|
+
return response
|
|
148
|
+
raise api_error(response)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class AsyncHTTPBackend(BaseHTTPBackend):
|
|
152
|
+
"""The async counterpart of `HTTPBackend`, sharing its config and retry policy."""
|
|
153
|
+
|
|
154
|
+
def __init__(self, config: HTTPConfig, *, transport: Any = None) -> None:
|
|
155
|
+
super().__init__(config, transport=transport)
|
|
156
|
+
self._client = httpx2.AsyncClient(timeout=config.timeout, transport=transport)
|
|
157
|
+
|
|
158
|
+
async def ask(self, request: SystemOneInput) -> SystemOneOutput:
|
|
159
|
+
response = await self._send(self._ask_request(request))
|
|
160
|
+
return SystemOneOutput.model_validate_json(response.content)
|
|
161
|
+
|
|
162
|
+
async def close(self) -> None:
|
|
163
|
+
await self._client.aclose()
|
|
164
|
+
|
|
165
|
+
async def _send(self, request: httpx2.Request) -> httpx2.Response:
|
|
166
|
+
attempt = 0
|
|
167
|
+
while True:
|
|
168
|
+
try:
|
|
169
|
+
return await self._attempt(request)
|
|
170
|
+
except SystemOneError as exc:
|
|
171
|
+
delay = self._delay_or_reraise(exc, attempt)
|
|
172
|
+
attempt += 1
|
|
173
|
+
await asyncio.sleep(delay)
|
|
174
|
+
|
|
175
|
+
async def _attempt(self, request: httpx2.Request) -> httpx2.Response:
|
|
176
|
+
try:
|
|
177
|
+
response = await self._client.send(request)
|
|
178
|
+
except httpx2.HTTPError as exc:
|
|
179
|
+
raise transport_error(request, exc) from exc
|
|
180
|
+
if response.is_success:
|
|
181
|
+
return response
|
|
182
|
+
raise api_error(response)
|