717 lines
25 KiB
Python
717 lines
25 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import subprocess
|
|
import uuid
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from .models import ContainerInfo, Deployment, Project, SessionInfo, TimelineEvent
|
|
from .trace_store import TraceStore
|
|
|
|
|
|
def _event_id() -> str:
|
|
return uuid.uuid4().hex[:12]
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now().isoformat(timespec="seconds")
|
|
|
|
|
|
def _data_dir(project: Project, deployment: Deployment) -> Path:
|
|
return (
|
|
Path(deployment.data_path)
|
|
if deployment.data_path
|
|
else Path(project.path) / "data"
|
|
)
|
|
|
|
|
|
def _task_events(
|
|
project: Project, deployment: Deployment
|
|
) -> list[tuple[str, Path, list[dict[str, Any]]]]:
|
|
task_dir = _data_dir(project, deployment) / "task_state"
|
|
if not task_dir.exists():
|
|
return []
|
|
sessions: list[tuple[str, Path, list[dict[str, Any]]]] = []
|
|
for path in sorted(task_dir.glob("*.events.jsonl")):
|
|
events: list[dict[str, Any]] = []
|
|
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
if not line.strip():
|
|
continue
|
|
try:
|
|
payload = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
payload = {"type": "raw", "message": line}
|
|
events.append(payload)
|
|
sessions.append((path.stem.removesuffix(".events"), path, events))
|
|
return sessions
|
|
|
|
|
|
def _expense_statuses(
|
|
project: Project, deployment: Deployment
|
|
) -> dict[str, dict[str, Any]]:
|
|
state_path = _data_dir(project, deployment) / "expenses.json"
|
|
if not state_path.exists():
|
|
return {}
|
|
try:
|
|
payload = json.loads(state_path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError:
|
|
return {}
|
|
expenses = payload.get("expenses", [])
|
|
if not isinstance(expenses, list):
|
|
return {}
|
|
return {
|
|
str(expense.get("id")): expense
|
|
for expense in expenses
|
|
if isinstance(expense, dict) and expense.get("id")
|
|
}
|
|
|
|
|
|
def _research_runs(project: Project, deployment: Deployment) -> list[dict[str, Any]]:
|
|
state_path = _data_dir(project, deployment) / "runs.json"
|
|
if not state_path.exists():
|
|
return []
|
|
try:
|
|
payload = json.loads(state_path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError:
|
|
return []
|
|
if not isinstance(payload, list):
|
|
return []
|
|
return [item for item in payload if isinstance(item, dict) and item.get("id")]
|
|
|
|
|
|
def list_sessions(
|
|
project: Project, deployment: Deployment, trace_store: TraceStore | None = None
|
|
) -> list[SessionInfo]:
|
|
results: list[SessionInfo] = []
|
|
captured_sessions = _captured_trace_sessions(trace_store, project, deployment)
|
|
used_captured_sessions: set[str] = set()
|
|
expense_statuses = _expense_statuses(project, deployment)
|
|
for expense_id, path, events in _task_events(project, deployment):
|
|
updated = datetime.fromtimestamp(path.stat().st_mtime).isoformat(
|
|
timespec="seconds"
|
|
)
|
|
expense = expense_statuses.get(expense_id, {})
|
|
trace_id, trace_url = _trace_info_from_events(events)
|
|
timeline_events = _semantic_events(
|
|
_events_from_payloads(expense_id, path, events)
|
|
)
|
|
captured_session = captured_sessions.get(expense_id)
|
|
if captured_session:
|
|
used_captured_sessions.add(expense_id)
|
|
timeline_events = []
|
|
trace_id = captured_session.trace_id or trace_id
|
|
trace_url = captured_session.trace_url or trace_url
|
|
if expense.get("review_running"):
|
|
status = "running"
|
|
else:
|
|
status = str(expense.get("status") or "completed")
|
|
if events:
|
|
joined = "\n".join(
|
|
str(event.get("message", "")) for event in events[-3:]
|
|
)
|
|
if "failed" in joined.lower() or "error" in joined.lower():
|
|
status = "failed"
|
|
results.append(
|
|
SessionInfo(
|
|
id=f"{deployment.id}:{expense_id}",
|
|
deployment_id=deployment.id,
|
|
project_id=project.id,
|
|
deployment_name=deployment.name,
|
|
project_name=project.name,
|
|
expense_id=expense_id,
|
|
status=status,
|
|
event_count=captured_session.event_count
|
|
if captured_session
|
|
else len(timeline_events),
|
|
trace_id=trace_id,
|
|
trace_url=trace_url,
|
|
started_at=updated,
|
|
updated_at=updated,
|
|
)
|
|
)
|
|
for run in _research_runs(project, deployment):
|
|
run_id = str(run.get("id"))
|
|
captured_session = captured_sessions.get(run_id)
|
|
if captured_session:
|
|
used_captured_sessions.add(run_id)
|
|
events = run.get("events", [])
|
|
event_count = len(events) if isinstance(events, list) else 0
|
|
if run.get("trace_id"):
|
|
event_count += 1
|
|
results.append(
|
|
SessionInfo(
|
|
id=f"{deployment.id}:{run_id}",
|
|
deployment_id=deployment.id,
|
|
project_id=project.id,
|
|
deployment_name=deployment.name,
|
|
project_name=project.name,
|
|
expense_id=run_id,
|
|
status=str(run.get("status") or "completed"),
|
|
event_count=captured_session.event_count
|
|
if captured_session
|
|
else event_count,
|
|
trace_id=captured_session.trace_id
|
|
if captured_session
|
|
else str(run.get("trace_id"))
|
|
if run.get("trace_id")
|
|
else None,
|
|
trace_url=captured_session.trace_url
|
|
if captured_session
|
|
else str(run.get("trace_url"))
|
|
if run.get("trace_url")
|
|
else None,
|
|
started_at=str(run.get("created_at") or ""),
|
|
updated_at=str(run.get("updated_at") or run.get("created_at") or ""),
|
|
)
|
|
)
|
|
for key, session in captured_sessions.items():
|
|
if key not in used_captured_sessions:
|
|
results.append(session)
|
|
return sorted(results, key=lambda item: item.updated_at or "", reverse=True)
|
|
|
|
|
|
def _trace_info_from_events(
|
|
events: list[dict[str, Any]],
|
|
) -> tuple[str | None, str | None]:
|
|
for event in events:
|
|
trace_id = event.get("trace_id") or event.get("traceId")
|
|
if trace_id:
|
|
trace_url = event.get("trace_url") or event.get("traceUrl")
|
|
return str(trace_id), str(trace_url) if trace_url else None
|
|
return None, None
|
|
|
|
|
|
def timeline_for_session(
|
|
project: Project,
|
|
deployment: Deployment,
|
|
expense_id: str,
|
|
trace_store: TraceStore | None = None,
|
|
) -> list[TimelineEvent]:
|
|
events: list[TimelineEvent] = _captured_timeline_for_session(
|
|
trace_store, project, deployment, expense_id
|
|
)
|
|
if not events:
|
|
for current_expense_id, path, payloads in _task_events(project, deployment):
|
|
if current_expense_id != expense_id:
|
|
continue
|
|
events = _semantic_events(_events_from_payloads(expense_id, path, payloads))
|
|
break
|
|
if not events:
|
|
for run in _research_runs(project, deployment):
|
|
if str(run.get("id")) != expense_id:
|
|
continue
|
|
timestamp = str(run.get("updated_at") or run.get("created_at") or _now())
|
|
trace_id = run.get("trace_id")
|
|
if trace_id:
|
|
events.append(
|
|
TimelineEvent(
|
|
id=f"{expense_id}-trace",
|
|
timestamp=timestamp,
|
|
source="trace",
|
|
type="trace",
|
|
message="Created OpenAI platform trace for this research run.",
|
|
metadata={
|
|
"trace_id": trace_id,
|
|
"trace_url": run.get("trace_url"),
|
|
"query": run.get("query"),
|
|
"mode": run.get("mode"),
|
|
},
|
|
)
|
|
)
|
|
run_events = run.get("events", [])
|
|
if isinstance(run_events, list):
|
|
for index, payload in enumerate(run_events, 1):
|
|
if not isinstance(payload, dict):
|
|
continue
|
|
label = str(payload.get("label") or payload.get("type") or "event")
|
|
status = str(payload.get("status") or "complete")
|
|
events.append(
|
|
TimelineEvent(
|
|
id=f"{expense_id}-run-{index}",
|
|
timestamp=timestamp,
|
|
source="agent",
|
|
type=label.lower(),
|
|
message=str(payload.get("message") or label),
|
|
level="error"
|
|
if status.lower() in {"error", "failed"}
|
|
else "info",
|
|
metadata={
|
|
"status": status,
|
|
"query": run.get("query"),
|
|
"mode": run.get("mode"),
|
|
"trace_id": trace_id,
|
|
"trace_url": run.get("trace_url"),
|
|
},
|
|
)
|
|
)
|
|
if run.get("summary"):
|
|
events.append(
|
|
TimelineEvent(
|
|
id=f"{expense_id}-summary",
|
|
timestamp=timestamp,
|
|
source="agent",
|
|
type="summary",
|
|
message=str(run.get("summary")),
|
|
metadata={
|
|
"query": run.get("query"),
|
|
"mode": run.get("mode"),
|
|
"trace_id": trace_id,
|
|
"trace_url": run.get("trace_url"),
|
|
},
|
|
)
|
|
)
|
|
break
|
|
if not events:
|
|
events.append(
|
|
TimelineEvent(
|
|
id=_event_id(),
|
|
timestamp=_now(),
|
|
source="session",
|
|
type="empty",
|
|
message="No session events have been recorded yet.",
|
|
)
|
|
)
|
|
return events
|
|
|
|
|
|
def _captured_traces(
|
|
trace_store: TraceStore | None, project: Project, deployment: Deployment
|
|
) -> dict[str, dict[str, Any]]:
|
|
if trace_store is None:
|
|
return {}
|
|
return trace_store.traces_for_deployment(deployment.id)
|
|
|
|
|
|
def _captured_trace_sessions(
|
|
trace_store: TraceStore | None, project: Project, deployment: Deployment
|
|
) -> dict[str, SessionInfo]:
|
|
sessions: dict[str, SessionInfo] = {}
|
|
for trace in _captured_traces(trace_store, project, deployment).values():
|
|
trace_id = str(trace.get("trace_id") or "")
|
|
if not trace_id:
|
|
continue
|
|
key = _captured_trace_session_key(trace)
|
|
events = _timeline_events_for_captured_trace(key, trace)
|
|
updated_at = str(
|
|
trace.get("ended_at") or _latest_trace_recorded_at(trace) or ""
|
|
)
|
|
started_at = str(trace.get("started_at") or updated_at)
|
|
sessions[key] = SessionInfo(
|
|
id=f"{deployment.id}:{key}",
|
|
deployment_id=deployment.id,
|
|
project_id=project.id,
|
|
deployment_name=deployment.name,
|
|
project_name=project.name,
|
|
expense_id=key,
|
|
status="completed" if trace.get("ended_at") else "running",
|
|
event_count=len(events),
|
|
trace_id=trace_id,
|
|
trace_url=_platform_trace_url(trace_id),
|
|
started_at=started_at,
|
|
updated_at=updated_at,
|
|
)
|
|
return sessions
|
|
|
|
|
|
def _captured_timeline_for_session(
|
|
trace_store: TraceStore | None,
|
|
project: Project,
|
|
deployment: Deployment,
|
|
session_key: str,
|
|
) -> list[TimelineEvent]:
|
|
for trace in _captured_traces(trace_store, project, deployment).values():
|
|
if session_key in {
|
|
_captured_trace_session_key(trace),
|
|
str(trace.get("trace_id") or ""),
|
|
}:
|
|
return _timeline_events_for_captured_trace(session_key, trace)
|
|
return []
|
|
|
|
|
|
def _captured_trace_session_key(trace: dict[str, Any]) -> str:
|
|
metadata = trace.get("metadata")
|
|
if isinstance(metadata, dict):
|
|
for key in ("expense_id", "run_id", "session_id"):
|
|
value = metadata.get(key)
|
|
if value:
|
|
return str(value)
|
|
if trace.get("group_id"):
|
|
return str(trace["group_id"])
|
|
return str(trace.get("trace_id") or "trace")
|
|
|
|
|
|
def _latest_trace_recorded_at(trace: dict[str, Any]) -> str | None:
|
|
records = trace.get("records")
|
|
if not isinstance(records, list):
|
|
return None
|
|
for record in reversed(records):
|
|
if isinstance(record, dict) and record.get("recorded_at"):
|
|
return str(record["recorded_at"])
|
|
return None
|
|
|
|
|
|
def _timeline_events_for_captured_trace(
|
|
session_key: str, trace: dict[str, Any]
|
|
) -> list[TimelineEvent]:
|
|
events: list[TimelineEvent] = []
|
|
span_order = trace.get("span_order", [])
|
|
spans = trace.get("spans", {})
|
|
if isinstance(span_order, list) and isinstance(spans, dict):
|
|
for span_id in span_order:
|
|
span = spans.get(span_id)
|
|
if not isinstance(span, dict):
|
|
continue
|
|
payload = span.get("end_payload") or span.get("payload")
|
|
if not isinstance(payload, dict):
|
|
continue
|
|
span_data = payload.get("span_data")
|
|
if not isinstance(span_data, dict):
|
|
span_data = {}
|
|
timestamp = str(
|
|
payload.get("started_at")
|
|
or span.get("started_recorded_at")
|
|
or span.get("ended_recorded_at")
|
|
or _latest_trace_recorded_at(trace)
|
|
or _now()
|
|
)
|
|
span_type = str(span_data.get("type") or "span")
|
|
name = _span_display_name(span_type, span_data)
|
|
metadata = {
|
|
"trace_id": trace.get("trace_id"),
|
|
"trace_url": _platform_trace_url(str(trace.get("trace_id"))),
|
|
"span_id": payload.get("id"),
|
|
"parent_id": payload.get("parent_id"),
|
|
"started_at": payload.get("started_at"),
|
|
"ended_at": payload.get("ended_at"),
|
|
"duration_ms": _duration_ms(
|
|
payload.get("started_at"), payload.get("ended_at")
|
|
),
|
|
"span_data": span_data,
|
|
"error": payload.get("error"),
|
|
}
|
|
events.append(
|
|
TimelineEvent(
|
|
id=str(payload.get("id") or f"{session_key}-{len(events) + 1}"),
|
|
timestamp=timestamp,
|
|
source=_span_source(span_type, span_data),
|
|
type=name,
|
|
message=_span_message(name, span_type, span_data),
|
|
level="error" if payload.get("error") else "info",
|
|
metadata={
|
|
key: value
|
|
for key, value in metadata.items()
|
|
if value is not None
|
|
},
|
|
)
|
|
)
|
|
if events:
|
|
return events
|
|
|
|
trace_id = str(trace.get("trace_id") or session_key)
|
|
return [
|
|
TimelineEvent(
|
|
id=f"{session_key}-trace",
|
|
timestamp=str(
|
|
trace.get("started_at") or _latest_trace_recorded_at(trace) or _now()
|
|
),
|
|
source="trace",
|
|
type="trace",
|
|
message=str(trace.get("workflow_name") or "OpenAI Agents trace"),
|
|
metadata={
|
|
"trace_id": trace_id,
|
|
"trace_url": _platform_trace_url(trace_id),
|
|
"group_id": trace.get("group_id"),
|
|
"metadata": trace.get("metadata"),
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
def _span_display_name(span_type: str, span_data: dict[str, Any]) -> str:
|
|
for key in ("name", "server_label", "tool_name"):
|
|
value = span_data.get(key)
|
|
if value:
|
|
return str(value)
|
|
return span_type
|
|
|
|
|
|
def _span_message(name: str, span_type: str, span_data: dict[str, Any]) -> str:
|
|
if span_type == "response":
|
|
return "POST /v1/responses"
|
|
if span_type == "generation":
|
|
return "LLM generation"
|
|
return name
|
|
|
|
|
|
def _span_source(span_type: str, span_data: dict[str, Any]) -> str:
|
|
name = str(span_data.get("name") or "")
|
|
if span_type in {"function", "mcp_tools"}:
|
|
return "tool"
|
|
if span_type in {"agent", "generation", "response", "handoff", "guardrail"}:
|
|
return "agent"
|
|
if span_type == "custom" and name in {"task", "turn"}:
|
|
return "session"
|
|
if span_type == "custom" and name.startswith("sandbox."):
|
|
return "sandbox"
|
|
if span_type in {"task", "turn"}:
|
|
return "session"
|
|
return "trace"
|
|
|
|
|
|
def _duration_ms(started_at: Any, ended_at: Any) -> int | None:
|
|
if not started_at or not ended_at:
|
|
return None
|
|
try:
|
|
started = datetime.fromisoformat(str(started_at).replace("Z", "+00:00"))
|
|
ended = datetime.fromisoformat(str(ended_at).replace("Z", "+00:00"))
|
|
except ValueError:
|
|
return None
|
|
return max(0, int((ended - started).total_seconds() * 1000))
|
|
|
|
|
|
def _platform_trace_url(trace_id: str) -> str:
|
|
return f"https://platform.openai.com/traces/trace?trace_id={trace_id}"
|
|
|
|
|
|
def _events_from_payloads(
|
|
expense_id: str, path: Path, payloads: list[dict[str, Any]]
|
|
) -> list[TimelineEvent]:
|
|
base_time = datetime.fromtimestamp(path.stat().st_mtime).isoformat(
|
|
timespec="seconds"
|
|
)
|
|
events: list[TimelineEvent] = []
|
|
for index, payload in enumerate(payloads, 1):
|
|
event_type = str(payload.get("type", "event"))
|
|
message = str(payload.get("message", event_type))
|
|
source = "session"
|
|
if "trace" in event_type:
|
|
source = "trace"
|
|
elif "tool" in event_type:
|
|
source = "tool"
|
|
elif "approval" in event_type:
|
|
source = "approval"
|
|
elif "sandbox" in event_type:
|
|
source = "sandbox"
|
|
events.append(
|
|
TimelineEvent(
|
|
id=f"{expense_id}-{index}",
|
|
timestamp=str(
|
|
payload.get("timestamp") or payload.get("created_at") or base_time
|
|
),
|
|
source=source,
|
|
type=event_type,
|
|
message=message,
|
|
level="error"
|
|
if "error" in event_type or "failed" in message.lower()
|
|
else "info",
|
|
metadata={
|
|
k: v for k, v in payload.items() if k not in {"type", "message"}
|
|
},
|
|
)
|
|
)
|
|
return events
|
|
|
|
|
|
def _semantic_events(events: list[TimelineEvent]) -> list[TimelineEvent]:
|
|
return _compact_tool_events(_compact_assistant_deltas(events))
|
|
|
|
|
|
def _compact_assistant_deltas(events: list[TimelineEvent]) -> list[TimelineEvent]:
|
|
compacted: list[TimelineEvent] = []
|
|
pending: list[TimelineEvent] = []
|
|
group_index = 0
|
|
|
|
def flush() -> None:
|
|
nonlocal group_index
|
|
if not pending:
|
|
return
|
|
group_index += 1
|
|
message = "".join(event.message for event in pending).strip()
|
|
if message:
|
|
compacted.append(
|
|
TimelineEvent(
|
|
id=f"{pending[0].id}-assistant-message-{group_index}",
|
|
timestamp=pending[0].timestamp,
|
|
source="agent",
|
|
type="model_response",
|
|
message=message,
|
|
metadata={
|
|
"delta_count": len(pending),
|
|
"event_name": "response_text_delta",
|
|
},
|
|
)
|
|
)
|
|
pending.clear()
|
|
|
|
for event in events:
|
|
if event.type == "assistant_delta":
|
|
pending.append(event)
|
|
continue
|
|
flush()
|
|
compacted.append(event)
|
|
flush()
|
|
return compacted
|
|
|
|
|
|
def _compact_tool_events(events: list[TimelineEvent]) -> list[TimelineEvent]:
|
|
compacted: list[TimelineEvent] = []
|
|
pending_index: dict[str, int] = {}
|
|
|
|
for event in events:
|
|
if event.source != "tool":
|
|
compacted.append(event)
|
|
continue
|
|
|
|
event_name = str(event.metadata.get("event_name", ""))
|
|
call_id = str(event.metadata.get("call_id", ""))
|
|
if event_name == "tool_called":
|
|
compacted.append(_tool_action_event(event))
|
|
if call_id:
|
|
pending_index[call_id] = len(compacted) - 1
|
|
continue
|
|
|
|
if event_name == "tool_output" and call_id and call_id in pending_index:
|
|
index = pending_index.pop(call_id)
|
|
compacted[index] = _merge_tool_action(compacted[index], event)
|
|
continue
|
|
|
|
compacted.append(_tool_action_event(event))
|
|
|
|
return compacted
|
|
|
|
|
|
def _tool_action_event(event: TimelineEvent) -> TimelineEvent:
|
|
tool_name = str(event.metadata.get("tool_name") or event.type or "tool")
|
|
event_name = str(event.metadata.get("event_name", ""))
|
|
action_state = "completed" if event_name == "tool_output" else "started"
|
|
metadata = dict(event.metadata)
|
|
return TimelineEvent(
|
|
id=event.id,
|
|
timestamp=event.timestamp,
|
|
source="tool",
|
|
type=tool_name,
|
|
message=f"{tool_name} {action_state}.",
|
|
level=event.level,
|
|
metadata=metadata,
|
|
)
|
|
|
|
|
|
def _merge_tool_action(
|
|
call_event: TimelineEvent, output_event: TimelineEvent
|
|
) -> TimelineEvent:
|
|
metadata = dict(call_event.metadata)
|
|
output_preview = output_event.metadata.get("output_preview")
|
|
if output_preview:
|
|
metadata["output_preview"] = output_preview
|
|
metadata["completed_at"] = output_event.timestamp
|
|
metadata["output_event_id"] = output_event.id
|
|
return TimelineEvent(
|
|
id=call_event.id,
|
|
timestamp=call_event.timestamp,
|
|
source="tool",
|
|
type=call_event.type,
|
|
message=f"{call_event.type} completed.",
|
|
level="error" if output_event.level == "error" else call_event.level,
|
|
metadata=metadata,
|
|
)
|
|
|
|
|
|
def docker_containers(
|
|
limit: int = 30, deployment_id: str | None = None
|
|
) -> list[ContainerInfo]:
|
|
command = [
|
|
"docker",
|
|
"ps",
|
|
"-a",
|
|
]
|
|
if deployment_id:
|
|
command.extend(["--filter", f"label=agents-sdk.deployment-id={deployment_id}"])
|
|
command.extend(
|
|
[
|
|
"--format",
|
|
"{{json .}}",
|
|
]
|
|
)
|
|
try:
|
|
completed = subprocess.run(
|
|
command,
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
timeout=5,
|
|
)
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return []
|
|
if completed.returncode != 0:
|
|
return []
|
|
containers: list[ContainerInfo] = []
|
|
for line in completed.stdout.splitlines()[:limit]:
|
|
if not line.strip():
|
|
continue
|
|
try:
|
|
payload = json.loads(line)
|
|
except json.JSONDecodeError:
|
|
continue
|
|
labels = _inspect_labels(str(payload.get("ID", "")))
|
|
containers.append(
|
|
ContainerInfo(
|
|
id=str(payload.get("ID", "")),
|
|
name=str(payload.get("Names", "")),
|
|
image=str(payload.get("Image", "")),
|
|
status=str(payload.get("Status", "")),
|
|
created_at=str(payload.get("CreatedAt", "")),
|
|
role=labels.get("agents-sdk.role")
|
|
or (
|
|
"sandbox"
|
|
if "python" in str(payload.get("Image", "")).lower()
|
|
else "observed"
|
|
),
|
|
labels=labels,
|
|
)
|
|
)
|
|
return containers
|
|
|
|
|
|
def docker_logs(container_id: str, limit: int = 200) -> str:
|
|
try:
|
|
completed = subprocess.run(
|
|
["docker", "logs", "--tail", str(limit), container_id],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
timeout=5,
|
|
)
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return ""
|
|
return (completed.stdout or "") + (completed.stderr or "")
|
|
|
|
|
|
def _inspect_labels(container_id: str) -> dict[str, str]:
|
|
if not container_id:
|
|
return {}
|
|
try:
|
|
completed = subprocess.run(
|
|
["docker", "inspect", container_id, "--format", "{{json .Config.Labels}}"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
timeout=5,
|
|
)
|
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
return {}
|
|
if completed.returncode != 0 or not completed.stdout.strip():
|
|
return {}
|
|
try:
|
|
labels = json.loads(completed.stdout)
|
|
except json.JSONDecodeError:
|
|
return {}
|
|
if not isinstance(labels, dict):
|
|
return {}
|
|
return {
|
|
str(key): str(value)
|
|
for key, value in labels.items()
|
|
if str(key).startswith("agents-sdk.")
|
|
}
|