- pooled_devices primary key changed from device_id alone to (host_id, device_id), so two hosts reporting the same local device_id no longer crash sync_host_devices() with an uncaught sqlite3.IntegrityError. - Device gained a capability_tags field so DevicePool.sync_host_devices() can actually populate PooledDevice.capability_tags from a real host sync instead of always falling back to an empty list. openspec: device-pool capability, archived change cloud-runtime
183 lines
4.9 KiB
Python
183 lines
4.9 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from datetime import UTC, datetime
|
|
from typing import Any, Literal
|
|
from uuid import uuid4
|
|
|
|
DeviceStatus = Literal["idle", "busy", "offline", "error"]
|
|
TaskStatus = Literal["created", "running", "completed", "failed", "cancelled"]
|
|
StepStatus = Literal["pending", "running", "completed", "failed"]
|
|
|
|
|
|
def utc_now() -> datetime:
|
|
return datetime.now(UTC)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Bounds:
|
|
x: float
|
|
y: float
|
|
width: float
|
|
height: float
|
|
|
|
@property
|
|
def right(self) -> float:
|
|
return self.x + self.width
|
|
|
|
@property
|
|
def bottom(self) -> float:
|
|
return self.y + self.height
|
|
|
|
@property
|
|
def center(self) -> tuple[float, float]:
|
|
return (self.x + self.width / 2, self.y + self.height / 2)
|
|
|
|
def to_dict(self) -> dict[str, float]:
|
|
return {
|
|
"x": self.x,
|
|
"y": self.y,
|
|
"width": self.width,
|
|
"height": self.height,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> "Bounds":
|
|
return cls(
|
|
x=float(data["x"]),
|
|
y=float(data["y"]),
|
|
width=float(data["width"]),
|
|
height=float(data["height"]),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Device:
|
|
id: str
|
|
status: DeviceStatus = "idle"
|
|
name: str | None = None
|
|
driver_type: str = "wda"
|
|
connection_info: dict[str, Any] = field(default_factory=dict)
|
|
capability_tags: list[str] = field(default_factory=list)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"id": self.id,
|
|
"name": self.name,
|
|
"status": self.status,
|
|
"driver_type": self.driver_type,
|
|
"connection_info": dict(self.connection_info),
|
|
"capability_tags": list(self.capability_tags),
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class SceneElement:
|
|
id: str
|
|
type: str
|
|
bounds: Bounds
|
|
text: str | None = None
|
|
confidence: float | None = None
|
|
source: str | None = None
|
|
|
|
@property
|
|
def center(self) -> tuple[float, float]:
|
|
return self.bounds.center
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
data: dict[str, Any] = {
|
|
"id": self.id,
|
|
"type": self.type,
|
|
"text": self.text,
|
|
"bounds": self.bounds.to_dict(),
|
|
"confidence": self.confidence,
|
|
}
|
|
if self.source:
|
|
data["source"] = self.source
|
|
return data
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> "SceneElement":
|
|
return cls(
|
|
id=str(data["id"]),
|
|
type=str(data.get("type") or "unknown"),
|
|
text=data.get("text"),
|
|
bounds=Bounds.from_dict(data["bounds"]),
|
|
confidence=data.get("confidence"),
|
|
source=data.get("source"),
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Scene:
|
|
width: int
|
|
height: int
|
|
elements: list[SceneElement] = field(default_factory=list)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"screen": {"width": self.width, "height": self.height},
|
|
"elements": [element.to_dict() for element in self.elements],
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: dict[str, Any]) -> "Scene":
|
|
screen = data.get("screen") or {}
|
|
return cls(
|
|
width=int(screen.get("width") or data.get("width") or 0),
|
|
height=int(screen.get("height") or data.get("height") or 0),
|
|
elements=[
|
|
SceneElement.from_dict(element)
|
|
for element in data.get("elements", [])
|
|
],
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class Task:
|
|
goal: str
|
|
device_id: str
|
|
id: str = field(default_factory=lambda: uuid4().hex)
|
|
status: TaskStatus = "created"
|
|
created_at: datetime = field(default_factory=utc_now)
|
|
updated_at: datetime = field(default_factory=utc_now)
|
|
completed_at: datetime | None = None
|
|
failure_reason: str | None = None
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"id": self.id,
|
|
"goal": self.goal,
|
|
"device_id": self.device_id,
|
|
"status": self.status,
|
|
"created_at": self.created_at.isoformat(),
|
|
"updated_at": self.updated_at.isoformat(),
|
|
"completed_at": self.completed_at.isoformat()
|
|
if self.completed_at
|
|
else None,
|
|
"failure_reason": self.failure_reason,
|
|
}
|
|
|
|
|
|
@dataclass
|
|
class Step:
|
|
description: str
|
|
action: str
|
|
args: dict[str, Any] = field(default_factory=dict)
|
|
id: str = field(default_factory=lambda: uuid4().hex)
|
|
status: StepStatus = "pending"
|
|
result: dict[str, Any] | None = None
|
|
error: str | None = None
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"id": self.id,
|
|
"description": self.description,
|
|
"action": self.action,
|
|
"args": dict(self.args),
|
|
"status": self.status,
|
|
"result": self.result,
|
|
"error": self.error,
|
|
}
|
|
|