refactor: unify skills+cli runtime and streamline ag-ui flow

This commit is contained in:
qzl
2026-04-22 17:09:37 +08:00
parent eeed737949
commit 4d55df45ab
111 changed files with 4858 additions and 3264 deletions
@@ -1,7 +1,6 @@
from __future__ import annotations
import base64
import os
from pathlib import Path
from uuid import UUID, uuid4
@@ -9,27 +8,34 @@ import httpx
import pytest
from sqlalchemy import select
from core.config.settings import config
from core.db.session import AsyncSessionLocal
from models.agent_chat_message import AgentChatMessage
from models.agent_chat_session import AgentChatSession
from schemas.enums import AgentChatMessageRole
BASE_URL = os.getenv("AGENT_LIVE_BASE_URL", "http://localhost:5775")
BASE_URL = f"http://localhost:{5775}"
FIXTURE_IMAGE_PATH = (
Path(__file__).resolve().parents[3] / "fixtures" / "images" / "calendar_text_cn.png"
)
def _require_test_phone() -> str:
phone = config.test.phone
if not phone:
pytest.fail("SOCIAL_TEST__PHONE is required for live integration tests")
return phone
async def _live_access_token(client: httpx.AsyncClient) -> str:
phone = os.getenv("AGENT_LIVE_PHONE")
password = os.getenv("AGENT_LIVE_PASSWORD")
if not phone or not password:
pytest.fail(
"AGENT_LIVE_INTEGRATION=1 requires AGENT_LIVE_PHONE and AGENT_LIVE_PASSWORD"
)
phone = _require_test_phone()
if not phone.startswith("+"):
phone = f"+{phone}"
code = config.test.code or "000000"
response = await client.post(
f"{BASE_URL}/api/v1/auth/sessions",
json={"phone": phone, "password": password},
f"{BASE_URL}/api/v1/auth/phone-session",
json={"phone": phone, "token": code},
)
response_text = response.text.strip().replace("\n", " ")
truncated_text = response_text[:200]
@@ -48,8 +54,8 @@ async def _live_access_token(client: httpx.AsyncClient) -> str:
@pytest.mark.asyncio
@pytest.mark.live
async def test_agent_sse_closed_loop_live() -> None:
if os.getenv("AGENT_LIVE_INTEGRATION") != "1":
pytest.skip("set AGENT_LIVE_INTEGRATION=1 to run live integration test")
if config.runtime.environment not in {"dev", "test"}:
pytest.skip("live integration tests require dev or test environment")
async with httpx.AsyncClient(timeout=30.0) as client:
token = await _live_access_token(client)
@@ -67,7 +73,7 @@ async def test_agent_sse_closed_loop_live() -> None:
],
"tools": [],
"context": [],
"forwardedProps": {"agent_type": "worker"},
"forwardedProps": {"runtime_mode": "chat"},
},
)
assert run_resp.status_code == 202
@@ -110,8 +116,8 @@ async def test_agent_sse_closed_loop_live() -> None:
@pytest.mark.asyncio
@pytest.mark.live
async def test_agent_runs_events_history_live_with_image_input() -> None:
if os.getenv("AGENT_LIVE_INTEGRATION") != "1":
pytest.skip("set AGENT_LIVE_INTEGRATION=1 to run live integration test")
if config.runtime.environment not in {"dev", "test"}:
pytest.skip("live integration tests require dev or test environment")
image_data = base64.b64encode(FIXTURE_IMAGE_PATH.read_bytes()).decode("ascii")
@@ -143,7 +149,7 @@ async def test_agent_runs_events_history_live_with_image_input() -> None:
],
"tools": [],
"context": [],
"forwardedProps": {"agent_type": "worker"},
"forwardedProps": {"runtime_mode": "chat"},
},
)
assert run_resp.status_code == 202
@@ -221,3 +227,78 @@ async def test_agent_runs_events_history_live_with_image_input() -> None:
assert user_attachments
assert isinstance(user_attachments[0], dict)
assert isinstance(user_attachments[0].get("path"), str)
@pytest.mark.asyncio
@pytest.mark.live
async def test_agent_tool_call_result_persisted_live() -> None:
if config.runtime.environment not in {"dev", "test"}:
pytest.skip("live integration tests require dev or test environment")
thread_id = str(uuid4())
async with httpx.AsyncClient(timeout=30.0) as client:
token = await _live_access_token(client)
headers = {"Authorization": f"Bearer {token}"}
run_resp = await client.post(
f"{BASE_URL}/api/v1/agent/runs",
headers=headers,
json={
"threadId": thread_id,
"runId": "run-tool-verify-1",
"state": {},
"messages": [
{
"id": "u1",
"role": "user",
"content": "帮我查一下明天有哪些日程安排",
}
],
"tools": [],
"context": [],
"forwardedProps": {"runtime_mode": "chat"},
},
)
assert run_resp.status_code == 202
accepted = run_resp.json()
assert str(accepted["threadId"]) == thread_id
events_url = f"{BASE_URL}/api/v1/agent/runs/{thread_id}/events?runId=run-tool-verify-1"
event_names: list[str] = []
async with client.stream(
"GET", events_url, headers=headers, timeout=90.0
) as sse_resp:
assert sse_resp.status_code == 200
async for line in sse_resp.aiter_lines():
if line.startswith("event:"):
event_name = line.split(":", 1)[1].strip()
event_names.append(event_name)
if event_name in {"RUN_FINISHED", "RUN_ERROR"}:
break
assert "RUN_STARTED" in event_names, (
f"missing RUN_STARTED, got: {event_names}"
)
finished_ok = "RUN_FINISHED" in event_names
finished_err = "RUN_ERROR" in event_names
assert finished_ok or finished_err, (
f"no terminal event, got: {event_names}"
)
async with AsyncSessionLocal() as session:
rows = await session.execute(
select(AgentChatMessage).where(
AgentChatMessage.session_id == UUID(thread_id),
AgentChatMessage.role == AgentChatMessageRole.TOOL,
)
)
tool_messages = list(rows.scalars().all())
if finished_ok:
assert len(tool_messages) >= 1, (
f"expected >=1 role='tool' message but found {len(tool_messages)}. "
f"SSE events: {event_names}"
)