feat: harden video analysis and runtime resilience
This commit is contained in:
1 parent
5ada543a09
commit
bb16991f2c
55 files changed
+5023
-437
No files matched your search
@@ -88,6 +88,43 @@ class SessionRepository:
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def get_latest_video_recordings_map(self) -> dict[str, dict[str, Any]]:
|
||||
"""Return the newest recording row for every session in one query.
|
||||
|
||||
The task-list endpoint renders every session at once. Calling
|
||||
``get_video_recording_for_session`` in that loop used to open a new
|
||||
SQLite connection for every row, which made a refresh progressively
|
||||
slower as history grew.
|
||||
"""
|
||||
try:
|
||||
with db_session(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='video_recordings'"
|
||||
)
|
||||
if cursor.fetchone() is None:
|
||||
return {}
|
||||
|
||||
rows = cursor.execute(
|
||||
"SELECT * FROM video_recordings WHERE session_id IS NOT NULL "
|
||||
"ORDER BY start_time DESC"
|
||||
).fetchall()
|
||||
latest: dict[str, dict[str, Any]] = {}
|
||||
for row in rows:
|
||||
recording = dict(row)
|
||||
session_id = str(recording.get("session_id") or "")
|
||||
if not session_id or session_id in latest:
|
||||
continue
|
||||
if "status" not in recording:
|
||||
recording["status"] = (
|
||||
"ready" if recording.get("end_time") is not None else "recording"
|
||||
)
|
||||
recording.setdefault("error", None)
|
||||
latest[session_id] = recording
|
||||
return latest
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
def mark_recording_failed_if_pending(self, session_id: str, error: str) -> bool:
|
||||
"""Close a recording lifecycle when its worker exits before finalization."""
|
||||
try:
|
||||
@@ -114,6 +151,40 @@ class SessionRepository:
|
||||
rows = cursor.fetchall()
|
||||
return [r[0] for r in rows if r and r[0]]
|
||||
|
||||
def get_llm_traces_for_profiles_map(
|
||||
self, session_ids: list[str] | None = None, limit: int = 3
|
||||
) -> dict[str, list[str]]:
|
||||
"""Return bounded LLM payloads for only the sessions needing fallback."""
|
||||
if session_ids is not None and not session_ids:
|
||||
return {}
|
||||
|
||||
session_filter = ""
|
||||
params: list[Any] = []
|
||||
if session_ids is not None:
|
||||
placeholders = ",".join("?" for _ in session_ids)
|
||||
session_filter = f" AND session_id IN ({placeholders})"
|
||||
params.extend(session_ids)
|
||||
params.append(limit)
|
||||
|
||||
with db_session(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT session_id, payload FROM ("
|
||||
" SELECT session_id, payload, ROW_NUMBER() OVER ("
|
||||
" PARTITION BY session_id ORDER BY timestamp ASC"
|
||||
" ) AS row_number FROM traces WHERE type = 'llm_call'"
|
||||
f"{session_filter}"
|
||||
") WHERE row_number <= ?",
|
||||
params,
|
||||
)
|
||||
result: dict[str, list[str]] = {}
|
||||
for row in cursor.fetchall():
|
||||
session_id = str(row[0] or "")
|
||||
payload = row[1]
|
||||
if session_id and payload:
|
||||
result.setdefault(session_id, []).append(payload)
|
||||
return result
|
||||
|
||||
def get_agent_trace_names(self, session_id: str) -> list[str]:
|
||||
try:
|
||||
with db_session(self.db_path) as conn:
|
||||
@@ -127,6 +198,23 @@ class SessionRepository:
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def get_agent_trace_names_map(self) -> dict[str, list[str]]:
|
||||
"""Return distinct agent/LLM trace names grouped by session."""
|
||||
with db_session(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT session_id, name FROM traces "
|
||||
"WHERE type IN ('agent', 'llm_call') AND session_id IS NOT NULL "
|
||||
"GROUP BY session_id, name"
|
||||
)
|
||||
result: dict[str, list[str]] = {}
|
||||
for row in cursor.fetchall():
|
||||
session_id = str(row[0] or "")
|
||||
name = row[1]
|
||||
if session_id and name:
|
||||
result.setdefault(session_id, []).append(name)
|
||||
return result
|
||||
|
||||
def get_session_by_id(self, session_id: str) -> dict[str, Any] | None:
|
||||
try:
|
||||
with db_session(self.db_path) as conn:
|
||||
|
||||
@@ -163,7 +163,12 @@ class StepRepository:
|
||||
co = (len(resp_text) // 4) + (resp_images * 258)
|
||||
return int(pr), int(co), int(pr + co)
|
||||
|
||||
def _clean_tool_payload(self, payload_raw: Any, trace_type: str | None = None) -> Any:
|
||||
def _clean_tool_payload(
|
||||
self,
|
||||
payload_raw: Any,
|
||||
trace_type: str | None = None,
|
||||
trace_name: str | None = None,
|
||||
) -> Any:
|
||||
if not payload_raw:
|
||||
return None
|
||||
payload_obj = payload_raw
|
||||
@@ -204,8 +209,18 @@ class StepRepository:
|
||||
)
|
||||
result_payload: dict[str, Any] = {"args": cleaned_args}
|
||||
|
||||
# Extract structured action execution results
|
||||
# Extract structured action execution results. Video analysis keeps its
|
||||
# result because the task stream needs to distinguish cached, partial,
|
||||
# recovering, waiting, completed, and terminal outcomes. Other tools
|
||||
# retain the historical compact payload to avoid sending large blobs.
|
||||
res = payload_obj.get("result")
|
||||
is_video_analysis = str(trace_name or "").lower() in {
|
||||
"video_analysis",
|
||||
"video_analyzer",
|
||||
"video_analyzer_pure",
|
||||
"spawn_sub_agent",
|
||||
"analyze_audio_only",
|
||||
}
|
||||
if isinstance(res, dict):
|
||||
if res.get("post_image_name"):
|
||||
result_payload["post_image_name"] = res["post_image_name"]
|
||||
@@ -213,8 +228,12 @@ class StepRepository:
|
||||
result_payload["pre_image_name"] = res["pre_image_name"]
|
||||
if res.get("outcome"):
|
||||
result_payload["outcome"] = res["outcome"]
|
||||
if is_video_analysis:
|
||||
result_payload["result"] = self._clean_value(res)
|
||||
elif res:
|
||||
res_str = str(res)
|
||||
if is_video_analysis:
|
||||
result_payload["result"] = res_str
|
||||
match = re.search(r"([a-f0-9]{64})", res_str)
|
||||
if match:
|
||||
result_payload["post_image_name"] = match.group(1)
|
||||
@@ -259,7 +278,7 @@ class StepRepository:
|
||||
return trace_dict
|
||||
|
||||
trace_dict["payload"] = self._clean_tool_payload(
|
||||
trace_dict.get("payload"), trace_dict.get("type")
|
||||
trace_dict.get("payload"), trace_dict.get("type"), trace_dict.get("name")
|
||||
)
|
||||
return trace_dict
|
||||
|
||||
|
||||
@@ -40,14 +40,18 @@ async def list_sessions():
|
||||
try:
|
||||
rows = session_repo.get_all_sessions()
|
||||
video_rec_map = session_repo.get_video_recordings_map()
|
||||
latest_recordings = session_repo.get_latest_video_recordings_map()
|
||||
agent_names_by_session = session_repo.get_agent_trace_names_map()
|
||||
video_idx = media_service.build_video_index()
|
||||
default_model_info = model_service.get_active_model_info()
|
||||
|
||||
orphaned_ids = []
|
||||
unresolved_profiles = []
|
||||
result = []
|
||||
|
||||
for row_dict in rows:
|
||||
s_id = str(row_dict.get("session_id"))
|
||||
recording = session_repo.get_video_recording_for_session(s_id)
|
||||
recording = latest_recordings.get(s_id)
|
||||
if row_dict.get("status") == "running":
|
||||
is_active = s_id in state.active_connections or (
|
||||
state.is_running and s_id == str(state.active_session_id)
|
||||
@@ -66,15 +70,36 @@ async def list_sessions():
|
||||
else None
|
||||
)
|
||||
|
||||
llm_traces = session_repo.get_llm_traces_for_profile(s_id)
|
||||
agent_names = session_repo.get_agent_trace_names(s_id)
|
||||
agent_names = agent_names_by_session.get(s_id, [])
|
||||
sess_profile = model_service.resolve_session_profile(
|
||||
row_dict, llm_traces, state.current_profile, agent_names=agent_names
|
||||
row_dict, None, state.current_profile, agent_names=agent_names
|
||||
)
|
||||
|
||||
row_dict["model_info"] = model_service.get_active_model_info(sess_profile)
|
||||
row_dict["model_info"] = (
|
||||
model_service.get_active_model_info(sess_profile)
|
||||
if sess_profile
|
||||
else default_model_info
|
||||
)
|
||||
if not sess_profile:
|
||||
unresolved_profiles.append(row_dict)
|
||||
result.append(row_dict)
|
||||
|
||||
# Most sessions carry a profile in device_info or an agent trace name.
|
||||
# Only inspect the much larger LLM payload for legacy rows that remain
|
||||
# ambiguous after those cheap checks.
|
||||
if unresolved_profiles:
|
||||
unresolved_ids = [str(row.get("session_id")) for row in unresolved_profiles]
|
||||
llm_traces_by_session = session_repo.get_llm_traces_for_profiles_map(unresolved_ids)
|
||||
for row_dict in unresolved_profiles:
|
||||
s_id = str(row_dict.get("session_id"))
|
||||
llm_traces = llm_traces_by_session.get(s_id)
|
||||
if not llm_traces:
|
||||
continue
|
||||
sess_profile = model_service.resolve_session_profile(
|
||||
row_dict, llm_traces, state.current_profile
|
||||
)
|
||||
if sess_profile:
|
||||
row_dict["model_info"] = model_service.get_active_model_info(sess_profile)
|
||||
|
||||
if orphaned_ids:
|
||||
try:
|
||||
harvested = session_repo.harvest_orphaned_sessions(orphaned_ids)
|
||||
|
||||
@@ -30,9 +30,9 @@ class SelectDeviceRequest(BaseModel):
|
||||
|
||||
|
||||
@router.get("/readiness", response_model=SystemReadinessReport)
|
||||
async def get_system_readiness() -> SystemReadinessReport:
|
||||
async def get_system_readiness(force: bool = False) -> SystemReadinessReport:
|
||||
"""Execute all diagnostic probes and return a comprehensive system readiness report."""
|
||||
return await readiness_engine.run_all()
|
||||
return await readiness_engine.run_all(force_refresh=force)
|
||||
|
||||
|
||||
@router.post("/devices/select")
|
||||
@@ -44,7 +44,7 @@ async def select_active_device(request: SelectDeviceRequest):
|
||||
|
||||
readiness_engine.set_active_device_serial(serial)
|
||||
# Return updated readiness
|
||||
report = await readiness_engine.run_all()
|
||||
report = await readiness_engine.run_all(force_refresh=True)
|
||||
return {
|
||||
"status": "success",
|
||||
"selected_serial": serial,
|
||||
@@ -56,7 +56,8 @@ async def select_active_device(request: SelectDeviceRequest):
|
||||
async def restart_adb_server():
|
||||
"""Restart local ADB server and return an updated readiness check."""
|
||||
restart_result = await readiness_engine.restart_adb_server()
|
||||
updated_report = await readiness_engine.run_all()
|
||||
readiness_engine.invalidate_cache()
|
||||
updated_report = await readiness_engine.run_all(force_refresh=True)
|
||||
return {
|
||||
"restart_result": restart_result,
|
||||
"report": updated_report,
|
||||
@@ -85,7 +86,8 @@ class ConnectAdbRequest(BaseModel):
|
||||
async def connect_wireless_adb(request: ConnectAdbRequest):
|
||||
"""Connect to a device over Wi-Fi and return updated readiness."""
|
||||
connect_result = await readiness_engine.connect_wireless_adb(request.host, request.port)
|
||||
updated_report = await readiness_engine.run_all()
|
||||
readiness_engine.invalidate_cache()
|
||||
updated_report = await readiness_engine.run_all(force_refresh=True)
|
||||
return {
|
||||
"connect_result": connect_result,
|
||||
"report": updated_report,
|
||||
@@ -222,7 +224,8 @@ async def update_credentials(request: UpdateCredentialsRequest):
|
||||
settings.set_api_key(provider, key, persist_to_env=request.persist_to_env)
|
||||
|
||||
# Re-run all diagnostic probes to build updated report
|
||||
updated_report = await readiness_engine.run_all()
|
||||
readiness_engine.invalidate_cache()
|
||||
updated_report = await readiness_engine.run_all(force_refresh=True)
|
||||
action_desc = (
|
||||
"successfully verified, updated, and applied" if key else "successfully cleared"
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user