Files
llm/scripts/smoke_onec_agent_api.py

256 lines
9.0 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import re
import sys
import uuid
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
def request_json(
method: str,
base_url: str,
path: str,
*,
payload: dict[str, Any] | None = None,
timeout: float = 5.0,
) -> tuple[int, dict[str, Any] | str, bool]:
body = json.dumps(payload).encode("utf-8") if payload is not None else None
req = Request(
f"{base_url}{path}",
data=body,
method=method,
headers={"Content-Type": "application/json"},
)
try:
with urlopen(req, timeout=timeout) as response:
raw = response.read().decode("utf-8")
if not raw:
return response.status, {}, False
return response.status, json.loads(raw), False
except HTTPError as exc:
raw = exc.read().decode("utf-8")
return exc.code, (json.loads(raw) if raw else {}), True
except URLError as exc:
return 0, str(exc), True
def assert_has_trace(payload: dict[str, Any]) -> None:
trace = payload.get("trace_id")
assert isinstance(trace, str), "trace_id is required"
assert re.fullmatch(r"[0-9a-f]{32}", trace), "trace_id format is invalid"
def unwrap(payload: dict[str, Any], wrapper: str) -> dict[str, Any]:
wrapped = payload.get(wrapper)
if isinstance(wrapped, dict):
return wrapped
return payload
def main() -> int:
parser = argparse.ArgumentParser(description="Smoke test for 1C agent API.")
parser.add_argument(
"--base-url",
default="http://docker-test.cin.su:8090",
help="Base API URL",
)
parser.add_argument(
"--timeout",
type=float,
default=5.0,
help="Request timeout in seconds",
)
parser.add_argument(
"--with-turn-check",
action="store_true",
help="Run /turn check (may be slower on first warm-up).",
)
parser.add_argument(
"--report",
type=Path,
help="Write JSON smoke report to file.",
)
args = parser.parse_args()
steps: list[dict[str, Any]] = []
project_id: str | None = None
chat_id: str | None = None
passed = 0
failed: list[dict[str, str]] = []
try:
status, health, is_error = request_json("GET", args.base_url, "/v1/health", timeout=args.timeout)
step = {"name": "health", "status": status, "error": is_error}
if not is_error:
assert status == 200 and health["status"] == "ok"
assert_has_trace(health)
step["service"] = health.get("service")
else:
raise AssertionError(f"Health request failed: {health}")
steps.append(step)
passed += 1
status, state, is_error = request_json("GET", args.base_url, "/v1/state", timeout=args.timeout)
step = {"name": "state", "status": status, "error": is_error}
if is_error:
raise AssertionError(f"State request failed: {state}")
assert status == 200
assert state.get("state", {}).get("projects", 0) >= 0
assert_has_trace(state)
step["project_count"] = state["state"]["projects"]
steps.append(step)
passed += 1
project_name = f"smoke-{uuid.uuid4()}"
status, project, is_error = request_json(
"POST",
args.base_url,
"/v1/projects",
payload={"name": project_name, "description": "agent smoke"},
timeout=args.timeout,
)
step = {"name": "create_project", "status": status, "error": is_error}
if is_error:
raise AssertionError(f"Create project failed: {project}")
assert status == 201 and "project" in project
project_id = project["project"]["id"]
assert_has_trace(project)
steps.append(step)
passed += 1
status, project_check, is_error = request_json(
"GET",
args.base_url,
f"/v1/projects/{project_id}",
timeout=args.timeout,
)
step = {"name": "get_project", "status": status, "error": is_error}
if is_error:
raise AssertionError(f"Get project failed: {project_check}")
assert status == 200
assert unwrap(project_check, "project")["id"] == project_id
assert_has_trace(project_check)
steps.append(step)
passed += 1
status, chat, is_error = request_json(
"POST",
args.base_url,
f"/v1/projects/{project_id}/chats",
payload={"title": "Диалог для smoke", "rag_profile": "default", "temperature": 0.12, "max_tokens": 128},
timeout=args.timeout,
)
step = {"name": "create_chat", "status": status, "error": is_error}
if is_error:
raise AssertionError(f"Create chat failed: {chat}")
assert status == 201 and "chat" in chat
chat_id = chat["chat"]["id"]
assert_has_trace(chat)
steps.append(step)
passed += 1
status, message, is_error = request_json(
"POST",
args.base_url,
f"/v1/projects/{project_id}/chats/{chat_id}/messages",
payload={"role": "user", "content": "Тестовое сообщение smoke"},
timeout=args.timeout,
)
step = {"name": "add_message", "status": status, "error": is_error}
if is_error:
raise AssertionError(f"Add message failed: {message}")
assert status == 201 and "message" in message
assert message["message"]["content"] == "Тестовое сообщение smoke"
assert_has_trace(message)
steps.append(step)
passed += 1
status, listed, is_error = request_json(
"GET",
args.base_url,
f"/v1/projects/{project_id}/chats/{chat_id}/messages?limit=10",
timeout=args.timeout,
)
step = {"name": "list_messages", "status": status, "error": is_error}
if is_error:
raise AssertionError(f"List messages failed: {listed}")
assert status == 200
assert len(listed.get("messages", [])) == 1
assert_has_trace(listed)
steps.append(step)
passed += 1
status, state, is_error = request_json(
"GET",
args.base_url,
f"/v1/projects/{project_id}/chats/{chat_id}",
timeout=args.timeout,
)
step = {"name": "get_chat", "status": status, "error": is_error}
if is_error:
raise AssertionError(f"Get chat failed: {state}")
assert status == 200
chat_payload = unwrap(state, "chat")
assert chat_payload["id"] == chat_id
assert_has_trace(state)
steps.append(step)
passed += 1
status, not_found, is_error = request_json(
"POST",
args.base_url,
"/v1/projects/does-not-exist/chats",
payload={"title": "fail-fast"},
timeout=args.timeout,
)
step = {"name": "negative_not_found_chat", "status": status, "error": is_error}
if not is_error or status != 404:
raise AssertionError(f"Expected 404 for unknown project, got {status} {not_found}")
assert_has_trace(not_found)
steps.append(step)
passed += 1
if args.with_turn_check:
status, turn, is_error = request_json(
"POST",
args.base_url,
f"/v1/projects/{project_id}/chats/{chat_id}/turn",
payload={"message": "Коротко: проверка маршрутизации.", "use_rag": False},
timeout=max(args.timeout, 60.0),
)
step = {"name": "turn", "status": status, "error": is_error}
if is_error:
raise AssertionError(f"Turn failed: {turn}")
assert status == 200
assert_has_trace(turn)
steps.append(step)
passed += 1
result = {"status": "ok", "passed": passed, "steps": steps}
if args.report:
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(result, ensure_ascii=False))
return 0
except Exception as exc:
failed.append({"error": str(exc), "at": steps[-1]["name"] if steps else "start"})
result = {"status": "failed", "passed": passed, "steps": steps, "errors": failed}
if args.report:
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps(result, ensure_ascii=False), file=sys.stderr)
return 1
finally:
if project_id is not None:
request_json("DELETE", args.base_url, f"/v1/projects/{project_id}", timeout=args.timeout)
if __name__ == "__main__":
raise SystemExit(main())