mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
regen the cloud client, add the six canonical flows, and gate them in CI
REGENERATED pkg/hanzoai/cloud/ from hanzo.yaml @ 07783f5 via the canonical
driver (hanzoai/openapi generate.py python) — 1132 paths, 1519 operations, 779
schemas, landing as 239 api + 1116 model modules. `generate.py python --check`
reports `[python] clean`: the committed tree is byte-identical to a fresh
generation, with no local strip of any kind.
The path count fell from 1885 because the spec deleted 18 products it authored
and served nowhere. Nothing here was lost to a collapse — LLM.md carried an open
defect saying 127 of 411 operations went missing to 23 case-variant tag groups
(AI/ai, Users/users). That is now fixed upstream, and this regeneration proves
it: 239 distinct tags produce 239 api modules, exactly 1:1.
Generating at all required two spec fixes. Both landed in hanzoai/openapi first,
neither is patched here:
- fc0c17a 35 /v1/platform operations carried no `responses`. OAS 3.x requires
it and openapi-generator aborts the whole document, so hanzo.yaml
was producing no client in ANY language.
- 07783f5 ChatCompletionResponse.choices was `items: {type: object}` — so
choices[0].message.content came out List[object], unusable without
a cast, on the most-called route in the API.
SIX EXAMPLES under examples/{hello,chat,money,store,agent,tools}, plus
examples/client.py as the single place a base URL or an env var is resolved.
Same six, same names, same order as the TypeScript set, so a reader who knows
one can navigate the other. They import from hanzoai.cloud — the generated
surface new work targets — not the frozen pkg/hanzoai/{api,models}.
Each flow's call sits behind `if __name__ == "__main__":` deliberately: that is
what lets the gate IMPORT all six to prove every `from hanzoai.cloud import X`
still resolves, with no API key and no socket. A spec change that renames or
drops an operation goes red in CI instead of in a user's app.
CI is the fleet convention and this repo had none: root hanzo.yml + a 7-line
cicd.yml importing hanzoai/ci. The gate is two blocks — import every generated
module (for generated code that IS the build step; there is no compiler to catch
a bad $ref or a model referencing a class the generator declined to emit), then
import the six flows. Both provision an interpreter with uv, because the arc
runner image promises none and a gate that silently no-ops is worse than none.
Scope is deliberate: the cloud client and its flows, not all 65 packages, so a
red gate means "the client the spec just produced is broken" rather than
"something, somewhere". Publishing is untouched — .hanzo/workflows/publish-pypi.yml
stays canonical because it reads the PyPI token from KMS. Two publish paths is
one too many.
3.1.4 -> 3.1.5. PyPI still serves 3.1.1; the tree has been ahead since the KMS
secret at hanzo/prod/python-sdk-publish went unseeded.
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
"""agent — define one, run it, read it back.
|
||||
|
||||
POST /v1/agents cloud_AgentsController.Create
|
||||
POST /v1/agents/{ref}/run cloud_AgentsController.Run
|
||||
GET /v1/agents/{ref} cloud_AgentsController.Get
|
||||
|
||||
``ref`` is the agent's public id (``agent_...``) OR its org-unique name, which
|
||||
is why the run and the read below can both use the name we just created without
|
||||
waiting for an id to come back.
|
||||
|
||||
python -m examples.agent
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from hanzoai.cloud import AgentsAPIApi, CloudAgentsCreateAgentRequest, CloudAgentsRunRequest
|
||||
|
||||
from examples.client import MODEL, client, run
|
||||
|
||||
# Org-unique: a fixed name collides with itself on the second run.
|
||||
NAME = f"example-greeter-{time.time_ns()}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with client() as api:
|
||||
agents = AgentsAPIApi(api)
|
||||
|
||||
created = agents.cloud_agents_controller_create(
|
||||
CloudAgentsCreateAgentRequest(
|
||||
name=NAME,
|
||||
model=MODEL,
|
||||
description="Created by the hanzoai SDK agent example.",
|
||||
instructions="You greet the user in one short sentence.",
|
||||
)
|
||||
)
|
||||
print("created:", created)
|
||||
|
||||
run_result = agents.cloud_agents_controller_run(
|
||||
NAME, CloudAgentsRunRequest(input="Greet a new Hanzo user.")
|
||||
)
|
||||
print("run:", run_result)
|
||||
|
||||
print("read back:", agents.cloud_agents_controller_get(NAME))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run(main)
|
||||
@@ -0,0 +1,38 @@
|
||||
"""chat — one completion.
|
||||
|
||||
``POST /v1/chat/completions`` (operationId ``ai_createChatCompletion``). The
|
||||
route is OpenAI-compatible, so the request and response shapes are the ones you
|
||||
already know; only the base URL and the key change.
|
||||
|
||||
``stream`` is left False on purpose. Streaming is a different transport (SSE)
|
||||
that the generated client returns as an opaque body, so a streaming example
|
||||
here would teach the wrong thing.
|
||||
|
||||
python -m examples.chat
|
||||
"""
|
||||
|
||||
from hanzoai.cloud import AiChatCompletionRequest, AiChatMessage, OpenAICompatibleApi
|
||||
|
||||
from examples.client import MODEL, client, run
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with client() as api:
|
||||
completion = OpenAICompatibleApi(api).ai_create_chat_completion(
|
||||
AiChatCompletionRequest(
|
||||
model=MODEL,
|
||||
messages=[AiChatMessage(role="user", content="Say hello in exactly five words.")],
|
||||
)
|
||||
)
|
||||
|
||||
choices = completion.choices or []
|
||||
if not choices:
|
||||
raise SystemExit("no choices returned")
|
||||
print(choices[0].message.content if choices[0].message else "(empty message)")
|
||||
|
||||
if completion.usage:
|
||||
print(f"tokens: {completion.usage.prompt_tokens} in / {completion.usage.completion_tokens} out")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run(main)
|
||||
@@ -0,0 +1,58 @@
|
||||
"""The one place an example learns where the API is and who it is.
|
||||
|
||||
Every flow imports this and nothing else builds a client, so there is a single
|
||||
answer to "which base URL?" and "which env var?" across all six.
|
||||
|
||||
Run any flow from the repo root::
|
||||
|
||||
python -m examples.hello
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from hanzoai.cloud import ApiClient, Configuration
|
||||
from hanzoai.cloud.exceptions import ApiException
|
||||
|
||||
#: Default host. ``HANZO_BASE_URL`` overrides it (staging, a local cloud, a tunnel).
|
||||
BASE_URL = os.environ.get("HANZO_BASE_URL", "https://api.hanzo.ai")
|
||||
|
||||
#: ``zen4`` is the flagship the spec documents as its own example value.
|
||||
MODEL = os.environ.get("HANZO_MODEL", "zen4")
|
||||
|
||||
|
||||
def api_key() -> str:
|
||||
"""Fail loudly and early when the key is absent.
|
||||
|
||||
Without this the SDK sends an unauthenticated request and the flow dies on a
|
||||
401 several frames deep, which reads like an API bug rather than an unset
|
||||
shell variable.
|
||||
"""
|
||||
key = os.environ.get("HANZO_API_KEY")
|
||||
if not key:
|
||||
raise SystemExit("HANZO_API_KEY is not set — export an IAM JWT or an hk- cloud key")
|
||||
return key
|
||||
|
||||
|
||||
def client() -> ApiClient:
|
||||
"""An ApiClient bound to the configured host and bearer key.
|
||||
|
||||
``access_token`` becomes ``Authorization: Bearer <key>``, which is the only
|
||||
scheme the spec declares.
|
||||
"""
|
||||
return ApiClient(Configuration(host=BASE_URL, access_token=api_key()))
|
||||
|
||||
|
||||
def run(main) -> None:
|
||||
"""Invoke a flow and report an API failure the way a caller can act on.
|
||||
|
||||
ApiException stringifies to the status line alone; the server's explanation
|
||||
is in ``.body``, which is the part worth printing.
|
||||
"""
|
||||
try:
|
||||
main()
|
||||
except ApiException as e:
|
||||
print(f"HTTP {e.status}: {e.body}", file=sys.stderr)
|
||||
raise SystemExit(1) from e
|
||||
@@ -0,0 +1,34 @@
|
||||
"""hello — who am I?
|
||||
|
||||
The smallest complete call: prove the key works and print the identity behind
|
||||
it. ``GET /v1/ai/account`` (operationId ``ai_getAccount``).
|
||||
|
||||
This is also where you meet the envelope, because every ``/v1/ai/*`` route uses
|
||||
it: ``status`` is the OUTCOME, not the HTTP code. A handled failure comes back
|
||||
as HTTP 200 with ``status='error'`` and a human ``msg``, so the client does NOT
|
||||
raise and code that only checks for an exception reports success on a failure.
|
||||
Branch on ``status``. Every AI-plane example in this repo does.
|
||||
|
||||
python -m examples.hello
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from hanzoai.cloud import AccountApi
|
||||
|
||||
from examples.client import BASE_URL, client, run
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with client() as api:
|
||||
envelope = AccountApi(api).ai_get_account()
|
||||
|
||||
if envelope.status != "ok":
|
||||
raise SystemExit(f"account read failed: {envelope.msg or 'no message'}")
|
||||
|
||||
print(f"hello from {BASE_URL}")
|
||||
print(json.dumps(envelope.data, indent=2, default=str))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run(main)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""money — what is in the wallet, and what spent it.
|
||||
|
||||
GET /v1/billing/balance billing_billingBalance prepaid credit balance
|
||||
GET /v1/billing/usage billing_billingUsage per-request usage ledger
|
||||
|
||||
Neither call takes an org: both derive the tenant SERVER-side from the JWT
|
||||
``owner`` claim, so a key can only ever read its own money. There is no org
|
||||
argument to pass and no ``X-Org-Id`` header to set.
|
||||
|
||||
python -m examples.money
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from hanzoai.cloud import BillingApi
|
||||
|
||||
from examples.client import client, run
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with client() as api:
|
||||
billing = BillingApi(api)
|
||||
|
||||
print("balance:", billing.billing_billing_balance().to_str())
|
||||
|
||||
# Unbounded, the ledger is every request the org ever made. Ask for a window.
|
||||
end = datetime.now(timezone.utc)
|
||||
start = end - timedelta(days=7)
|
||||
usage = billing.billing_billing_usage(
|
||||
start=start.isoformat(),
|
||||
end=end.isoformat(),
|
||||
)
|
||||
print("usage (last 7d):", usage.to_str())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run(main)
|
||||
@@ -0,0 +1,38 @@
|
||||
"""store — a KV round-trip: set, read it back, delete.
|
||||
|
||||
PUT /v1/kv/keys/{key} kv_setKey
|
||||
GET /v1/kv/keys/{key} kv_getKey
|
||||
DELETE /v1/kv/keys/{key} kv_deleteKey
|
||||
|
||||
The delete runs in ``finally``, so a failed read still cleans up rather than
|
||||
leaving the key behind for the next run to collide with.
|
||||
|
||||
python -m examples.store
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from hanzoai.cloud import KeysApi, KvSetKeyRequest
|
||||
|
||||
from examples.client import client, run
|
||||
|
||||
KEY = f"examples/store/{time.time_ns()}"
|
||||
VALUE = "hello from the hanzoai SDK"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with client() as api:
|
||||
kv = KeysApi(api)
|
||||
|
||||
kv.kv_set_key(KEY, KvSetKeyRequest(value=VALUE, ttl=60))
|
||||
print(f"set {KEY}")
|
||||
|
||||
try:
|
||||
print("read back:", kv.kv_get_key(KEY).to_str())
|
||||
finally:
|
||||
kv.kv_delete_key(KEY)
|
||||
print(f"deleted {KEY}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run(main)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""tools — list the MCP tools this key can reach.
|
||||
|
||||
``POST /v1/automations/mcp`` (operationId ``automations_mcp``) is the JSON-RPC
|
||||
2.0 MCP surface in hanzo.yaml, and the only one with typed request/response
|
||||
schemas — ``method`` is an enum the model validates, so ``tools/lst`` is a
|
||||
pydantic ValidationError here rather than a -32601 at runtime.
|
||||
|
||||
JSON-RPC reports failure INSIDE a 200: a bad method comes back as
|
||||
``error={code, message}``, not an HTTP 4xx, so no exception is raised. Check
|
||||
``error`` before reading ``result``.
|
||||
|
||||
python -m examples.tools
|
||||
"""
|
||||
|
||||
from hanzoai.cloud import AutomationsMcpRequest, MCPApi
|
||||
|
||||
from examples.client import client, run
|
||||
|
||||
|
||||
def main() -> None:
|
||||
with client() as api:
|
||||
response = MCPApi(api).automations_mcp(
|
||||
AutomationsMcpRequest(jsonrpc="2.0", id=1, method="tools/list")
|
||||
)
|
||||
|
||||
if response.error:
|
||||
raise SystemExit(f"JSON-RPC {response.error.code}: {response.error.message}")
|
||||
|
||||
tools = (response.result or {}).get("tools", [])
|
||||
print(f"{len(tools)} tools")
|
||||
for tool in tools:
|
||||
print(f" {tool.get('name')} — {tool.get('description') or '(no description)'}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run(main)
|
||||
Reference in New Issue
Block a user