zot/examples/rpc/python/zot_client.py

115 lines
3.3 KiB
Python
Raw Normal View History

feat: zotcore SDK + zot rpc subprocess protocol two new ways to embed the zot agent runtime in third-party apps: 1. pkg/zotcore - public Go SDK - Runtime type: New(Config), Prompt(ctx,text,imgs)->chan Event, Cancel, Compact, SetModel, State, Messages, Cost, ListModels, Close. Concurrent-safe; one prompt at a time per Runtime, ErrBusy if you try to overlap. Spawn multiple Runtimes for multiple projects. - Public types mirror the JSON-RPC wire schema 1:1 so consumers can share parsing code with the out-of-process clients. - Internal core/agent/provider stay internal; SDK is a thin facade that exposes only what's stable. 2. zot rpc subcommand - newline-delimited JSON on stdin/stdout - 'zot rpc' (or 'zot --rpc') turns the agent runtime into a subprocess that any language can drive via pipes. - Commands: hello, prompt, abort, compact, get_state, get_messages, clear, set_model, get_models, ping. Each optionally carries an id; the matching response echoes it. - Stream notifications: turn_start, user_message, assistant_start, text_delta, tool_call, tool_progress, tool_result, assistant_message, usage, turn_end, done, error, compact_done. Same shape as the existing --json mode events (modes.EventToJSON / ContentToJSON were exported for reuse). - Auth: optional ZOTCORE_RPC_TOKEN env var; first command must be hello {token: ...} when set. Without the env var the spawning process is implicitly trusted. - Concurrency: one prompt or compact at a time per process, enforced by a turnMu mutex. abort fires immediately regardless. Stdin close exits the process. 3. docs/rpc.md - full schema reference 4. examples/rpc/{python,node,shell,go} - reference clients 5. examples/sdk - in-process Go embedding example 6. README updated with a new modes entry and an embedding section
2026-04-19 12:26:48 +02:00
"""Minimal Python client for the `zot rpc` JSON protocol.
Usage:
python zot_client.py "fix the failing test"
Spawns `zot rpc`, sends one prompt, prints assistant text as it streams,
and exits when the turn finishes. No external dependencies stdlib
only. Implements just enough of the protocol to be useful as a
starting point; see docs/rpc.md for the full schema.
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import threading
import uuid
class ZotClient:
def __init__(self, *flags: str) -> None:
argv = ["zot", "rpc", *flags]
env = os.environ.copy()
self.proc = subprocess.Popen(
argv,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=sys.stderr,
env=env,
text=True,
bufsize=1, # line-buffered
)
self._lock = threading.Lock()
def send(self, **command) -> str:
"""Send one command, return its `id`."""
if "id" not in command:
command["id"] = uuid.uuid4().hex[:8]
line = json.dumps(command)
with self._lock:
assert self.proc.stdin is not None
self.proc.stdin.write(line + "\n")
self.proc.stdin.flush()
return command["id"]
def events(self):
"""Yield every JSON object the server emits, until stdout closes."""
assert self.proc.stdout is not None
for line in self.proc.stdout:
line = line.strip()
if not line:
continue
try:
yield json.loads(line)
except json.JSONDecodeError:
continue
def close(self) -> None:
if self.proc.stdin is not None:
try:
self.proc.stdin.close()
except Exception:
pass
self.proc.wait(timeout=5)
def main() -> int:
if len(sys.argv) < 2:
print("usage: zot_client.py <prompt>", file=sys.stderr)
return 2
prompt = " ".join(sys.argv[1:])
client = ZotClient()
try:
token = os.environ.get("ZOTCORE_RPC_TOKEN")
if token:
client.send(type="hello", token=token)
client.send(type="prompt", message=prompt)
for ev in client.events():
t = ev.get("type")
if t == "text_delta":
sys.stdout.write(ev.get("delta", ""))
sys.stdout.flush()
elif t == "tool_call":
print(
f"\n[tool] {ev.get('name')}({json.dumps(ev.get('args', {}))})",
file=sys.stderr,
)
elif t == "tool_result":
if ev.get("is_error"):
print("[tool error]", file=sys.stderr)
elif t == "usage":
cum = ev.get("cumulative", {})
print(
f"\n[usage] cum input={cum.get('input')} "
f"output={cum.get('output')} cost=${cum.get('cost_usd', 0):.4f}",
file=sys.stderr,
)
elif t == "done":
break
elif t == "error":
print(f"\n[error] {ev.get('message')}", file=sys.stderr)
return 1
print()
return 0
finally:
client.close()
if __name__ == "__main__":
sys.exit(main())