zettcode-server 0.1.1__tar.gz → 0.3.4__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.
- {zettcode_server-0.1.1 → zettcode_server-0.3.4}/.gitignore +7 -0
- zettcode_server-0.3.4/PKG-INFO +23 -0
- zettcode_server-0.3.4/pyproject.toml +47 -0
- {zettcode_server-0.1.1/src/zettcode_server → zettcode_server-0.3.4/src/zettcode/server}/__init__.py +4 -4
- {zettcode_server-0.1.1/src/zettcode_server → zettcode_server-0.3.4/src/zettcode/server}/app.py +176 -23
- {zettcode_server-0.1.1/src/zettcode_server → zettcode_server-0.3.4/src/zettcode/server}/cli.py +12 -7
- zettcode_server-0.3.4/src/zettcode/server/py.typed +0 -0
- zettcode_server-0.3.4/src/zettcode/server/runner.py +789 -0
- {zettcode_server-0.1.1/src/zettcode_server → zettcode_server-0.3.4/src/zettcode/server}/schemas.py +186 -14
- {zettcode_server-0.1.1/src/zettcode_server → zettcode_server-0.3.4/src/zettcode/server}/serialize.py +45 -3
- {zettcode_server-0.1.1/src/zettcode_server → zettcode_server-0.3.4/src/zettcode/server}/settings.py +4 -1
- zettcode_server-0.1.1/PKG-INFO +0 -67
- zettcode_server-0.1.1/README.md +0 -38
- zettcode_server-0.1.1/pyproject.toml +0 -59
- zettcode_server-0.1.1/src/zettcode_server/runner.py +0 -472
|
@@ -24,7 +24,14 @@ docs/.vitepress/.temp/
|
|
|
24
24
|
# The TypeScript client is built from its sources; only dist/ is published.
|
|
25
25
|
clients/typescript/dist/
|
|
26
26
|
|
|
27
|
+
# The gateway's page is built from clients/gateway into the package when a distribution is made (`make gateway-ui`);
|
|
28
|
+
# the build output is not kept in the repository. pyproject's `artifacts` puts it in the wheel all the same.
|
|
29
|
+
packages/zettcode-gateway/src/zettcode/gateway/static/
|
|
30
|
+
|
|
27
31
|
# The palette gallery is generated by `make palettes`: every widget painted in
|
|
28
32
|
# every palette, written by docs/scripts/palette_gallery.py. Regenerate it
|
|
29
33
|
# rather than committing a 2 MB copy that goes stale the moment a colour moves.
|
|
30
34
|
docs/internal/palettes.html
|
|
35
|
+
|
|
36
|
+
# The release order is generated from the pyproject.toml files (`make order`); it is not kept in git.
|
|
37
|
+
packages/order.txt
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: zettcode-server
|
|
3
|
+
Version: 0.3.4
|
|
4
|
+
Summary: The HTTP face of ZettCode: a FastAPI server that drives the SDK
|
|
5
|
+
Project-URL: Repository, https://github.com/Chang-LeHung/zettcode
|
|
6
|
+
Author: Chang-LeHung
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: api,server,zettcode
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
+
Classifier: Typing :: Typed
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: fastapi<1,>=0.115
|
|
19
|
+
Requires-Dist: pydantic<3,>=2
|
|
20
|
+
Requires-Dist: starlette<2,>=0.48
|
|
21
|
+
Requires-Dist: uvicorn<1,>=0.35
|
|
22
|
+
Requires-Dist: zettcode-engine
|
|
23
|
+
Requires-Dist: zettcode-sdk
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "zettcode-server"
|
|
3
|
+
description = "The HTTP face of ZettCode: a FastAPI server that drives the SDK"
|
|
4
|
+
requires-python = ">=3.10"
|
|
5
|
+
dynamic = ["version"]
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [{name = "Chang-LeHung"}]
|
|
8
|
+
keywords = ["zettcode", "server", "api"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 5 - Production/Stable",
|
|
11
|
+
"Programming Language :: Python :: 3.10",
|
|
12
|
+
"Programming Language :: Python :: 3.11",
|
|
13
|
+
"Programming Language :: Python :: 3.12",
|
|
14
|
+
"Programming Language :: Python :: 3.13",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
"Programming Language :: Python :: 3.15",
|
|
17
|
+
"Typing :: Typed",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"zettcode-engine",
|
|
21
|
+
"zettcode-sdk",
|
|
22
|
+
"fastapi>=0.115,<1",
|
|
23
|
+
"starlette>=0.48,<2",
|
|
24
|
+
"uvicorn>=0.35,<1",
|
|
25
|
+
"pydantic>=2,<3",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Repository = "https://github.com/Chang-LeHung/zettcode"
|
|
30
|
+
|
|
31
|
+
[build-system]
|
|
32
|
+
requires = ["hatchling"]
|
|
33
|
+
build-backend = "hatchling.build"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.version]
|
|
36
|
+
# One version for the whole repository: the engine ships the file.
|
|
37
|
+
path = "../zettcode-engine/src/zettcode/version.py"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/zettcode"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = ["/src", "/pyproject.toml"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
asyncio_mode = "auto"
|
{zettcode_server-0.1.1/src/zettcode_server → zettcode_server-0.3.4/src/zettcode/server}/__init__.py
RENAMED
|
@@ -4,20 +4,20 @@ The server is a thin layer: an endpoint says what to ask the agent and how to le
|
|
|
4
4
|
SDK does the work. It owns no model loop, no tools, and no session format, so a session begun here is one the
|
|
5
5
|
terminal can open afterwards, and the other way round.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
It is part of this package, not a package of its own: one ``pip install zettcode`` writes both console scripts, the
|
|
8
|
+
terminal's ``zettcode`` and the server's ``zettcode-server``. The application is built by :func:`create_app`, and
|
|
9
|
+
served by :mod:`zettcode.server.cli`.
|
|
8
10
|
"""
|
|
9
11
|
|
|
10
12
|
from __future__ import annotations
|
|
11
13
|
|
|
12
14
|
from typing import TYPE_CHECKING, Any
|
|
13
15
|
|
|
14
|
-
__version__ = "0.1.1"
|
|
15
|
-
|
|
16
16
|
if TYPE_CHECKING: # pragma: no cover - annotations only; importing the app costs FastAPI
|
|
17
17
|
from .app import create_app
|
|
18
18
|
from .settings import Settings
|
|
19
19
|
|
|
20
|
-
__all__ = ["Settings", "
|
|
20
|
+
__all__ = ["Settings", "create_app"]
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def __getattr__(name: str) -> Any:
|
{zettcode_server-0.1.1/src/zettcode_server → zettcode_server-0.3.4/src/zettcode/server}/app.py
RENAMED
|
@@ -25,6 +25,7 @@ from zettcode.sdk.errors import (
|
|
|
25
25
|
NothingToDoError,
|
|
26
26
|
RunError,
|
|
27
27
|
RunTimeoutError,
|
|
28
|
+
SendError,
|
|
28
29
|
SessionBusyError,
|
|
29
30
|
SessionLockedError,
|
|
30
31
|
SessionNotFoundError,
|
|
@@ -33,8 +34,8 @@ from zettcode.sdk.errors import (
|
|
|
33
34
|
from zettcode.sdk.models import add_model as add_model_entry
|
|
34
35
|
from zettcode.sdk.models import remove_model as remove_model_entry
|
|
35
36
|
from zettcode.sdk.models import update_model as update_model_entry
|
|
37
|
+
from zettcode.version import __version__
|
|
36
38
|
|
|
37
|
-
from . import __version__
|
|
38
39
|
from .runner import NoModelError, Runner, prepare, result_payload
|
|
39
40
|
from .runner import Request as RunRequest
|
|
40
41
|
from .schemas import (
|
|
@@ -42,6 +43,9 @@ from .schemas import (
|
|
|
42
43
|
ApprovalAnswer,
|
|
43
44
|
ApprovalPolicy,
|
|
44
45
|
CancelledOut,
|
|
46
|
+
CommandIn,
|
|
47
|
+
CommandOut,
|
|
48
|
+
CommandsOut,
|
|
45
49
|
CompactionOut,
|
|
46
50
|
ContextOut,
|
|
47
51
|
CurrentSessionOut,
|
|
@@ -50,11 +54,15 @@ from .schemas import (
|
|
|
50
54
|
ErrorOut,
|
|
51
55
|
HealthOut,
|
|
52
56
|
HistoryOut,
|
|
57
|
+
InboxMessageOut,
|
|
58
|
+
InboxOut,
|
|
53
59
|
MentionsOut,
|
|
54
60
|
ModelCreate,
|
|
55
61
|
ModelsOut,
|
|
56
62
|
ModelUpdate,
|
|
63
|
+
PluginOut,
|
|
57
64
|
PluginsOut,
|
|
65
|
+
PluginSwitchIn,
|
|
58
66
|
PromptRequest,
|
|
59
67
|
QuestionAnswer,
|
|
60
68
|
RenameRequest,
|
|
@@ -64,6 +72,8 @@ from .schemas import (
|
|
|
64
72
|
RunStateOut,
|
|
65
73
|
SelfCompactionOut,
|
|
66
74
|
SelfCompactionUpdate,
|
|
75
|
+
SendIn,
|
|
76
|
+
SentOut,
|
|
67
77
|
SessionCreate,
|
|
68
78
|
SessionOut,
|
|
69
79
|
SessionsOut,
|
|
@@ -103,6 +113,7 @@ FAILURES: tuple[tuple[type[BaseException], int, str], ...] = (
|
|
|
103
113
|
(NothingToDoError, 409, "nothing_to_do"),
|
|
104
114
|
(NoModelError, 409, "no_model"),
|
|
105
115
|
(ConfigurationError, 400, "invalid"),
|
|
116
|
+
(SendError, 404, "unreachable"),
|
|
106
117
|
(RunTimeoutError, 504, "timeout"),
|
|
107
118
|
(RunError, 502, "run"),
|
|
108
119
|
(ZettCodeError, 500, "internal"),
|
|
@@ -157,6 +168,34 @@ def stream_of(runner: Runner, request: RunRequest, *, resume: bool = False) -> S
|
|
|
157
168
|
)
|
|
158
169
|
|
|
159
170
|
|
|
171
|
+
def message_stream_of(runner: Runner, message_id: str) -> StreamingResponse:
|
|
172
|
+
"""Answer one message another session sent, and stream the turn as ``/prompts`` streams one.
|
|
173
|
+
|
|
174
|
+
A message that is not waiting any more — answered already, or never arrived — is a 404, decided here, while a
|
|
175
|
+
status line is still possible; what fails once the stream is open becomes an ``error`` frame.
|
|
176
|
+
"""
|
|
177
|
+
if not runner.ready:
|
|
178
|
+
raise NoModelError("No model is configured; add one with POST /models")
|
|
179
|
+
try:
|
|
180
|
+
frames = runner.stream_message(message_id)
|
|
181
|
+
except KeyError:
|
|
182
|
+
raise StarletteHTTPException(status_code=404, detail="No message with that id is waiting") from None
|
|
183
|
+
|
|
184
|
+
async def body() -> AsyncIterator[bytes]:
|
|
185
|
+
try:
|
|
186
|
+
async for name, payload in frames:
|
|
187
|
+
yield sse_frame(name, payload)
|
|
188
|
+
except Exception as error: # noqa: BLE001 - the status line is gone; the failure goes down the stream
|
|
189
|
+
_, code = failure_of(error)
|
|
190
|
+
yield sse_frame("error", error_body(code, str(error)))
|
|
191
|
+
|
|
192
|
+
return StreamingResponse(
|
|
193
|
+
body(),
|
|
194
|
+
media_type="text/event-stream",
|
|
195
|
+
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
160
199
|
def runner_of(request: Request) -> Runner:
|
|
161
200
|
"""Return the runner the server's lifespan put on the application."""
|
|
162
201
|
return request.app.state.runner
|
|
@@ -248,9 +287,14 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
248
287
|
"""
|
|
249
288
|
if runner.busy:
|
|
250
289
|
raise SessionBusyError("A run is in progress; change the models when it ends")
|
|
251
|
-
|
|
252
|
-
if
|
|
253
|
-
|
|
290
|
+
entry = body.entry()
|
|
291
|
+
if runner.ready:
|
|
292
|
+
# The client the runs use changes with the file, so the model it lists and the one the next prompt can
|
|
293
|
+
# choose are the file's at once; a rebuild would drop the conversation in progress and its settings.
|
|
294
|
+
await asyncio.to_thread(runner.client.add_model, entry)
|
|
295
|
+
else:
|
|
296
|
+
added = await asyncio.to_thread(add_model_entry, entry, path=settings.config_path)
|
|
297
|
+
await runner.start(model=added[-1].name)
|
|
254
298
|
return ModelsOut(models=[model_out(model) for model in runner.models_now()], current=runner.current_model)
|
|
255
299
|
|
|
256
300
|
@protected.patch("/models/{name}", response_model=ModelsOut, tags=["models"])
|
|
@@ -261,8 +305,10 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
261
305
|
changes = body.changes()
|
|
262
306
|
if not changes:
|
|
263
307
|
raise ValueError("Name at least one field to change")
|
|
264
|
-
|
|
265
|
-
|
|
308
|
+
if runner.ready:
|
|
309
|
+
await asyncio.to_thread(runner.client.update_model, name, changes)
|
|
310
|
+
else:
|
|
311
|
+
await asyncio.to_thread(update_model_entry, name, changes, path=settings.config_path)
|
|
266
312
|
return ModelsOut(models=[model_out(model) for model in runner.models_now()], current=runner.current_model)
|
|
267
313
|
|
|
268
314
|
@protected.delete("/models/{name}", response_model=ModelsOut, tags=["models"])
|
|
@@ -270,13 +316,15 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
270
316
|
"""Remove one model from the configuration file; the last one cannot go, since a client needs one."""
|
|
271
317
|
if runner.busy:
|
|
272
318
|
raise SessionBusyError("A run is in progress; change the models when it ends")
|
|
273
|
-
|
|
274
|
-
|
|
319
|
+
if runner.ready:
|
|
320
|
+
await asyncio.to_thread(runner.client.remove_model, name)
|
|
321
|
+
else:
|
|
322
|
+
await asyncio.to_thread(remove_model_entry, name, path=settings.config_path)
|
|
275
323
|
return ModelsOut(models=[model_out(model) for model in runner.models_now()], current=runner.current_model)
|
|
276
324
|
|
|
277
325
|
@protected.get("/settings", response_model=SettingsOut, tags=["models"])
|
|
278
326
|
async def settings_out(runner: RunnerDep) -> SettingsOut:
|
|
279
|
-
"""Return the model, effort and
|
|
327
|
+
"""Return the model, effort, permission and harness a run starts with."""
|
|
280
328
|
client = runner.client
|
|
281
329
|
return SettingsOut(
|
|
282
330
|
model=model_out(client.model),
|
|
@@ -284,18 +332,20 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
284
332
|
efforts=list(client.efforts),
|
|
285
333
|
permission=client.permission,
|
|
286
334
|
permissions=list(client.permissions),
|
|
335
|
+
harness=client.harness,
|
|
287
336
|
)
|
|
288
337
|
|
|
289
338
|
@protected.patch("/settings", response_model=SettingsOut, tags=["models"])
|
|
290
339
|
async def change_settings(update: SettingsUpdate, runner: RunnerDep) -> SettingsOut:
|
|
291
|
-
"""Change the model,
|
|
340
|
+
"""Change the model, effort, permission level, harness or compaction point for the prompts that follow."""
|
|
292
341
|
if (
|
|
293
342
|
update.model is None
|
|
294
343
|
and update.effort is None
|
|
295
344
|
and update.permission is None
|
|
345
|
+
and update.harness is None
|
|
296
346
|
and update.compact_percent is None
|
|
297
347
|
):
|
|
298
|
-
raise ValueError("Name at least one of model, effort, permission or compact_percent")
|
|
348
|
+
raise ValueError("Name at least one of model, effort, permission, harness or compact_percent")
|
|
299
349
|
client = runner.client
|
|
300
350
|
if update.model is not None:
|
|
301
351
|
client.use_model(update.model)
|
|
@@ -303,6 +353,8 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
303
353
|
client.use_effort(update.effort)
|
|
304
354
|
if update.permission is not None:
|
|
305
355
|
client.use_permission(update.permission)
|
|
356
|
+
if update.harness is not None:
|
|
357
|
+
client.use_harness(update.harness)
|
|
306
358
|
unsaved: str | None = None
|
|
307
359
|
if update.compact_percent is not None:
|
|
308
360
|
if runner.busy:
|
|
@@ -313,16 +365,19 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
313
365
|
|
|
314
366
|
@protected.get("/sessions", response_model=SessionsOut, tags=["sessions"])
|
|
315
367
|
async def list_sessions(runner: RunnerDep, limit: Annotated[int, Query(ge=1, le=200)] = 20) -> SessionsOut:
|
|
316
|
-
"""List the stored sessions of this workspace, most recently active first."""
|
|
368
|
+
"""List the stored sessions of this workspace, most recently active first, and which of them are running."""
|
|
317
369
|
client = runner.client
|
|
318
370
|
stored = await client.sessions.list(limit=limit)
|
|
319
|
-
|
|
371
|
+
running = runner.running_sessions()
|
|
372
|
+
return SessionsOut(sessions=[session_out(item, client.session.id, item.id in running) for item in stored])
|
|
320
373
|
|
|
321
374
|
@protected.post("/sessions", response_model=SessionOut, status_code=201, tags=["sessions"])
|
|
322
375
|
async def new_session(runner: RunnerDep, body: SessionCreate | None = None) -> SessionOut:
|
|
323
|
-
"""Start an empty conversation and make it the one the next prompt continues.
|
|
324
|
-
|
|
325
|
-
|
|
376
|
+
"""Start an empty conversation and make it the one the next prompt continues.
|
|
377
|
+
|
|
378
|
+
A run in the conversation being left goes on there, and shows as running in the list.
|
|
379
|
+
"""
|
|
380
|
+
session = await runner.new_session()
|
|
326
381
|
if body is not None and body.title:
|
|
327
382
|
await session.rename(body.title)
|
|
328
383
|
return SessionOut(id=session.id, title=session.title, current=True)
|
|
@@ -334,8 +389,12 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
334
389
|
|
|
335
390
|
@protected.post("/sessions/{session_id}/open", response_model=CurrentSessionOut, tags=["sessions"])
|
|
336
391
|
async def open_session(session_id: str, runner: RunnerDep) -> CurrentSessionOut:
|
|
337
|
-
"""Continue a stored conversation: the next prompt goes to it.
|
|
338
|
-
|
|
392
|
+
"""Continue a stored conversation: the next prompt goes to it.
|
|
393
|
+
|
|
394
|
+
A run in the conversation being left goes on there. Opening a conversation that is running brings it back as
|
|
395
|
+
it is, run and all.
|
|
396
|
+
"""
|
|
397
|
+
session = await runner.open_session(session_id)
|
|
339
398
|
return current_session_out(session)
|
|
340
399
|
|
|
341
400
|
@protected.post("/sessions/current/rename", response_model=SessionOut, tags=["sessions"])
|
|
@@ -421,6 +480,30 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
421
480
|
"""
|
|
422
481
|
return stream_of(runner, request)
|
|
423
482
|
|
|
483
|
+
@protected.get("/commands", response_model=CommandsOut, tags=["prompts"])
|
|
484
|
+
async def commands(runner: RunnerDep) -> CommandsOut:
|
|
485
|
+
"""List the commands every front end may run: a name, a summary, and what the run takes.
|
|
486
|
+
|
|
487
|
+
These are what the plugins registered on ``ctx.commands`` — text and a toast, or a preset for a turn — so a
|
|
488
|
+
client with no terminal can offer them without reimplementing any of them.
|
|
489
|
+
"""
|
|
490
|
+
return CommandsOut(commands=list(runner.commands()))
|
|
491
|
+
|
|
492
|
+
@protected.post("/commands/{name}", response_model=CommandOut, tags=["prompts"])
|
|
493
|
+
async def run_command(name: str, body: CommandIn, runner: RunnerDep) -> CommandOut:
|
|
494
|
+
"""Run one command by name and return what it answered with.
|
|
495
|
+
|
|
496
|
+
A command that answers with a prompt is not run as a turn here: the client sends ``POST /prompts`` with that
|
|
497
|
+
text and the ``shown`` line, so the turn belongs to the conversation the client is looking at, like every
|
|
498
|
+
other prompt.
|
|
499
|
+
|
|
500
|
+
``404 not_found`` when no plugin registered that name.
|
|
501
|
+
"""
|
|
502
|
+
try:
|
|
503
|
+
return await runner.run_command(name, body.argument)
|
|
504
|
+
except KeyError:
|
|
505
|
+
raise StarletteHTTPException(status_code=404, detail=f"No plugin registered the command {name!r}") from None
|
|
506
|
+
|
|
424
507
|
@protected.post("/runs/resume", tags=["prompts"], responses={200: {"content": {"text/event-stream": {}}}})
|
|
425
508
|
async def resume_run(request: ResumeRequest, runner: RunnerDep) -> StreamingResponse:
|
|
426
509
|
"""Continue the interrupted request where it stopped, streaming it like ``/prompts``.
|
|
@@ -456,7 +539,8 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
456
539
|
@protected.post("/runs/steer", response_model=SteeredOut, tags=["prompts"])
|
|
457
540
|
async def steer(body: SteerIn, runner: RunnerDep) -> SteeredOut:
|
|
458
541
|
"""Send a message into the run in progress; it drops what it was doing to take it into account."""
|
|
459
|
-
|
|
542
|
+
taken = runner.steer(body.text, body)
|
|
543
|
+
return SteeredOut(steered=taken is not None, text=taken)
|
|
460
544
|
|
|
461
545
|
@protected.post("/runs/cancel", response_model=CancelledOut, tags=["prompts"])
|
|
462
546
|
async def cancel_run(runner: RunnerDep) -> CancelledOut:
|
|
@@ -465,7 +549,12 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
465
549
|
|
|
466
550
|
@protected.post("/sessions/current/ask", response_model=AnswerOut, tags=["sessions"])
|
|
467
551
|
async def ask_side_question(body: SideQuestionRequest, runner: RunnerDep) -> AnswerOut:
|
|
468
|
-
"""Ask something answered from the conversation but kept out of it, as ``/btw`` does.
|
|
552
|
+
"""Ask something answered from the conversation but kept out of it, as ``/btw`` does.
|
|
553
|
+
|
|
554
|
+
The client that asked is the reader, so the run is stored with the reader's origin and a reopen may restore it;
|
|
555
|
+
nothing has to be passed for that. Only the terminal restores one on resume — ``/history`` leaves every side
|
|
556
|
+
exchange out — and what is drawn live is the front end's own decision, as for the streamed sibling.
|
|
557
|
+
"""
|
|
469
558
|
if runner.busy:
|
|
470
559
|
raise SessionBusyError("A run is already in progress; ask again when it ends")
|
|
471
560
|
return AnswerOut(answer=await runner.client.session.ask(body.question, timeout=body.timeout))
|
|
@@ -522,11 +611,53 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
522
611
|
headers={"Content-Disposition": f'attachment; filename="zettcode-{session.id[:8]}.html"'},
|
|
523
612
|
)
|
|
524
613
|
|
|
614
|
+
@protected.post("/sessions/current/send", response_model=SentOut, tags=["sessions"])
|
|
615
|
+
async def send_message(body: SendIn, runner: RunnerDep) -> SentOut:
|
|
616
|
+
"""Send a message to another ZettCode session explicitly, without a model turn."""
|
|
617
|
+
to, disposition = await runner.send(body.to, body.text, mode=body.mode)
|
|
618
|
+
return SentOut(to=to, disposition=disposition)
|
|
619
|
+
|
|
525
620
|
@protected.get("/plugins", response_model=PluginsOut, tags=["models"])
|
|
526
621
|
async def plugins(runner: RunnerDep) -> PluginsOut:
|
|
527
|
-
"""List the plugins this server loaded,
|
|
622
|
+
"""List the plugins this server loaded, the ones that failed, and every one a switch can reach."""
|
|
528
623
|
report = runner.client.plugins
|
|
529
|
-
return PluginsOut(
|
|
624
|
+
return PluginsOut(
|
|
625
|
+
loaded=list(report.loaded),
|
|
626
|
+
failures=list(report.failures),
|
|
627
|
+
entries=[
|
|
628
|
+
PluginOut(
|
|
629
|
+
name=entry.name,
|
|
630
|
+
origin=entry.origin,
|
|
631
|
+
builtin=entry.builtin,
|
|
632
|
+
enabled=entry.enabled,
|
|
633
|
+
failure=entry.failure,
|
|
634
|
+
setting=entry.setting,
|
|
635
|
+
switchable=entry.switchable,
|
|
636
|
+
why=entry.why,
|
|
637
|
+
)
|
|
638
|
+
for entry in report.entries
|
|
639
|
+
],
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
@protected.put("/plugins/{name}", response_model=PluginOut, tags=["models"])
|
|
643
|
+
async def switch_plugin(name: str, body: PluginSwitchIn, runner: RunnerDep) -> PluginOut:
|
|
644
|
+
"""Turn one plugin's work on or off for the next start, and return it as the list now shows it.
|
|
645
|
+
|
|
646
|
+
Nothing is loaded or unloaded here: a switch is read once, when a process starts. A compiled-in plugin is
|
|
647
|
+
switched by a configuration key in the server's configuration file, an installed one by [plugins] disable there;
|
|
648
|
+
a client that wants the change now restarts the server, which is what the extension does.
|
|
649
|
+
"""
|
|
650
|
+
entry = await asyncio.to_thread(runner.client.set_plugin_enabled, name, body.enabled)
|
|
651
|
+
return PluginOut(
|
|
652
|
+
name=entry.name,
|
|
653
|
+
origin=entry.origin,
|
|
654
|
+
builtin=entry.builtin,
|
|
655
|
+
enabled=entry.enabled,
|
|
656
|
+
failure=entry.failure,
|
|
657
|
+
setting=entry.setting,
|
|
658
|
+
switchable=entry.switchable,
|
|
659
|
+
why=entry.why,
|
|
660
|
+
)
|
|
530
661
|
|
|
531
662
|
@protected.get("/mentions", response_model=MentionsOut, tags=["models"])
|
|
532
663
|
async def mentions(
|
|
@@ -534,9 +665,31 @@ def create_app(settings: Settings, *, client_factory: ClientFactory | None = Non
|
|
|
534
665
|
query: Annotated[str, Query(description="What was typed after the @, without it")] = "",
|
|
535
666
|
limit: Annotated[int, Query(ge=1, le=200)] = 50,
|
|
536
667
|
) -> MentionsOut:
|
|
537
|
-
"""List the files, skills and plugin resources an ``@`` in the prompt can reference."""
|
|
668
|
+
"""List the files, skills, sessions and plugin resources an ``@`` in the prompt can reference."""
|
|
538
669
|
return MentionsOut(mentions=[mention_out(item) for item in await runner.mentions(query, limit)])
|
|
539
670
|
|
|
671
|
+
@protected.get("/messages", response_model=InboxOut, tags=["prompts"])
|
|
672
|
+
async def messages(runner: RunnerDep) -> InboxOut:
|
|
673
|
+
"""List the messages other sessions sent that this chat has not answered yet, oldest first."""
|
|
674
|
+
return InboxOut(
|
|
675
|
+
messages=[
|
|
676
|
+
InboxMessageOut(
|
|
677
|
+
id=waiting.id,
|
|
678
|
+
sender_id=waiting.message.sender_id,
|
|
679
|
+
sender=waiting.message.sender,
|
|
680
|
+
text=waiting.message.text,
|
|
681
|
+
via=waiting.message.via,
|
|
682
|
+
hops=waiting.message.hops,
|
|
683
|
+
)
|
|
684
|
+
for waiting in runner.waiting()
|
|
685
|
+
]
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
@protected.post("/messages/{message_id}", tags=["prompts"], responses={200: {"content": {"text/event-stream": {}}}})
|
|
689
|
+
async def answer_message(message_id: str, runner: RunnerDep) -> StreamingResponse:
|
|
690
|
+
"""Answer one message another session sent, streaming the turn exactly as ``/prompts`` does."""
|
|
691
|
+
return message_stream_of(runner, message_id)
|
|
692
|
+
|
|
540
693
|
@protected.post("/doctor", response_model=DoctorOut, tags=["models"])
|
|
541
694
|
async def doctor(runner: RunnerDep, body: DoctorRequest | None = None) -> DoctorOut:
|
|
542
695
|
"""Ask every configured model one short question and report which of them answer, as ``zettcode doctor``."""
|
{zettcode_server-0.1.1/src/zettcode_server → zettcode_server-0.3.4/src/zettcode/server}/cli.py
RENAMED
|
@@ -11,7 +11,8 @@ import os
|
|
|
11
11
|
import sys
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
-
from . import __version__
|
|
14
|
+
from zettcode.version import __version__
|
|
15
|
+
|
|
15
16
|
from .settings import DEFAULT_HOST, DEFAULT_PERMISSION, DEFAULT_PORT, TOKEN_VARIABLE, Settings
|
|
16
17
|
|
|
17
18
|
|
|
@@ -41,9 +42,14 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
41
42
|
)
|
|
42
43
|
parser.add_argument(
|
|
43
44
|
"--permission",
|
|
44
|
-
choices=("deny", "ask", "safe", "auto"),
|
|
45
|
+
choices=("deny", "ask", "safe", "full", "auto"),
|
|
45
46
|
default=DEFAULT_PERMISSION,
|
|
46
|
-
help=f"shell permission a run starts at (default: {DEFAULT_PERMISSION})",
|
|
47
|
+
help=f"shell permission a run starts at (default: {DEFAULT_PERMISSION}); auto is the old name of full",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--harness",
|
|
51
|
+
action="store_true",
|
|
52
|
+
help="start with the just-in-time harness on: the model may write and run its own Python tools (default: off)",
|
|
47
53
|
)
|
|
48
54
|
parser.add_argument("--model", default=None, help="model to start on, by name (default: the first configured)")
|
|
49
55
|
parser.add_argument("--effort", default=None, help="reasoning effort to start on (default: the model's own)")
|
|
@@ -59,10 +65,8 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
59
65
|
|
|
60
66
|
|
|
61
67
|
def version_text() -> str:
|
|
62
|
-
"""Return the line ``--version`` prints: the server
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return f"zettcode-server {__version__} (zettcode {agent})"
|
|
68
|
+
"""Return the line ``--version`` prints: the server and the agent are one package, so one version."""
|
|
69
|
+
return f"zettcode-server {__version__}"
|
|
66
70
|
|
|
67
71
|
|
|
68
72
|
def settings_from(argv: list[str] | None = None) -> Settings:
|
|
@@ -79,6 +83,7 @@ def settings_from(argv: list[str] | None = None) -> Settings:
|
|
|
79
83
|
port=args.port,
|
|
80
84
|
token=args.token or os.environ.get(TOKEN_VARIABLE) or None,
|
|
81
85
|
permission=args.permission,
|
|
86
|
+
harness=args.harness,
|
|
82
87
|
model=args.model,
|
|
83
88
|
effort=args.effort,
|
|
84
89
|
config_path=args.config.expanduser() if args.config is not None else None,
|
|
File without changes
|