agent-runtime-client 0.1.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.
- agent_runtime_client-0.1.0/LICENSE +21 -0
- agent_runtime_client-0.1.0/PKG-INFO +239 -0
- agent_runtime_client-0.1.0/README.md +213 -0
- agent_runtime_client-0.1.0/pyproject.toml +39 -0
- agent_runtime_client-0.1.0/setup.cfg +4 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client/__init__.py +84 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client/client.py +319 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client/errors.py +51 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client/functions.py +92 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client/helpers.py +30 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client/models.py +131 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client/py.typed +0 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client/validation.py +493 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client.egg-info/PKG-INFO +239 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client.egg-info/SOURCES.txt +19 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client.egg-info/dependency_links.txt +1 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client.egg-info/requires.txt +3 -0
- agent_runtime_client-0.1.0/src/agent_runtime_client.egg-info/top_level.txt +1 -0
- agent_runtime_client-0.1.0/tests/test_client.py +158 -0
- agent_runtime_client-0.1.0/tests/test_runtime.py +75 -0
- agent_runtime_client-0.1.0/tests/test_validation.py +198 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Petr Tripolsky
|
|
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,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-runtime-client
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python client for agent-runtime over the standard library: generate_object() with answers validated against a full JSON Schema, generate_text(), and the admin API.
|
|
5
|
+
Author-email: Petr Tripolsky <tripolskypetr@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/tripolskypetr/agent-runtime/tree/master/packages/python-sdk#readme
|
|
8
|
+
Project-URL: Repository, https://github.com/tripolskypetr/agent-runtime
|
|
9
|
+
Project-URL: Issues, https://github.com/tripolskypetr/agent-runtime/issues
|
|
10
|
+
Project-URL: Server, https://github.com/tripolskypetr/agent-runtime
|
|
11
|
+
Keywords: agent-runtime,sdk,client,ai-agent,ollama,structured-output,json-schema,generateObject,generateText
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.9
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Provides-Extra: unicode
|
|
24
|
+
Requires-Dist: regex>=2024.4.16; extra == "unicode"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# agent-runtime-client
|
|
28
|
+
|
|
29
|
+
> Python client for [agent-runtime](https://github.com/tripolskypetr/agent-runtime) over the standard library. `generate_object()` returns an answer validated against your JSON Schema, `generate_text()` a plain text one, and the admin API gives you the transcripts.
|
|
30
|
+
|
|
31
|
+
agent-runtime is a REST harness for a [pi](https://github.com/earendil-works/pi)-based agent on Ollama: it runs the agent with `bash`, `read_file` and `provide_answer` tools in an isolated workspace and returns an answer that matches a JSON schema. This package is the Python counterpart of [agent-runtime-sdk](https://www.npmjs.com/package/agent-runtime-sdk) for Node.js: the same `generate_object()` / `generate_text()` pair, the same error model, the same admin API.
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
- `generate_object()`: the answer as a `dict`, validated on the client with a port of the server's own validator; a mismatch runs the inference again
|
|
36
|
+
- `generate_text()`: plain text answers from the same agent
|
|
37
|
+
- `inference()` / `text()`: the full typed response (`status` is `"success"` or `"error"`) without raising
|
|
38
|
+
- `AgentRuntimeError` with the server's error code, per-attempt details and inference id
|
|
39
|
+
- Admin API: list and search inferences, read transcripts, download attachments, delete runs
|
|
40
|
+
- Zero dependencies, Python 3.9+, `urllib` underneath; the HTTP layer is replaceable for tests
|
|
41
|
+
- `format` is a full JSON Schema (draft 2020-12), enforced server-side by ajv
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install agent-runtime-client
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Run the server first (see the [agent-runtime README](https://github.com/tripolskypetr/agent-runtime#quick-start-docker)):
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
docker run -p 10050:10050 -e OLLAMA_API_KEY=... tripolskypetr/agent-runtime
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from agent_runtime_client import encode_file, generate_object
|
|
59
|
+
|
|
60
|
+
report = generate_object(
|
|
61
|
+
[{"role": "user", "content": "sales.csv is in your working directory. Total the amount column and name the top city."}],
|
|
62
|
+
{
|
|
63
|
+
"type": "object",
|
|
64
|
+
"required": ["total", "topCity"],
|
|
65
|
+
"properties": {
|
|
66
|
+
"total": {"type": "number", "description": "Sum of the amount column"},
|
|
67
|
+
"topCity": {"type": "string", "enum": ["Berlin", "Paris"], "description": "City with the largest amount"},
|
|
68
|
+
"note": {"type": "string", "description": "Optional remark"},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
files=[encode_file("sales.csv", "city,amount\nBerlin,120.5\nParis,340.25")],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
report["total"] # 460.75
|
|
75
|
+
report["topCity"] # "Paris"
|
|
76
|
+
report.get("note") # None unless the model filled it
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The standalone functions create a client per call. Without `base_url` they talk to `AGENT_RUNTIME_URL` or `http://localhost:10050`:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from agent_runtime_client import generate_text
|
|
83
|
+
|
|
84
|
+
summary = generate_text(
|
|
85
|
+
[{"role": "user", "content": "Summarise notes.txt"}],
|
|
86
|
+
files=[encode_file("notes.txt", notes)],
|
|
87
|
+
base_url="http://agent-runtime:10050",
|
|
88
|
+
headers={"x-token": "…"},
|
|
89
|
+
timeout=600,
|
|
90
|
+
)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Keyword arguments: `files`, `model`, `instructions`, `base_url`, `headers`, `timeout` (seconds, `None` waits as long as the inference takes), `validate_answer`, `max_attempts`, `transport`.
|
|
94
|
+
|
|
95
|
+
For many calls against one server, or for the admin API, keep a client:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from agent_runtime_client import create_client
|
|
99
|
+
|
|
100
|
+
client = create_client("http://localhost:10050")
|
|
101
|
+
report = client.generate_object(messages, format)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Images
|
|
105
|
+
|
|
106
|
+
Images go into `messages[].images` as base64; the server shows them to the model in user messages and stores them in the history. They are not written to the workspace: to process a picture with bash or `read_file`, pass it in `files`.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
from agent_runtime_client import generate_object, user_message
|
|
110
|
+
|
|
111
|
+
answer = generate_object(
|
|
112
|
+
[user_message("Describe the chart", [open("chart.png", "rb").read()])],
|
|
113
|
+
{"type": "object", "required": ["trend"], "properties": {"trend": {"type": "string", "enum": ["up", "down", "flat"]}}},
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Records of a `wwwroot.jsonl` file (`{"role", "content", "images"}`) can be passed as `messages` directly.
|
|
118
|
+
|
|
119
|
+
## Client-side validation and retries
|
|
120
|
+
|
|
121
|
+
The package ships a port of the server's answer validator ([validation.py](https://github.com/tripolskypetr/agent-runtime/blob/master/packages/python-sdk/src/agent_runtime_client/validation.py): a dependency-free JSON Schema checker with the same messages as the TypeScript original; its tests pin them against the shared fixtures). The client runs it on every successful answer, and when the answer does not match `format` it runs the inference again, up to `max_attempts` (3) times in total, then fails with `schema_mismatch` and one detail line per rejected inference. This should never trigger against a current server, which validates with the same logic; it is the client's guarantee that the value you get really matches the schema.
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
client = create_client(validate_answer=True, max_attempts=3) # defaults
|
|
125
|
+
generate_object(messages, format, validate_answer=False) # trust the server, no retries
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The validator is also exported for answers that come from somewhere else, e.g. a cache on disk after a schema change:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from agent_runtime_client import assert_answer, validate_answer
|
|
132
|
+
|
|
133
|
+
result = validate_answer(json.loads(cached), Agent1Format)
|
|
134
|
+
if result.success:
|
|
135
|
+
result.data["verdict"]
|
|
136
|
+
else:
|
|
137
|
+
print(result.error) # 'Invalid arguments: signal.direction: must be exactly "long"'
|
|
138
|
+
|
|
139
|
+
answer = assert_answer(json.loads(cached), Agent1Format) # raises ValueError instead
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Supported keywords: `type` (string or list, `null` included), `enum`, `const`, `oneOf` / `anyOf` / `allOf` / `not`, `if` / `then` / `else`, local `$ref`, `properties` / `required` / `additionalProperties` / `patternProperties` / `propertyNames` / `dependentRequired` / `dependentSchemas` / `minProperties` / `maxProperties`, `items` / `prefixItems` / `contains` / `uniqueItems` / `minItems` / `maxItems`, `minLength` / `maxLength` / `pattern`, `minimum` / `maximum` / `exclusiveMinimum` / `exclusiveMaximum` / `multipleOf`. `format` and unknown keywords are ignored, so the client is never stricter than the server. `pattern` is matched with Python's `re`; a pattern it cannot compile, such as `\p{L}` classes, is skipped unless the optional `regex` package is installed: `pip install agent-runtime-client[unicode]`.
|
|
143
|
+
|
|
144
|
+
## Responses and errors
|
|
145
|
+
|
|
146
|
+
`generate_object()` and `generate_text()` raise `AgentRuntimeError` when the agent fails. `inference()` and `text()` return the typed response instead:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from agent_runtime_client import inference
|
|
150
|
+
|
|
151
|
+
response = inference(messages, format)
|
|
152
|
+
if response["status"] == "success":
|
|
153
|
+
response["data"] # the answer
|
|
154
|
+
response["turns"] # model calls
|
|
155
|
+
else:
|
|
156
|
+
response["error"]["code"] # "schema_mismatch" | "empty_answer" | "max_turns" | "timeout" | "provider_error" | "internal_error"
|
|
157
|
+
response["error"]["details"] # one line per failed attempt
|
|
158
|
+
response["id"] # look the transcript up in the admin UI
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from agent_runtime_client import AgentRuntimeError, generate_object
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
generate_object(messages, format)
|
|
166
|
+
except AgentRuntimeError as error:
|
|
167
|
+
error.status # HTTP status, 0 for network failures
|
|
168
|
+
error.code # InferenceErrorCode ("invalid_format" for a rejected schema) or None for other HTTP errors
|
|
169
|
+
error.details # per-attempt details
|
|
170
|
+
error.inference_id # id of the stored run
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
The server rejects a schema that does not compile (a `$ref` to a missing `$defs` entry) with HTTP 400 and `error.code = "invalid_format"`; no inference is recorded.
|
|
174
|
+
|
|
175
|
+
## Admin API
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
listed = client.list_inferences(status="error", mode="object", limit=20)
|
|
179
|
+
found = client.list_inferences(q="Hilda") # case-insensitive substring search
|
|
180
|
+
record = client.get_inference(listed["items"][0]["id"]) # request, transcript, result/error, attachments
|
|
181
|
+
file = client.get_attachment(record["id"], record["attachments"][0]["id"]) # {...meta, "data": bytes}
|
|
182
|
+
client.delete_inference(record["id"])
|
|
183
|
+
client.health()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Long inferences
|
|
187
|
+
|
|
188
|
+
An inference can run for minutes. The default `timeout` is `None`, so the client waits as long as the server takes; set `timeout` in seconds to bound a call. The HTTP layer is a plain function and can be replaced, for a `requests` session or a mock in tests:
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
def transport(method, url, headers, body, timeout):
|
|
192
|
+
response = session.request(method, url, headers=headers, data=body, timeout=timeout)
|
|
193
|
+
return response.status_code, dict(response.headers), response.content
|
|
194
|
+
|
|
195
|
+
client = create_client(transport=transport)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## API
|
|
199
|
+
|
|
200
|
+
Standalone functions (create the client internally):
|
|
201
|
+
|
|
202
|
+
| Function | Returns |
|
|
203
|
+
|----------|---------|
|
|
204
|
+
| `generate_object(messages, format, **kw)` | `dict` |
|
|
205
|
+
| `generate_text(messages, **kw)` | `str` |
|
|
206
|
+
| `inference(messages, format, **kw)` | `InferenceResponse` |
|
|
207
|
+
| `text(messages, **kw)` | `TextResponse` |
|
|
208
|
+
|
|
209
|
+
Client:
|
|
210
|
+
|
|
211
|
+
| Method | Returns |
|
|
212
|
+
|--------|---------|
|
|
213
|
+
| `create_client(base_url=None, **options)` / `AgentRuntimeClient(...)` | options: `headers`, `timeout`, `validate_answer` (True), `max_attempts` (3), `transport` |
|
|
214
|
+
| `generate_object(messages, format, *, files, model, instructions, headers, timeout)` | `dict` |
|
|
215
|
+
| `generate_text(messages, *, ...)` | `str` |
|
|
216
|
+
| `inference(...)` / `text(...)` | typed response |
|
|
217
|
+
| `health()` | `Health` |
|
|
218
|
+
| `list_inferences(*, limit, offset, status, mode, q)` | `{"total", "items"}` |
|
|
219
|
+
| `get_inference(id)` | `InferenceRecord` |
|
|
220
|
+
| `get_attachment(id, attachment_id)` | `Attachment` with `data: bytes` |
|
|
221
|
+
| `delete_inference(id)` | `{"ok": bool}` |
|
|
222
|
+
| `validate_answer(value, format)`, `assert_answer(value, format)` | client-side validation |
|
|
223
|
+
| `validate_tool_arguments(value, format)`, `validate_value(value, schema, path)` | the raw validator |
|
|
224
|
+
| `encode_file(name, bytes_or_str)`, `encode_image(bytes)`, `user_message(text, images=None)` | request helpers |
|
|
225
|
+
|
|
226
|
+
The response shapes are `TypedDict`s in `agent_runtime_client.models`.
|
|
227
|
+
|
|
228
|
+
## Development
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
python3 -m venv .venv && .venv/bin/pip install build twine
|
|
232
|
+
.venv/bin/python -m unittest discover -s tests -v # validator and client, no server
|
|
233
|
+
BASE_URL=http://localhost:10151 .venv/bin/python -m unittest tests.test_runtime -v # against a test server
|
|
234
|
+
.venv/bin/python -m build && .venv/bin/twine check dist/*
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# agent-runtime-client
|
|
2
|
+
|
|
3
|
+
> Python client for [agent-runtime](https://github.com/tripolskypetr/agent-runtime) over the standard library. `generate_object()` returns an answer validated against your JSON Schema, `generate_text()` a plain text one, and the admin API gives you the transcripts.
|
|
4
|
+
|
|
5
|
+
agent-runtime is a REST harness for a [pi](https://github.com/earendil-works/pi)-based agent on Ollama: it runs the agent with `bash`, `read_file` and `provide_answer` tools in an isolated workspace and returns an answer that matches a JSON schema. This package is the Python counterpart of [agent-runtime-sdk](https://www.npmjs.com/package/agent-runtime-sdk) for Node.js: the same `generate_object()` / `generate_text()` pair, the same error model, the same admin API.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- `generate_object()`: the answer as a `dict`, validated on the client with a port of the server's own validator; a mismatch runs the inference again
|
|
10
|
+
- `generate_text()`: plain text answers from the same agent
|
|
11
|
+
- `inference()` / `text()`: the full typed response (`status` is `"success"` or `"error"`) without raising
|
|
12
|
+
- `AgentRuntimeError` with the server's error code, per-attempt details and inference id
|
|
13
|
+
- Admin API: list and search inferences, read transcripts, download attachments, delete runs
|
|
14
|
+
- Zero dependencies, Python 3.9+, `urllib` underneath; the HTTP layer is replaceable for tests
|
|
15
|
+
- `format` is a full JSON Schema (draft 2020-12), enforced server-side by ajv
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install agent-runtime-client
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Run the server first (see the [agent-runtime README](https://github.com/tripolskypetr/agent-runtime#quick-start-docker)):
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
docker run -p 10050:10050 -e OLLAMA_API_KEY=... tripolskypetr/agent-runtime
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from agent_runtime_client import encode_file, generate_object
|
|
33
|
+
|
|
34
|
+
report = generate_object(
|
|
35
|
+
[{"role": "user", "content": "sales.csv is in your working directory. Total the amount column and name the top city."}],
|
|
36
|
+
{
|
|
37
|
+
"type": "object",
|
|
38
|
+
"required": ["total", "topCity"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"total": {"type": "number", "description": "Sum of the amount column"},
|
|
41
|
+
"topCity": {"type": "string", "enum": ["Berlin", "Paris"], "description": "City with the largest amount"},
|
|
42
|
+
"note": {"type": "string", "description": "Optional remark"},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
files=[encode_file("sales.csv", "city,amount\nBerlin,120.5\nParis,340.25")],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
report["total"] # 460.75
|
|
49
|
+
report["topCity"] # "Paris"
|
|
50
|
+
report.get("note") # None unless the model filled it
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The standalone functions create a client per call. Without `base_url` they talk to `AGENT_RUNTIME_URL` or `http://localhost:10050`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from agent_runtime_client import generate_text
|
|
57
|
+
|
|
58
|
+
summary = generate_text(
|
|
59
|
+
[{"role": "user", "content": "Summarise notes.txt"}],
|
|
60
|
+
files=[encode_file("notes.txt", notes)],
|
|
61
|
+
base_url="http://agent-runtime:10050",
|
|
62
|
+
headers={"x-token": "…"},
|
|
63
|
+
timeout=600,
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Keyword arguments: `files`, `model`, `instructions`, `base_url`, `headers`, `timeout` (seconds, `None` waits as long as the inference takes), `validate_answer`, `max_attempts`, `transport`.
|
|
68
|
+
|
|
69
|
+
For many calls against one server, or for the admin API, keep a client:
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from agent_runtime_client import create_client
|
|
73
|
+
|
|
74
|
+
client = create_client("http://localhost:10050")
|
|
75
|
+
report = client.generate_object(messages, format)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Images
|
|
79
|
+
|
|
80
|
+
Images go into `messages[].images` as base64; the server shows them to the model in user messages and stores them in the history. They are not written to the workspace: to process a picture with bash or `read_file`, pass it in `files`.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from agent_runtime_client import generate_object, user_message
|
|
84
|
+
|
|
85
|
+
answer = generate_object(
|
|
86
|
+
[user_message("Describe the chart", [open("chart.png", "rb").read()])],
|
|
87
|
+
{"type": "object", "required": ["trend"], "properties": {"trend": {"type": "string", "enum": ["up", "down", "flat"]}}},
|
|
88
|
+
)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Records of a `wwwroot.jsonl` file (`{"role", "content", "images"}`) can be passed as `messages` directly.
|
|
92
|
+
|
|
93
|
+
## Client-side validation and retries
|
|
94
|
+
|
|
95
|
+
The package ships a port of the server's answer validator ([validation.py](https://github.com/tripolskypetr/agent-runtime/blob/master/packages/python-sdk/src/agent_runtime_client/validation.py): a dependency-free JSON Schema checker with the same messages as the TypeScript original; its tests pin them against the shared fixtures). The client runs it on every successful answer, and when the answer does not match `format` it runs the inference again, up to `max_attempts` (3) times in total, then fails with `schema_mismatch` and one detail line per rejected inference. This should never trigger against a current server, which validates with the same logic; it is the client's guarantee that the value you get really matches the schema.
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
client = create_client(validate_answer=True, max_attempts=3) # defaults
|
|
99
|
+
generate_object(messages, format, validate_answer=False) # trust the server, no retries
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The validator is also exported for answers that come from somewhere else, e.g. a cache on disk after a schema change:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from agent_runtime_client import assert_answer, validate_answer
|
|
106
|
+
|
|
107
|
+
result = validate_answer(json.loads(cached), Agent1Format)
|
|
108
|
+
if result.success:
|
|
109
|
+
result.data["verdict"]
|
|
110
|
+
else:
|
|
111
|
+
print(result.error) # 'Invalid arguments: signal.direction: must be exactly "long"'
|
|
112
|
+
|
|
113
|
+
answer = assert_answer(json.loads(cached), Agent1Format) # raises ValueError instead
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Supported keywords: `type` (string or list, `null` included), `enum`, `const`, `oneOf` / `anyOf` / `allOf` / `not`, `if` / `then` / `else`, local `$ref`, `properties` / `required` / `additionalProperties` / `patternProperties` / `propertyNames` / `dependentRequired` / `dependentSchemas` / `minProperties` / `maxProperties`, `items` / `prefixItems` / `contains` / `uniqueItems` / `minItems` / `maxItems`, `minLength` / `maxLength` / `pattern`, `minimum` / `maximum` / `exclusiveMinimum` / `exclusiveMaximum` / `multipleOf`. `format` and unknown keywords are ignored, so the client is never stricter than the server. `pattern` is matched with Python's `re`; a pattern it cannot compile, such as `\p{L}` classes, is skipped unless the optional `regex` package is installed: `pip install agent-runtime-client[unicode]`.
|
|
117
|
+
|
|
118
|
+
## Responses and errors
|
|
119
|
+
|
|
120
|
+
`generate_object()` and `generate_text()` raise `AgentRuntimeError` when the agent fails. `inference()` and `text()` return the typed response instead:
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from agent_runtime_client import inference
|
|
124
|
+
|
|
125
|
+
response = inference(messages, format)
|
|
126
|
+
if response["status"] == "success":
|
|
127
|
+
response["data"] # the answer
|
|
128
|
+
response["turns"] # model calls
|
|
129
|
+
else:
|
|
130
|
+
response["error"]["code"] # "schema_mismatch" | "empty_answer" | "max_turns" | "timeout" | "provider_error" | "internal_error"
|
|
131
|
+
response["error"]["details"] # one line per failed attempt
|
|
132
|
+
response["id"] # look the transcript up in the admin UI
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from agent_runtime_client import AgentRuntimeError, generate_object
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
generate_object(messages, format)
|
|
140
|
+
except AgentRuntimeError as error:
|
|
141
|
+
error.status # HTTP status, 0 for network failures
|
|
142
|
+
error.code # InferenceErrorCode ("invalid_format" for a rejected schema) or None for other HTTP errors
|
|
143
|
+
error.details # per-attempt details
|
|
144
|
+
error.inference_id # id of the stored run
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The server rejects a schema that does not compile (a `$ref` to a missing `$defs` entry) with HTTP 400 and `error.code = "invalid_format"`; no inference is recorded.
|
|
148
|
+
|
|
149
|
+
## Admin API
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
listed = client.list_inferences(status="error", mode="object", limit=20)
|
|
153
|
+
found = client.list_inferences(q="Hilda") # case-insensitive substring search
|
|
154
|
+
record = client.get_inference(listed["items"][0]["id"]) # request, transcript, result/error, attachments
|
|
155
|
+
file = client.get_attachment(record["id"], record["attachments"][0]["id"]) # {...meta, "data": bytes}
|
|
156
|
+
client.delete_inference(record["id"])
|
|
157
|
+
client.health()
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Long inferences
|
|
161
|
+
|
|
162
|
+
An inference can run for minutes. The default `timeout` is `None`, so the client waits as long as the server takes; set `timeout` in seconds to bound a call. The HTTP layer is a plain function and can be replaced, for a `requests` session or a mock in tests:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
def transport(method, url, headers, body, timeout):
|
|
166
|
+
response = session.request(method, url, headers=headers, data=body, timeout=timeout)
|
|
167
|
+
return response.status_code, dict(response.headers), response.content
|
|
168
|
+
|
|
169
|
+
client = create_client(transport=transport)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## API
|
|
173
|
+
|
|
174
|
+
Standalone functions (create the client internally):
|
|
175
|
+
|
|
176
|
+
| Function | Returns |
|
|
177
|
+
|----------|---------|
|
|
178
|
+
| `generate_object(messages, format, **kw)` | `dict` |
|
|
179
|
+
| `generate_text(messages, **kw)` | `str` |
|
|
180
|
+
| `inference(messages, format, **kw)` | `InferenceResponse` |
|
|
181
|
+
| `text(messages, **kw)` | `TextResponse` |
|
|
182
|
+
|
|
183
|
+
Client:
|
|
184
|
+
|
|
185
|
+
| Method | Returns |
|
|
186
|
+
|--------|---------|
|
|
187
|
+
| `create_client(base_url=None, **options)` / `AgentRuntimeClient(...)` | options: `headers`, `timeout`, `validate_answer` (True), `max_attempts` (3), `transport` |
|
|
188
|
+
| `generate_object(messages, format, *, files, model, instructions, headers, timeout)` | `dict` |
|
|
189
|
+
| `generate_text(messages, *, ...)` | `str` |
|
|
190
|
+
| `inference(...)` / `text(...)` | typed response |
|
|
191
|
+
| `health()` | `Health` |
|
|
192
|
+
| `list_inferences(*, limit, offset, status, mode, q)` | `{"total", "items"}` |
|
|
193
|
+
| `get_inference(id)` | `InferenceRecord` |
|
|
194
|
+
| `get_attachment(id, attachment_id)` | `Attachment` with `data: bytes` |
|
|
195
|
+
| `delete_inference(id)` | `{"ok": bool}` |
|
|
196
|
+
| `validate_answer(value, format)`, `assert_answer(value, format)` | client-side validation |
|
|
197
|
+
| `validate_tool_arguments(value, format)`, `validate_value(value, schema, path)` | the raw validator |
|
|
198
|
+
| `encode_file(name, bytes_or_str)`, `encode_image(bytes)`, `user_message(text, images=None)` | request helpers |
|
|
199
|
+
|
|
200
|
+
The response shapes are `TypedDict`s in `agent_runtime_client.models`.
|
|
201
|
+
|
|
202
|
+
## Development
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
python3 -m venv .venv && .venv/bin/pip install build twine
|
|
206
|
+
.venv/bin/python -m unittest discover -s tests -v # validator and client, no server
|
|
207
|
+
BASE_URL=http://localhost:10151 .venv/bin/python -m unittest tests.test_runtime -v # against a test server
|
|
208
|
+
.venv/bin/python -m build && .venv/bin/twine check dist/*
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## License
|
|
212
|
+
|
|
213
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agent-runtime-client"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python client for agent-runtime over the standard library: generate_object() with answers validated against a full JSON Schema, generate_text(), and the admin API."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "Petr Tripolsky", email = "tripolskypetr@gmail.com" }]
|
|
13
|
+
keywords = ["agent-runtime", "sdk", "client", "ai-agent", "ollama", "structured-output", "json-schema", "generateObject", "generateText"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Topic :: Software Development :: Libraries",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = []
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
unicode = ["regex>=2024.4.16"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/tripolskypetr/agent-runtime/tree/master/packages/python-sdk#readme"
|
|
31
|
+
Repository = "https://github.com/tripolskypetr/agent-runtime"
|
|
32
|
+
Issues = "https://github.com/tripolskypetr/agent-runtime/issues"
|
|
33
|
+
Server = "https://github.com/tripolskypetr/agent-runtime"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
agent_runtime_client = ["py.typed"]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Python client for agent-runtime: generate_object() / generate_text() over the REST API, plus the admin API.
|
|
2
|
+
|
|
3
|
+
Import name ``agent_runtime_client``, distribution ``agent-runtime-client``. Zero dependencies, Python 3.9+.
|
|
4
|
+
"""
|
|
5
|
+
from .client import DEFAULT_BASE_URL, DEFAULT_MAX_ATTEMPTS, AgentRuntimeClient, Transport, create_client, resolve_base_url, urllib_transport
|
|
6
|
+
from .errors import AgentRuntimeError, is_inference_failure, is_invalid_format
|
|
7
|
+
from .functions import generate_object, generate_text, inference, text
|
|
8
|
+
from .helpers import encode_file, encode_image, user_message
|
|
9
|
+
from .models import (
|
|
10
|
+
Attachment,
|
|
11
|
+
AttachmentKind,
|
|
12
|
+
AttachmentMeta,
|
|
13
|
+
FormatModel,
|
|
14
|
+
Health,
|
|
15
|
+
InferenceError,
|
|
16
|
+
InferenceErrorCode,
|
|
17
|
+
InferenceFailure,
|
|
18
|
+
InferenceMode,
|
|
19
|
+
InferenceRecord,
|
|
20
|
+
InferenceResponse,
|
|
21
|
+
InferenceStatus,
|
|
22
|
+
InferenceSuccess,
|
|
23
|
+
InferenceSummary,
|
|
24
|
+
ListInferencesResponse,
|
|
25
|
+
RequestFile,
|
|
26
|
+
RequestMessage,
|
|
27
|
+
RequestRole,
|
|
28
|
+
RunInfo,
|
|
29
|
+
StoredRequest,
|
|
30
|
+
TextResponse,
|
|
31
|
+
TextSuccess,
|
|
32
|
+
)
|
|
33
|
+
from .validation import ValidationResult, assert_answer, resolve_ref, type_of, validate_answer, validate_tool_arguments, validate_value
|
|
34
|
+
|
|
35
|
+
__version__ = "0.1.0"
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AgentRuntimeClient",
|
|
39
|
+
"AgentRuntimeError",
|
|
40
|
+
"Attachment",
|
|
41
|
+
"AttachmentKind",
|
|
42
|
+
"AttachmentMeta",
|
|
43
|
+
"DEFAULT_BASE_URL",
|
|
44
|
+
"DEFAULT_MAX_ATTEMPTS",
|
|
45
|
+
"FormatModel",
|
|
46
|
+
"Health",
|
|
47
|
+
"InferenceError",
|
|
48
|
+
"InferenceErrorCode",
|
|
49
|
+
"InferenceFailure",
|
|
50
|
+
"InferenceMode",
|
|
51
|
+
"InferenceRecord",
|
|
52
|
+
"InferenceResponse",
|
|
53
|
+
"InferenceStatus",
|
|
54
|
+
"InferenceSuccess",
|
|
55
|
+
"InferenceSummary",
|
|
56
|
+
"ListInferencesResponse",
|
|
57
|
+
"RequestFile",
|
|
58
|
+
"RequestMessage",
|
|
59
|
+
"RequestRole",
|
|
60
|
+
"RunInfo",
|
|
61
|
+
"StoredRequest",
|
|
62
|
+
"TextResponse",
|
|
63
|
+
"TextSuccess",
|
|
64
|
+
"Transport",
|
|
65
|
+
"ValidationResult",
|
|
66
|
+
"assert_answer",
|
|
67
|
+
"create_client",
|
|
68
|
+
"encode_file",
|
|
69
|
+
"encode_image",
|
|
70
|
+
"generate_object",
|
|
71
|
+
"generate_text",
|
|
72
|
+
"inference",
|
|
73
|
+
"is_inference_failure",
|
|
74
|
+
"is_invalid_format",
|
|
75
|
+
"resolve_base_url",
|
|
76
|
+
"resolve_ref",
|
|
77
|
+
"text",
|
|
78
|
+
"type_of",
|
|
79
|
+
"urllib_transport",
|
|
80
|
+
"user_message",
|
|
81
|
+
"validate_answer",
|
|
82
|
+
"validate_tool_arguments",
|
|
83
|
+
"validate_value",
|
|
84
|
+
]
|