feat: unify history tools and improve execution reliability
This commit is contained in:
1 parent
d23a4f6472
commit
2fea9865ee
116 files changed
+8291
-2503
No files matched your search
@@ -12,6 +12,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
@@ -301,6 +302,89 @@ class SessionRepository:
|
||||
result.setdefault(session_id, []).append(name)
|
||||
return result
|
||||
|
||||
def get_session_usage(self, session_id: str) -> dict[str, Any]:
|
||||
"""Aggregate measured LLM usage for one session.
|
||||
|
||||
Sums every ``llm_usage`` trace the token meter recorded (all agents,
|
||||
Flash and Pro alike) and reports the prompt size of the latest
|
||||
Operator/FlashRunner call as the live executor context. Also echoes
|
||||
the profile and Pro tuning the run was started with, read from the
|
||||
session's stored ``device_info``.
|
||||
"""
|
||||
from artemis.services.token_meter import (
|
||||
OPERATOR_CONTEXT_WINDOW_TOKENS,
|
||||
OPERATOR_NODE_NAMES,
|
||||
)
|
||||
|
||||
def _int(value: Any) -> int:
|
||||
try:
|
||||
return int(value or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
llm_calls = prompt = completion = total = cached = 0
|
||||
operator_context: int | None = None
|
||||
operator_context_at: float | None = None
|
||||
with db_session(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"SELECT payload, timestamp FROM traces WHERE session_id = ?"
|
||||
" AND type = 'llm_call' AND name = 'llm_usage' ORDER BY timestamp ASC",
|
||||
(session_id,),
|
||||
)
|
||||
for row in cursor.fetchall():
|
||||
raw = row["payload"]
|
||||
try:
|
||||
payload = json.loads(raw) if isinstance(raw, str) else raw
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
if not isinstance(payload, dict):
|
||||
continue
|
||||
p = _int(payload.get("prompt_tokens"))
|
||||
c = _int(payload.get("completion_tokens"))
|
||||
t = _int(payload.get("total_tokens")) or (p + c)
|
||||
if t <= 0:
|
||||
continue
|
||||
llm_calls += 1
|
||||
prompt += p
|
||||
completion += c
|
||||
total += t
|
||||
cached += _int(payload.get("cached_tokens"))
|
||||
node = str(payload.get("node") or "").strip().lower()
|
||||
if node in OPERATOR_NODE_NAMES:
|
||||
operator_context = _int(payload.get("context_base_tokens")) or p
|
||||
operator_context_at = row["timestamp"]
|
||||
|
||||
cursor.execute("SELECT device_info FROM sessions WHERE session_id = ?", (session_id,))
|
||||
session_row = cursor.fetchone()
|
||||
|
||||
profile: str | None = None
|
||||
run_tuning: dict[str, Any] | None = None
|
||||
if session_row and session_row["device_info"]:
|
||||
try:
|
||||
d_info = json.loads(session_row["device_info"])
|
||||
except (ValueError, TypeError):
|
||||
d_info = None
|
||||
if isinstance(d_info, dict):
|
||||
profile = d_info.get("profile") or None
|
||||
tuning = d_info.get("run_tuning")
|
||||
if isinstance(tuning, dict) and tuning:
|
||||
run_tuning = dict(tuning)
|
||||
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"llm_calls": llm_calls,
|
||||
"prompt_tokens": prompt,
|
||||
"completion_tokens": completion,
|
||||
"total_tokens": total,
|
||||
"cached_tokens": cached,
|
||||
"operator_context_tokens": operator_context,
|
||||
"operator_context_window_tokens": OPERATOR_CONTEXT_WINDOW_TOKENS,
|
||||
"operator_context_updated_at": operator_context_at,
|
||||
"profile": profile,
|
||||
"run_tuning": run_tuning,
|
||||
}
|
||||
|
||||
def get_session_by_id(self, session_id: str) -> dict[str, Any] | None:
|
||||
try:
|
||||
with db_session(self.db_path) as conn:
|
||||
|
||||
@@ -176,6 +176,15 @@ async def get_session_details(session_id: str):
|
||||
return dict(row)
|
||||
|
||||
|
||||
@router.get("/api/sessions/{session_id}/usage")
|
||||
async def get_session_usage(session_id: str):
|
||||
"""Session-wide LLM token totals, live executor context size and run tuning."""
|
||||
try:
|
||||
return await asyncio.to_thread(session_repo.get_session_usage, session_id)
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/api/sessions/{session_id}/tree")
|
||||
async def get_tree(session_id: str):
|
||||
try:
|
||||
|
||||
@@ -397,15 +397,19 @@ class MediaService:
|
||||
or not segment_path.is_file()
|
||||
):
|
||||
continue
|
||||
segments.append(
|
||||
{
|
||||
"url": cls.path_to_video_url(segment_path),
|
||||
"start": float(item.get("start", 0)),
|
||||
"duration": float(item.get("duration", 0)),
|
||||
"width": int(item.get("width", 0)),
|
||||
"height": int(item.get("height", 0)),
|
||||
}
|
||||
)
|
||||
segment = {
|
||||
"url": cls.path_to_video_url(segment_path),
|
||||
"start": float(item.get("start", 0)),
|
||||
"duration": float(item.get("duration", 0)),
|
||||
"width": int(item.get("width", 0)),
|
||||
"height": int(item.get("height", 0)),
|
||||
}
|
||||
# Manifest v2 carries the real session-relative timeline so the
|
||||
# UI can seek by step time across scrcpy restart gaps.
|
||||
if item.get("offset_ms") is not None and item.get("duration_ms") is not None:
|
||||
segment["offset_ms"] = int(item["offset_ms"])
|
||||
segment["duration_ms"] = int(item["duration_ms"])
|
||||
segments.append(segment)
|
||||
return segments
|
||||
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
|
||||
return []
|
||||
@@ -535,30 +539,43 @@ class MediaService:
|
||||
return f"Error reading task plan: {e}"
|
||||
return "No task plan created yet."
|
||||
|
||||
@staticmethod
|
||||
def _read_jsonl_records(path: Path, label: str, session_id: str) -> list[dict[str, Any]]:
|
||||
records: list[dict[str, Any]] = []
|
||||
if not path.is_file():
|
||||
return records
|
||||
try:
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except ValueError:
|
||||
continue
|
||||
if isinstance(rec, dict):
|
||||
records.append(rec)
|
||||
except (OSError, ValueError) as e:
|
||||
logger.warning(f"Failed to read {label} for {session_id}: {e}")
|
||||
return records
|
||||
|
||||
@staticmethod
|
||||
def get_session_checks(session_id: str) -> dict[str, Any]:
|
||||
"""Checker material of one session for UI backfill: the append-only
|
||||
verdict ledger (``check_ledger.jsonl``) and the machine-readable run
|
||||
outcome (``run_outcome.json``), both written by
|
||||
``artemis.graph.checkpoints``. Missing files mean "no checks ran".
|
||||
verdict ledger (``check_ledger.jsonl``), the per-attempt transcripts of
|
||||
the Checker's streamed reasoning (``check_streams.jsonl``: one record
|
||||
per attempt with timestamped ``segments``) and the machine-readable run
|
||||
outcome (``run_outcome.json``), all written by the Checker /
|
||||
``artemis.graph.checkpoints``. Missing files mean "no checks ran"
|
||||
(sessions recorded before transcripts existed simply have no streams).
|
||||
"""
|
||||
session_dir = TRACES_PATH / session_id
|
||||
records: list[dict[str, Any]] = []
|
||||
ledger_path = session_dir / "check_ledger.jsonl"
|
||||
if ledger_path.is_file():
|
||||
try:
|
||||
for line in ledger_path.read_text(encoding="utf-8").splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
try:
|
||||
rec = json.loads(line)
|
||||
except ValueError:
|
||||
continue
|
||||
if isinstance(rec, dict):
|
||||
records.append(rec)
|
||||
except (OSError, ValueError) as e:
|
||||
logger.warning(f"Failed to read check ledger for {session_id}: {e}")
|
||||
records = MediaService._read_jsonl_records(
|
||||
session_dir / "check_ledger.jsonl", "check ledger", session_id
|
||||
)
|
||||
streams = MediaService._read_jsonl_records(
|
||||
session_dir / "check_streams.jsonl", "check streams", session_id
|
||||
)
|
||||
|
||||
run_outcome: dict[str, Any] | None = None
|
||||
outcome_path = session_dir / "run_outcome.json"
|
||||
@@ -570,7 +587,7 @@ class MediaService:
|
||||
except (OSError, ValueError) as e:
|
||||
logger.warning(f"Failed to read run outcome for {session_id}: {e}")
|
||||
|
||||
return {"records": records, "run_outcome": run_outcome}
|
||||
return {"records": records, "streams": streams, "run_outcome": run_outcome}
|
||||
|
||||
@staticmethod
|
||||
def get_session_notes_content(session_id: str) -> dict[str, str]:
|
||||
|
||||
Reference in new issue
Block a user