File size: 6,375 Bytes
f8dbde1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 | """Python SDK for a deg server (stdlib only).
```python
from deg.client import Deg
client = Deg("http://127.0.0.1:8790") # base_url; "/v1" is appended if missing
decision = client.decisions.create( # OpenAI Decisions API signature
model="deg-latest",
input="Order AB-1234 arrived broken. Please refund me.",
questions=[
{"type": "choice", "name": "event", "instructions": "Which event is this?",
"choices": [{"value": "refund_request"}, {"value": "shipping_issue"}]},
{"type": "extract", "name": "order_id", "instructions": "The order id."},
{"type": "generate", "name": "note", "instructions": "One-line note for the agent.",
"max_length": 200},
],
)
decision.answers[0].choice, decision.answers[1].value, decision.answers[2].text
client.systemone(state="...", questions={"urgent": {"type": "noul", "instructions": "..."}})
client.fill(context="...", schema={...}) # deg /v1/fill
```
Responses are :class:`Record` objects: plain ``dict`` subclasses whose keys are also
attributes (``decision.answers[0].probability``), so they serialize as the wire JSON.
HTTP errors raise :class:`DegAPIError` with ``status`` and the decoded ``body``.
The stock ``openai`` SDK (>= 3.26) also works against the same server:
``OpenAI(base_url="http://127.0.0.1:8790/v1", api_key="unused").decisions.create(...)``.
"""
from __future__ import annotations
import json
import os
import urllib.error
import urllib.request
from collections.abc import Iterable, Mapping
from typing import Any
DEFAULT_BASE_URL = "http://127.0.0.1:8790/v1"
DEFAULT_MODEL = "deg-latest"
class DegAPIError(RuntimeError):
def __init__(self, status: int, body: Any) -> None:
super().__init__(f"deg HTTP {status}: {json.dumps(body)[:500]}")
self.status = status
self.body = body
class Record(dict):
"""A response object: a dict whose keys are also attributes (recursively)."""
def __getattr__(self, name: str) -> Any:
try:
return self[name]
except KeyError:
raise AttributeError(name) from None
@classmethod
def wrap(cls, value: Any) -> Any:
if isinstance(value, dict):
return cls({k: cls.wrap(v) for k, v in value.items()})
if isinstance(value, list):
return [cls.wrap(v) for v in value]
return value
class Decisions:
def __init__(self, client: Deg) -> None:
self._client = client
def create(self, *, input: str | Iterable[Mapping[str, Any]], # noqa: A002 -- OpenAI name
questions: Iterable[Mapping[str, Any]], model: str = DEFAULT_MODEL,
safety_identifier: str | None = None, options: Mapping[str, Any] | None = None,
timeout: float | None = None) -> Record:
"""``POST /v1/decisions``, the OpenAI ``client.decisions.create`` signature (plus
deg's request ``options``)."""
body: dict[str, Any] = {"model": model,
"input": input if isinstance(input, str) else list(input),
"questions": list(questions)}
if safety_identifier is not None:
body["safety_identifier"] = safety_identifier
if options is not None:
body["options"] = dict(options)
return self._client._post("/decisions", body, timeout)
class Deg:
def __init__(self, base_url: str | None = None, *, api_key: str | None = None,
timeout: float = 300.0) -> None:
url = (base_url or os.environ.get("DEG_BASE_URL") or DEFAULT_BASE_URL).rstrip("/")
self.base_url = url if url.endswith("/v1") else url + "/v1"
self._api_key = api_key if api_key is not None else os.environ.get("DEG_API_KEY")
self.timeout = timeout
self.decisions = Decisions(self)
def __repr__(self) -> str: # never show the key
return f"Deg({self.base_url!r})"
def systemone(self, state: Any, questions: Mapping[str, Mapping[str, Any]], *,
model: str = DEFAULT_MODEL, options: Mapping[str, Any] | None = None,
timeout: float | None = None) -> Record:
"""``POST /v1/systemone`` (TypeSafe Jev request: state + questions by id)."""
body: dict[str, Any] = {"model": model, "state": state, "questions": dict(questions)}
if options is not None:
body["options"] = dict(options)
return self._post("/systemone", body, timeout)
def fill(self, context: Any, schema: Mapping[str, Any], *, model: str = DEFAULT_MODEL,
options: Mapping[str, Any] | None = None, timeout: float | None = None) -> Record:
"""``POST /v1/fill``: ``{model, value, meta, usage, plan}`` (SPEC.md §8)."""
body: dict[str, Any] = {"context": context, "schema": dict(schema), "model": model}
if options is not None:
body["options"] = dict(options)
return self._post("/fill", body, timeout)
def models(self) -> Record:
return self._request("GET", "/models", None, None)
def health(self) -> Record:
root = self.base_url[: -len("/v1")]
return self._request("GET", "/health", None, None, base=root)
def _post(self, path: str, body: dict[str, Any], timeout: float | None) -> Record:
return self._request("POST", path, body, timeout)
def _request(self, method: str, path: str, body: Any, timeout: float | None,
base: str | None = None) -> Record:
headers = {"Content-Type": "application/json", "Accept": "application/json"}
if self._api_key:
headers["Authorization"] = f"Bearer {self._api_key}"
data = None if body is None else json.dumps(body, ensure_ascii=False).encode("utf-8")
req = urllib.request.Request((base or self.base_url) + path, data=data,
headers=headers, method=method)
try:
with urllib.request.urlopen(req, timeout=timeout or self.timeout) as resp:
return Record.wrap(json.loads(resp.read()))
except urllib.error.HTTPError as exc:
raw = exc.read().decode("utf-8", "replace")
try:
err: Any = json.loads(raw)
except json.JSONDecodeError:
err = raw
raise DegAPIError(exc.code, err) from None
|