feat: checkpoint device agent runtime milestones
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from semantic.config import DEFAULT_MODEL
|
||||
from semantic.models import SEMANTIC_SCENE_SCHEMA, SemanticScene
|
||||
from semantic.prompts import ENRICHMENT_SYSTEM_PROMPT, scene_user_prompt
|
||||
|
||||
_SDK_UNAVAILABLE_EXCEPTION_NAMES = {
|
||||
"APITimeoutError",
|
||||
"APIConnectionError",
|
||||
"RateLimitError",
|
||||
"AuthenticationError",
|
||||
"APIStatusError",
|
||||
}
|
||||
|
||||
|
||||
class EnrichmentUnavailable(Exception):
|
||||
"""Internal signal for expected enrichment transport/response failures."""
|
||||
|
||||
|
||||
class AnthropicSemanticClient:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
model: str = DEFAULT_MODEL,
|
||||
transport: Any | None = None,
|
||||
max_tokens: int = 1024,
|
||||
) -> None:
|
||||
self.model = model
|
||||
self._transport = transport
|
||||
self.max_tokens = max_tokens
|
||||
|
||||
def enrich(self, scene_json: dict[str, Any], *, timeout: float) -> dict[str, Any]:
|
||||
try:
|
||||
response = self._create_message(scene_json, timeout=timeout)
|
||||
payload = _extract_response_body(response)
|
||||
return _validate_payload(payload)
|
||||
except EnrichmentUnavailable:
|
||||
raise
|
||||
except Exception as exc:
|
||||
raise EnrichmentUnavailable(str(exc)) from exc
|
||||
|
||||
def _create_message(self, scene_json: dict[str, Any], *, timeout: float) -> Any:
|
||||
client = self._client()
|
||||
kwargs = {
|
||||
"model": self.model,
|
||||
"max_tokens": self.max_tokens,
|
||||
"timeout": timeout,
|
||||
"system": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": ENRICHMENT_SYSTEM_PROMPT,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}
|
||||
],
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": scene_user_prompt(scene_json),
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
"output_config": {
|
||||
"format": {
|
||||
"type": "json_schema",
|
||||
"schema": SEMANTIC_SCENE_SCHEMA,
|
||||
}
|
||||
},
|
||||
}
|
||||
messages = getattr(client, "messages", None)
|
||||
if messages is not None:
|
||||
return messages.create(**kwargs)
|
||||
return client.create(**kwargs)
|
||||
|
||||
def _client(self) -> Any:
|
||||
if self._transport is not None:
|
||||
return self._transport
|
||||
|
||||
try:
|
||||
import anthropic
|
||||
except Exception as exc:
|
||||
raise EnrichmentUnavailable("anthropic SDK is unavailable") from exc
|
||||
|
||||
self._transport = anthropic.Anthropic()
|
||||
return self._transport
|
||||
|
||||
|
||||
def _extract_response_body(response: Any) -> dict[str, Any]:
|
||||
if _looks_like_semantic_scene(response):
|
||||
return response
|
||||
|
||||
for key in ("output", "parsed", "json"):
|
||||
value = _value(response, key)
|
||||
if _looks_like_semantic_scene(value):
|
||||
return value
|
||||
|
||||
content = _value(response, "content")
|
||||
if _looks_like_semantic_scene(content):
|
||||
return content
|
||||
if isinstance(content, str):
|
||||
return _decode_json(content)
|
||||
if isinstance(content, list):
|
||||
for block in content:
|
||||
for key in ("parsed", "json", "input", "content"):
|
||||
value = _value(block, key)
|
||||
if _looks_like_semantic_scene(value):
|
||||
return value
|
||||
text = _value(block, "text")
|
||||
if isinstance(text, str):
|
||||
return _decode_json(text)
|
||||
|
||||
raise ValueError("structured semantic response body not found")
|
||||
|
||||
|
||||
def _decode_json(text: str) -> dict[str, Any]:
|
||||
decoded = json.loads(text)
|
||||
if not isinstance(decoded, dict):
|
||||
raise ValueError("structured semantic response must be a JSON object")
|
||||
return decoded
|
||||
|
||||
|
||||
def _validate_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
||||
return SemanticScene.from_dict(payload).to_dict()
|
||||
|
||||
|
||||
def _looks_like_semantic_scene(value: Any) -> bool:
|
||||
return isinstance(value, dict) and {
|
||||
"page",
|
||||
"intents",
|
||||
"widgets",
|
||||
}.issubset(value)
|
||||
|
||||
|
||||
def _value(source: Any, key: str) -> Any:
|
||||
if isinstance(source, dict):
|
||||
return source.get(key)
|
||||
value = getattr(source, key, None)
|
||||
return None if callable(value) else value
|
||||
Reference in New Issue
Block a user