feat: add official Codex skill and harden task execution
Install and update artemis-use with task diagnostics and plugin guidance. Preserve cross-device file receipts, review canonical reports, guard stage transitions and reduce small history summaries. Improve exploration lifecycle, request recovery and browser live connections.
This commit is contained in:
1 parent
2328a4df0c
commit
e1ea099592
128 files changed
+4944
-700
No files matched your search
@@ -2,6 +2,7 @@
|
||||
|
||||
from artemis.config.agent import load_agent_config
|
||||
from artemis.config.exploration import ExplorationConfig
|
||||
from artemis.exploration.case_prompt import require_resolved_case
|
||||
from artemis.exploration.models import CreateExploration, SaveCaseRequest, ValidateCaseRequest
|
||||
from artemis.exploration.demonstrations import Demonstrations
|
||||
from artemis.exploration.store import ExplorationConflict, ExplorationStore, explorations
|
||||
@@ -10,8 +11,7 @@ from apps.admin_console.services.task_import import TaskOptions
|
||||
|
||||
|
||||
def exploration_case_prompt(case: dict) -> str:
|
||||
prefix = f"前置条件:\n{case['preconditions']}\n\n" if case.get("preconditions") else ""
|
||||
return prefix + case["goal"]
|
||||
return require_resolved_case(case)
|
||||
|
||||
|
||||
class ExplorationService:
|
||||
|
||||
@@ -7,7 +7,9 @@ import time
|
||||
from apps.admin_console.database.connection import db_session
|
||||
|
||||
|
||||
def request_diagnostics(session_id: str, *, db_path=None, limit: int | None = None) -> dict:
|
||||
def request_diagnostics(
|
||||
session_id: str, *, db_path=None, limit: int | None = None, offset: int = 0
|
||||
) -> dict:
|
||||
with db_session(db_path) as conn:
|
||||
session = conn.execute(
|
||||
"SELECT status FROM sessions WHERE session_id = ?", (session_id,)
|
||||
@@ -17,9 +19,13 @@ def request_diagnostics(session_id: str, *, db_path=None, limit: int | None = No
|
||||
"ORDER BY timestamp DESC"
|
||||
)
|
||||
params: tuple = (session_id,)
|
||||
total = conn.execute(
|
||||
"SELECT count(*) FROM traces WHERE session_id=? AND type='llm_diagnostic'",
|
||||
params,
|
||||
).fetchone()[0]
|
||||
if limit is not None:
|
||||
sql += " LIMIT ?"
|
||||
params += (limit,)
|
||||
sql += " LIMIT ? OFFSET ?"
|
||||
params += (limit, offset)
|
||||
rows = conn.execute(sql, params).fetchall()
|
||||
requests = []
|
||||
seen = set()
|
||||
@@ -34,6 +40,7 @@ def request_diagnostics(session_id: str, *, db_path=None, limit: int | None = No
|
||||
request_id = value.get("request_id")
|
||||
value["active"] = (
|
||||
running
|
||||
and offset == 0
|
||||
and request_id not in seen
|
||||
and value.get("phase")
|
||||
in {
|
||||
@@ -64,6 +71,9 @@ def request_diagnostics(session_id: str, *, db_path=None, limit: int | None = No
|
||||
"session_status": session["status"] if session else "unknown",
|
||||
"collected_at": time.time(),
|
||||
"requests": requests,
|
||||
"total": total,
|
||||
"offset": offset,
|
||||
"next_offset": offset + limit if limit is not None and offset + limit < total else None,
|
||||
"note": "时间戳为 Unix 秒;timestamps_utc 为 UTC。HTTP 200 不代表完整输出。usage=null 表示服务未报告用量。历史任务没有记录的字段无法补推。",
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Read the impact of deleting a task without deleting or stopping anything."""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from artemis.exploration.store import ExplorationConflict, explorations
|
||||
from apps.admin_console.database.repositories.session_repository import session_repo
|
||||
from apps.admin_console.services.automation_store import store
|
||||
from apps.admin_console.services.exploration_deletion import _check_runs
|
||||
|
||||
|
||||
def deletion_preview(session_id: str) -> dict:
|
||||
session_id = str(UUID(session_id))
|
||||
run = explorations.run(session_id)
|
||||
if run and run["kind"] == "exploration":
|
||||
return exploration_deletion_preview(run["exploration_id"])
|
||||
if not session_repo.get_session_by_id(session_id) and not store.job(session_id):
|
||||
raise KeyError("任务不存在")
|
||||
_check_runs([{"id": session_id}])
|
||||
job = store.job(session_id) or {}
|
||||
if job.get("status") in {"pending", "queued", "running", "task_paused", "resuming"}:
|
||||
raise ExplorationConflict("任务仍在执行或排队,请先停止并等待结束")
|
||||
return {
|
||||
"session_ids": [session_id],
|
||||
"preserved_session_ids": [],
|
||||
"removes": ["任务历史", "报告", "截图、录像及附件"],
|
||||
}
|
||||
|
||||
|
||||
def exploration_deletion_preview(exploration_id: str) -> dict:
|
||||
session = explorations.get(exploration_id)
|
||||
runs = explorations.runs(exploration_id)
|
||||
_check_runs(runs)
|
||||
if session.get("active_run_id") or session.get("active_demonstration_id"):
|
||||
raise ExplorationConflict("探索尚有活动任务或示范,请先停止并等待结束")
|
||||
return {
|
||||
"exploration_id": exploration_id,
|
||||
"session_ids": [run["id"] for run in runs if run["kind"] == "exploration"],
|
||||
"preserved_session_ids": [run["id"] for run in runs if run["kind"] != "exploration"],
|
||||
"removes": ["探索对话", "用例版本", "探索执行历史及证据"],
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Bounded, read-only task diagnostics shared by MCP and HTTP clients."""
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from apps.admin_console.database.repositories.session_repository import session_repo
|
||||
from apps.admin_console.services import evidence_service, report_export
|
||||
from apps.admin_console.services.automation_store import identifier, store
|
||||
from apps.admin_console.services.request_diagnostics import request_diagnostics
|
||||
|
||||
|
||||
_SECRET = re.compile(
|
||||
r"(?i)^(api[_-]?key|access[_-]?token|refresh[_-]?token|authorization|password|secret|client_secret|token_key)$"
|
||||
)
|
||||
|
||||
|
||||
def public_value(value, *, max_chars: int = 60000):
|
||||
"""Remove common credential forms and bound text without inventing missing evidence."""
|
||||
remaining = max_chars
|
||||
remaining_nodes = 2000
|
||||
truncated = False
|
||||
secrets = [
|
||||
v
|
||||
for k, v in os.environ.items()
|
||||
if len(v) >= 8 and re.search(r"(?i)(api_?key|password|secret|token)$", k)
|
||||
]
|
||||
|
||||
def clean(item, depth=0):
|
||||
nonlocal remaining, remaining_nodes, truncated
|
||||
remaining_nodes -= 1
|
||||
if depth > 12:
|
||||
truncated = True
|
||||
return "[嵌套内容已截断]"
|
||||
if isinstance(item, dict):
|
||||
result = {}
|
||||
for k, v in item.items():
|
||||
if remaining_nodes <= 0 or len(result) >= 100:
|
||||
truncated = True
|
||||
break
|
||||
key = clean(str(k), depth + 1)
|
||||
result[key] = "[REDACTED]" if _SECRET.fullmatch(str(k)) else clean(v, depth + 1)
|
||||
return result
|
||||
if isinstance(item, list):
|
||||
result = []
|
||||
for v in item:
|
||||
if remaining_nodes <= 0 or len(result) >= 100:
|
||||
truncated = True
|
||||
break
|
||||
result.append(clean(v, depth + 1))
|
||||
return result
|
||||
if not isinstance(item, str):
|
||||
return item
|
||||
for secret in secrets:
|
||||
item = item.replace(secret, "[REDACTED]")
|
||||
item = re.sub(r"(?i)\bBearer\s+[^\s\"',;]+", "Bearer [REDACTED]", item)
|
||||
item = re.sub(r"\bsk-[A-Za-z0-9_-]+", "[REDACTED]", item)
|
||||
item = re.sub(r"(https?://)[^\s/@]+:[^\s/@]+@", r"\1[REDACTED]@", item)
|
||||
item = re.sub(
|
||||
r"""(?ix)((?:api[_-]?key|access[_-]?token|refresh[_-]?token|authorization|password|secret|token_key)["']?\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s&,;]+)""",
|
||||
r"\1[REDACTED]",
|
||||
item,
|
||||
)
|
||||
allowance = min(8000, remaining)
|
||||
if len(item) > allowance:
|
||||
truncated = True
|
||||
item = item[:allowance] + "…[已截断;按步骤或关键词缩小范围]"
|
||||
remaining = max(0, remaining - min(len(item), allowance))
|
||||
return item
|
||||
|
||||
result = clean(value)
|
||||
return {**result, "text_truncated": truncated, "content_truncated": truncated}
|
||||
|
||||
|
||||
def page(items: list, offset: int, limit: int) -> dict:
|
||||
return {
|
||||
"items": items[offset : offset + limit],
|
||||
"total": len(items),
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"next_offset": offset + limit if offset + limit < len(items) else None,
|
||||
}
|
||||
|
||||
|
||||
def inspect_task(session_id: str, view: str, *, offset: int = 0, limit: int = 20) -> dict:
|
||||
identifier(session_id)
|
||||
if offset < 0 or not 1 <= limit <= 100:
|
||||
raise ValueError("offset 必须非负,limit 必须在 1–100 之间")
|
||||
if not session_repo.get_session_by_id(session_id) and not store.job(session_id):
|
||||
raise LookupError("任务记录不存在")
|
||||
if view == "usage":
|
||||
result = session_repo.get_session_usage(session_id)
|
||||
result["cache_hit_rate"] = (
|
||||
result["cached_tokens"] / result["prompt_tokens"] if result["prompt_tokens"] else None
|
||||
)
|
||||
result["note"] = (
|
||||
"缓存率=已记录 cached_tokens / prompt_tokens;无用量不等于零消耗,未上报的请求无法补推。"
|
||||
)
|
||||
elif view == "requests":
|
||||
result = request_diagnostics(session_id, limit=limit, offset=offset)
|
||||
elif view == "logs":
|
||||
root = evidence_service.task_root(session_id)
|
||||
result = {"logs": []}
|
||||
for name in ("stdout.log", "stderr.log"):
|
||||
path = evidence_service.evidence_file(root, name)
|
||||
if path is None:
|
||||
result["logs"].append({"name": name, "available": False})
|
||||
continue
|
||||
with path.open("rb") as stream:
|
||||
size = path.stat().st_size
|
||||
start = max(0, size - 128 * 1024)
|
||||
stream.seek(start)
|
||||
raw = stream.read(128 * 1024)
|
||||
lines = raw.decode("utf-8", errors="replace").splitlines()
|
||||
if start and lines:
|
||||
lines = lines[1:]
|
||||
# Offset counts backward from the newest line within the bounded tail.
|
||||
end = max(0, len(lines) - offset)
|
||||
result["logs"].append(
|
||||
{
|
||||
"name": name,
|
||||
"available": True,
|
||||
"bytes": size,
|
||||
"lines": lines[max(0, end - limit) : end],
|
||||
"offset": offset,
|
||||
"next_offset": offset + limit if end > limit else None,
|
||||
"older_bytes_omitted": start,
|
||||
"order": "oldest_to_newest",
|
||||
}
|
||||
)
|
||||
elif view in {"report", "evidence"}:
|
||||
report = report_export.session_report(session_id)
|
||||
if view == "report":
|
||||
result = {**report, "steps": page(report.get("steps", []), offset, limit)}
|
||||
else:
|
||||
index = evidence_service.export_index(
|
||||
session_id, report, report_export.session_videos(report)
|
||||
)
|
||||
result = {
|
||||
**index,
|
||||
**{
|
||||
key: page(index.get(key, []), offset, limit)
|
||||
for key in ("steps", "calls", "recordings", "android_videos", "files")
|
||||
},
|
||||
}
|
||||
else:
|
||||
raise ValueError("view 需为 report / usage / requests / logs / evidence")
|
||||
return public_value({"session_id": session_id, **result})
|
||||
Reference in new issue
Block a user