refactor: unify skills+cli runtime and streamline ag-ui flow
This commit is contained in:
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -33,13 +33,13 @@ def _make_job_response(
|
||||
status=overrides.get("status", "active"),
|
||||
is_system=overrides.get("is_system", False),
|
||||
config=overrides.get(
|
||||
"config",
|
||||
{
|
||||
"input_template": "Hello",
|
||||
"enabled_tools": [],
|
||||
"context": {
|
||||
"source": "latest_chat",
|
||||
"window_mode": "day",
|
||||
"config",
|
||||
{
|
||||
"input_template": "Hello",
|
||||
"enabled_skills": [],
|
||||
"context": {
|
||||
"source": "latest_chat",
|
||||
"window_mode": "day",
|
||||
"window_count": 2,
|
||||
},
|
||||
"schedule": {
|
||||
@@ -118,7 +118,7 @@ def test_create_automation_job_requires_auth() -> None:
|
||||
"timezone": "Asia/Shanghai",
|
||||
"config": {
|
||||
"input_template": "Hello",
|
||||
"enabled_tools": [],
|
||||
"enabled_skills": [],
|
||||
"context": {
|
||||
"source": "latest_chat",
|
||||
"window_mode": "day",
|
||||
@@ -161,7 +161,7 @@ def test_create_automation_job_succeeds() -> None:
|
||||
"status": "active",
|
||||
"config": {
|
||||
"input_template": "Hello",
|
||||
"enabled_tools": [],
|
||||
"enabled_skills": [],
|
||||
"context": {
|
||||
"source": "latest_chat",
|
||||
"window_mode": "day",
|
||||
@@ -205,7 +205,7 @@ def test_create_automation_job_respects_limit() -> None:
|
||||
"status": "active",
|
||||
"config": {
|
||||
"input_template": "Hello",
|
||||
"enabled_tools": [],
|
||||
"enabled_skills": [],
|
||||
"context": {
|
||||
"source": "latest_chat",
|
||||
"window_mode": "day",
|
||||
|
||||
Reference in New Issue
Block a user