Files
2026-07-26 18:50:51 +08:00

256 lines
9.1 KiB
Python
Executable File

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import asyncio
import json
import os
import tempfile
import time
import uuid
from dataclasses import replace
from pathlib import Path
from typing import Any
from docker.errors import NotFound
from agent_platform.auth import UserIdentity
from agent_platform.config import Settings
from agent_platform.gateway.provider import DockerExecutionProvider, workspace_ref
from agent_platform.models import get_model_spec
from agent_platform.runtime.loop import AgentLoop
from agent_platform.runtime.provider import ModelProvider
from agent_platform.runtime.schemas import PlanItem
from agent_platform.runtime.tools import TOOL_METADATA, ToolContext
from agent_platform.store import RuntimeStore
EVAL_TOOL_NAMES = (
"workspace_status",
"list_files",
"read_file",
"search_files",
"write_file",
"apply_patch",
"exec",
"update_plan",
)
class DirectDockerRegistry:
def __init__(
self,
execution: DockerExecutionProvider,
store: RuntimeStore,
tool_timeout_seconds: int,
) -> None:
self.execution = execution
self.store = store
self.tool_timeout_seconds = tool_timeout_seconds
def specs(self, *, read_only: bool = False, allow_delegate: bool = True) -> list[dict[str, Any]]:
names = [name for name in EVAL_TOOL_NAMES if not read_only or TOOL_METADATA[name].read_only]
return [TOOL_METADATA[name].openai_spec() for name in names]
async def execute(
self,
name: str,
arguments: dict[str, Any],
context: ToolContext,
) -> dict[str, Any]:
user_id = context.identity.user_id
if name == "workspace_status":
result = await self.execution.status(user_id)
elif name == "list_files":
result = await self.execution.list_files(
user_id,
str(arguments.get("path", ".")),
int(arguments.get("max_depth", 4)),
int(arguments.get("limit", 500)),
)
elif name == "read_file":
result = await self.execution.read_file(
user_id,
str(arguments["path"]),
int(arguments.get("start_line", 1)),
int(arguments.get("max_lines", 1000)),
)
elif name == "search_files":
result = await self.execution.search_files(
user_id,
str(arguments["query"]),
str(arguments.get("path", ".")),
arguments.get("glob"),
int(arguments.get("limit", 200)),
)
elif name == "write_file":
result = await self.execution.write_file(
user_id,
str(arguments["path"]),
str(arguments["content"]),
)
elif name == "apply_patch":
result = await self.execution.apply_patch(
user_id,
str(arguments["patch"]),
str(arguments.get("cwd", ".")),
)
elif name == "exec":
result = await self.execution.exec(
user_id,
str(arguments["command"]),
str(arguments.get("cwd", ".")),
min(
int(arguments.get("timeout_seconds", 120)),
self.tool_timeout_seconds,
),
)
elif name == "update_plan":
items = [PlanItem.model_validate(item).model_dump() for item in arguments.get("items", [])]
await self.store.update_plan(user_id, context.chat_id, items)
return {
"ok": True,
"explanation": arguments.get("explanation"),
"items": items,
}
else:
raise ValueError(f"Unsupported evaluation tool: {name}")
return result.model_dump()
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Run the real Agent loop against a live provider in a disposable Docker workspace."
)
parser.add_argument("--model", default="deepseek-v4-pro")
parser.add_argument(
"--prompt",
default="写脚本对比一下几个常见排序算法,给一个报告",
)
parser.add_argument(
"--workspace-image",
default=os.getenv("WORKSPACE_IMAGE", "k1412-agent-workspace:test"),
)
parser.add_argument("--keep-workspace", action="store_true")
return parser.parse_args()
async def cleanup_workspace(execution: DockerExecutionProvider, user_id: str) -> None:
ref = workspace_ref(user_id)
try:
execution.client.containers.get(ref.container_name).remove(force=True)
except NotFound:
pass
try:
execution.client.volumes.get(ref.volume_name).remove(force=True)
except NotFound:
pass
try:
execution.client.networks.get(ref.network_name).remove()
except NotFound:
pass
async def main() -> None:
args = parse_args()
spec = get_model_spec(args.model)
if spec.provider == "deepseek" and not os.getenv("DEEPSEEK_API_KEY", "").strip():
raise SystemExit("DEEPSEEK_API_KEY is required")
user_id = f"live-eval-{uuid.uuid4().hex}"
chat_id = f"live-eval-{uuid.uuid4().hex}"
started = time.monotonic()
event_counts: dict[str, int] = {}
usage_totals = {
"prompt_tokens": 0,
"completion_tokens": 0,
"reasoning_tokens": 0,
"cached_tokens": 0,
"total_tokens": 0,
}
with tempfile.TemporaryDirectory(prefix="k1412-live-eval-") as temp_dir:
settings = replace(
Settings.from_env(),
database_url=f"sqlite+aiosqlite:///{Path(temp_dir) / 'runtime.sqlite3'}",
workspace_image=args.workspace_image,
workspace_network_enabled=False,
workspace_memory_limit="1g",
workspace_cpu_limit=1.0,
workspace_pids_limit=128,
tool_timeout_seconds=180,
)
store = RuntimeStore(settings.database_url)
await store.initialize()
provider = ModelProvider(settings)
execution = DockerExecutionProvider(settings)
registry = DirectDockerRegistry(execution, store, settings.tool_timeout_seconds)
async def record(event_type: str, payload: dict[str, Any]) -> None:
event_counts[event_type] = event_counts.get(event_type, 0) + 1
if event_type == "model.responded":
usage = payload.get("usage") or {}
prompt_details = usage.get("prompt_tokens_details") or {}
completion_details = usage.get("completion_tokens_details") or {}
usage_totals["prompt_tokens"] += int(usage.get("prompt_tokens", 0))
usage_totals["completion_tokens"] += int(usage.get("completion_tokens", 0))
usage_totals["reasoning_tokens"] += int(completion_details.get("reasoning_tokens", 0))
usage_totals["cached_tokens"] += int(
prompt_details.get("cached_tokens", 0) or usage.get("prompt_cache_hit_tokens", 0)
)
usage_totals["total_tokens"] += int(usage.get("total_tokens", 0))
if event_type == "tool.completed":
print(
json.dumps(
{
"event": event_type,
"name": payload.get("name"),
"ok": payload.get("ok"),
"summary": str(payload.get("summary", ""))[:500],
},
ensure_ascii=False,
),
flush=True,
)
try:
answer = await AgentLoop(
provider,
registry,
store,
max_tool_output_chars=settings.max_tool_output_chars,
).run(
spec=spec,
messages=[{"role": "user", "content": args.prompt}],
identity=UserIdentity(user_id, "", "Live Eval", "user"),
raw_user_jwt="local-evaluation",
chat_id=chat_id,
callback=record,
)
listing = await execution.list_files(user_id, ".", 4, 500)
print(
json.dumps(
{
"model": spec.public_id,
"provider_model": spec.provider_model,
"elapsed_seconds": round(time.monotonic() - started, 3),
"events": event_counts,
"usage": usage_totals,
"answer": answer,
"workspace": listing.model_dump(),
"workspace_id": workspace_ref(user_id).workspace_id if args.keep_workspace else None,
},
ensure_ascii=False,
indent=2,
)
)
finally:
await provider.close()
await store.close()
if not args.keep_workspace:
await cleanup_workspace(execution, user_id)
execution.client.close()
if __name__ == "__main__":
asyncio.run(main())