Add SystemTest Agent and Controller
This commit is contained in:
commit
53f523f187
31 files changed
+9150
No files matched your search
@@ -0,0 +1,124 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def utc_now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemoryItem:
|
||||
kind: str
|
||||
content: dict[str, Any]
|
||||
created_at: str = field(default_factory=utc_now)
|
||||
task_id: str | None = None
|
||||
step: int | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return {
|
||||
"kind": self.kind,
|
||||
"content": self.content,
|
||||
"created_at": self.created_at,
|
||||
"task_id": self.task_id,
|
||||
"step": self.step,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, payload: dict[str, Any]) -> "MemoryItem":
|
||||
return cls(
|
||||
kind=str(payload.get("kind") or "event"),
|
||||
content=payload.get("content") if isinstance(payload.get("content"), dict) else {},
|
||||
created_at=str(payload.get("created_at") or utc_now()),
|
||||
task_id=str(payload.get("task_id")) if payload.get("task_id") is not None else None,
|
||||
step=int(payload["step"]) if isinstance(payload.get("step"), int) else None,
|
||||
metadata=payload.get("metadata") if isinstance(payload.get("metadata"), dict) else {},
|
||||
)
|
||||
|
||||
|
||||
class Blackboard:
|
||||
"""
|
||||
Lightweight file-backed memory shared by the local Host/App agent layers.
|
||||
|
||||
It intentionally mirrors UFO's blackboard shape without pulling in the full
|
||||
dependency stack: requests, trajectories, observations, screenshots and facts.
|
||||
"""
|
||||
|
||||
DEFAULT_KINDS = ("requests", "trajectories", "observations", "screenshots", "facts")
|
||||
|
||||
def __init__(self, path: Path, max_items_per_kind: int = 200):
|
||||
self.path = path
|
||||
self.max_items_per_kind = max_items_per_kind
|
||||
self._content: dict[str, list[MemoryItem]] = {kind: [] for kind in self.DEFAULT_KINDS}
|
||||
self.load()
|
||||
|
||||
def load(self) -> None:
|
||||
if not self.path.exists():
|
||||
return
|
||||
try:
|
||||
raw = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return
|
||||
if not isinstance(raw, dict):
|
||||
return
|
||||
for kind in self.DEFAULT_KINDS:
|
||||
entries = raw.get(kind)
|
||||
if isinstance(entries, list):
|
||||
self._content[kind] = [
|
||||
MemoryItem.from_dict(item)
|
||||
for item in entries
|
||||
if isinstance(item, dict)
|
||||
][-self.max_items_per_kind :]
|
||||
|
||||
def save(self) -> None:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
payload = {
|
||||
kind: [item.to_dict() for item in items[-self.max_items_per_kind :]]
|
||||
for kind, items in self._content.items()
|
||||
}
|
||||
self.path.write_text(
|
||||
json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
def append(
|
||||
self,
|
||||
kind: str,
|
||||
content: dict[str, Any],
|
||||
*,
|
||||
task_id: str | None = None,
|
||||
step: int | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> MemoryItem:
|
||||
if kind not in self._content:
|
||||
self._content[kind] = []
|
||||
item = MemoryItem(
|
||||
kind=kind,
|
||||
content=content,
|
||||
task_id=task_id,
|
||||
step=step,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
self._content[kind].append(item)
|
||||
self._content[kind] = self._content[kind][-self.max_items_per_kind :]
|
||||
self.save()
|
||||
return item
|
||||
|
||||
def recent(self, kind: str, limit: int = 10) -> list[dict[str, Any]]:
|
||||
return [item.to_dict() for item in self._content.get(kind, [])[-limit:]]
|
||||
|
||||
def snapshot(self, limit: int = 5) -> dict[str, list[dict[str, Any]]]:
|
||||
return {kind: self.recent(kind, limit) for kind in self._content}
|
||||
|
||||
def summary(self) -> dict[str, Any]:
|
||||
return {
|
||||
"path": str(self.path),
|
||||
"counts": {kind: len(items) for kind, items in self._content.items()},
|
||||
"latest_trajectory": self.recent("trajectories", 1),
|
||||
}
|
||||
Reference in new issue
Block a user