refactor: 重命名 agent_chat 模块为 agent
This commit is contained in:
@@ -15,6 +15,7 @@ class AgUiEventTypeWire {
|
|||||||
static const toolCallEnd = 'TOOL_CALL_END';
|
static const toolCallEnd = 'TOOL_CALL_END';
|
||||||
static const toolCallResult = 'TOOL_CALL_RESULT';
|
static const toolCallResult = 'TOOL_CALL_RESULT';
|
||||||
static const toolCallError = 'TOOL_CALL_ERROR';
|
static const toolCallError = 'TOOL_CALL_ERROR';
|
||||||
|
static const messagesSnapshot = 'MESSAGES_SNAPSHOT';
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AgUiEventType {
|
enum AgUiEventType {
|
||||||
@@ -29,6 +30,7 @@ enum AgUiEventType {
|
|||||||
toolCallEnd,
|
toolCallEnd,
|
||||||
toolCallResult,
|
toolCallResult,
|
||||||
toolCallError,
|
toolCallError,
|
||||||
|
messagesSnapshot,
|
||||||
unknown,
|
unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ const _wireToTypeMap = {
|
|||||||
AgUiEventTypeWire.toolCallEnd: AgUiEventType.toolCallEnd,
|
AgUiEventTypeWire.toolCallEnd: AgUiEventType.toolCallEnd,
|
||||||
AgUiEventTypeWire.toolCallResult: AgUiEventType.toolCallResult,
|
AgUiEventTypeWire.toolCallResult: AgUiEventType.toolCallResult,
|
||||||
AgUiEventTypeWire.toolCallError: AgUiEventType.toolCallError,
|
AgUiEventTypeWire.toolCallError: AgUiEventType.toolCallError,
|
||||||
|
AgUiEventTypeWire.messagesSnapshot: AgUiEventType.messagesSnapshot,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _typeToWireMap = {
|
const _typeToWireMap = {
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from core.agent_chat.event_bridge import map_internal_event
|
from core.agent.event_bridge import map_internal_event
|
||||||
|
|
||||||
|
|
||||||
class AguiAdapter:
|
class AguiAdapter:
|
||||||
+1
-1
@@ -17,7 +17,7 @@ class CrewAITemplate:
|
|||||||
|
|
||||||
|
|
||||||
def _default_static_root() -> Path:
|
def _default_static_root() -> Path:
|
||||||
return Path(__file__).resolve().parents[3] / "config" / "static" / "agent_chat"
|
return Path(__file__).resolve().parents[3] / "config" / "static" / "agent"
|
||||||
|
|
||||||
|
|
||||||
def _read_yaml(file_path: Path) -> dict[str, Any]:
|
def _read_yaml(file_path: Path) -> dict[str, Any]:
|
||||||
@@ -5,7 +5,7 @@ import hashlib
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Protocol
|
from typing import Protocol
|
||||||
|
|
||||||
from core.agent_chat.storage_adapter import StorageAdapter
|
from core.agent.storage_adapter import StorageAdapter
|
||||||
|
|
||||||
_ALLOWED_MIME_TYPES = {
|
_ALLOWED_MIME_TYPES = {
|
||||||
"audio/mpeg",
|
"audio/mpeg",
|
||||||
+2
-2
@@ -4,8 +4,8 @@ import asyncio
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Awaitable, Callable
|
from typing import Any, Awaitable, Callable
|
||||||
|
|
||||||
from core.agent_chat.cost_tracker import CostTracker
|
from core.agent.cost_tracker import CostTracker
|
||||||
from core.agent_chat import events
|
from core.agent import events
|
||||||
|
|
||||||
StageCallable = Callable[..., Awaitable[dict[str, Any]]]
|
StageCallable = Callable[..., Awaitable[dict[str, Any]]]
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ def _default_catalog_path() -> Path:
|
|||||||
Path(__file__).resolve().parents[1]
|
Path(__file__).resolve().parents[1]
|
||||||
/ "config"
|
/ "config"
|
||||||
/ "static"
|
/ "static"
|
||||||
/ "agent_chat"
|
/ "agent"
|
||||||
/ "llm_catalog.yaml"
|
/ "llm_catalog.yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from core.auth.models import CurrentUser
|
from core.auth.models import CurrentUser
|
||||||
from core.db import get_db
|
from core.db import get_db
|
||||||
from v1.agent_chat.service import AgentChatService
|
from v1.agent.service import AgentChatService
|
||||||
from v1.profile.dependencies import get_current_user
|
from v1.profile.dependencies import get_current_user
|
||||||
|
|
||||||
|
|
||||||
def get_agent_chat_service(
|
def get_agent_service(
|
||||||
session: Annotated[AsyncSession, Depends(get_db)],
|
session: Annotated[AsyncSession, Depends(get_db)],
|
||||||
user: Annotated[CurrentUser, Depends(get_current_user)],
|
user: Annotated[CurrentUser, Depends(get_current_user)],
|
||||||
) -> AgentChatService:
|
) -> AgentChatService:
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Annotated
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from v1.agent.dependencies import get_agent_service
|
||||||
|
from v1.agent.schemas import AgentChatRunRequest, AgentChatRunResponse
|
||||||
|
from v1.agent.service import AgentChatService
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/agent", tags=["agent"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("", response_model=AgentChatRunResponse)
|
||||||
|
async def run_agent_chat(
|
||||||
|
payload: AgentChatRunRequest,
|
||||||
|
service: Annotated[AgentChatService, Depends(get_agent_service)],
|
||||||
|
) -> AgentChatRunResponse:
|
||||||
|
return await service.run(payload)
|
||||||
@@ -9,15 +9,15 @@ from fastapi import HTTPException
|
|||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, select
|
||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from core.agent_chat.agui_adapter import AguiAdapter
|
from core.agent.agui_adapter import AguiAdapter
|
||||||
from core.agent_chat.orchestrator import AgentChatOrchestrator
|
from core.agent.orchestrator import AgentChatOrchestrator
|
||||||
from core.auth.models import CurrentUser
|
from core.auth.models import CurrentUser
|
||||||
from core.db.base_service import BaseService
|
from core.db.base_service import BaseService
|
||||||
from core.logging import get_logger
|
from core.logging import get_logger
|
||||||
from models.agent_chat_message import AgentChatMessage, AgentChatMessageRole
|
from models.agent_chat_message import AgentChatMessage, AgentChatMessageRole
|
||||||
from models.agent_chat_session import AgentChatSession, AgentChatSessionStatus
|
from models.agent_chat_session import AgentChatSession, AgentChatSessionStatus
|
||||||
from v1.auth.rate_limit import enforce_rate_limit
|
from v1.auth.rate_limit import enforce_rate_limit
|
||||||
from v1.agent_chat.schemas import (
|
from v1.agent.schemas import (
|
||||||
AgentChatEvent,
|
AgentChatEvent,
|
||||||
AgentChatRunRequest,
|
AgentChatRunRequest,
|
||||||
AgentChatRunResponse,
|
AgentChatRunResponse,
|
||||||
@@ -26,7 +26,7 @@ from v1.agent_chat.schemas import (
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
logger = get_logger("v1.agent_chat.service")
|
logger = get_logger("v1.agent.service")
|
||||||
|
|
||||||
|
|
||||||
def build_session_title(first_message: str, *, now: datetime) -> str:
|
def build_session_title(first_message: str, *, now: datetime) -> str:
|
||||||
@@ -73,7 +73,7 @@ class AgentChatService(BaseService):
|
|||||||
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
raise HTTPException(status_code=422, detail=str(exc)) from exc
|
||||||
user_id = self.require_user_id()
|
user_id = self.require_user_id()
|
||||||
await enforce_rate_limit(
|
await enforce_rate_limit(
|
||||||
scope="agent_chat_run",
|
scope="agent_run",
|
||||||
identifier=str(user_id),
|
identifier=str(user_id),
|
||||||
limit=60,
|
limit=60,
|
||||||
window_seconds=60,
|
window_seconds=60,
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import Annotated
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
|
|
||||||
from v1.agent_chat.dependencies import get_agent_chat_service
|
|
||||||
from v1.agent_chat.schemas import AgentChatRunRequest, AgentChatRunResponse
|
|
||||||
from v1.agent_chat.service import AgentChatService
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/agent-chat", tags=["agent-chat"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("", response_model=AgentChatRunResponse)
|
|
||||||
async def run_agent_chat(
|
|
||||||
payload: AgentChatRunRequest,
|
|
||||||
service: Annotated[AgentChatService, Depends(get_agent_chat_service)],
|
|
||||||
) -> AgentChatRunResponse:
|
|
||||||
return await service.run(payload)
|
|
||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from core.http.models import HealthResponse
|
from core.http.models import HealthResponse
|
||||||
from v1.agent_chat.router import router as agent_chat_router
|
from v1.agent.router import router as agent_router
|
||||||
from v1.auth.router import router as auth_router
|
from v1.auth.router import router as auth_router
|
||||||
from v1.friendships.router import router as friendships_router
|
from v1.friendships.router import router as friendships_router
|
||||||
from v1.inbox_messages.router import router as inbox_messages_router
|
from v1.inbox_messages.router import router as inbox_messages_router
|
||||||
@@ -17,7 +17,7 @@ router.include_router(auth_router)
|
|||||||
router.include_router(friendships_router)
|
router.include_router(friendships_router)
|
||||||
router.include_router(infra_router)
|
router.include_router(infra_router)
|
||||||
router.include_router(users_router)
|
router.include_router(users_router)
|
||||||
router.include_router(agent_chat_router)
|
router.include_router(agent_router)
|
||||||
router.include_router(schedule_items_router)
|
router.include_router(schedule_items_router)
|
||||||
router.include_router(inbox_messages_router)
|
router.include_router(inbox_messages_router)
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,13 @@ from playwright.sync_api import sync_playwright
|
|||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from v1.agent_chat.dependencies import get_agent_chat_service
|
from v1.agent.dependencies import get_agent_service
|
||||||
from v1.agent_chat.schemas import (
|
from v1.agent.schemas import (
|
||||||
AgentChatEvent,
|
AgentChatEvent,
|
||||||
AgentChatRunRequest,
|
AgentChatRunRequest,
|
||||||
AgentChatRunResponse,
|
AgentChatRunResponse,
|
||||||
)
|
)
|
||||||
from v1.agent_chat.service import AgentChatService
|
from v1.agent.service import AgentChatService
|
||||||
|
|
||||||
|
|
||||||
class FakeE2EAgentChatService(AgentChatService):
|
class FakeE2EAgentChatService(AgentChatService):
|
||||||
@@ -66,7 +66,7 @@ def _start_server(host: str, port: int):
|
|||||||
|
|
||||||
|
|
||||||
def test_agent_chat_flow_e2e() -> None:
|
def test_agent_chat_flow_e2e() -> None:
|
||||||
app.dependency_overrides[get_agent_chat_service] = lambda: FakeE2EAgentChatService()
|
app.dependency_overrides[get_agent_service] = lambda: FakeE2EAgentChatService()
|
||||||
host = "127.0.0.1"
|
host = "127.0.0.1"
|
||||||
port = _find_free_port()
|
port = _find_free_port()
|
||||||
server, thread = _start_server(host, port)
|
server, thread = _start_server(host, port)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from decimal import Decimal
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from models.agent_chat_session import AgentChatSession, AgentChatSessionStatus
|
from models.agent_chat_session import AgentChatSession, AgentChatSessionStatus
|
||||||
from v1.agent_chat.service import select_recent_session
|
from v1.agent.service import select_recent_session
|
||||||
|
|
||||||
|
|
||||||
def test_recent_session_home_default_selection() -> None:
|
def test_recent_session_home_default_selection() -> None:
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import pytest
|
|||||||
from core.auth.models import CurrentUser
|
from core.auth.models import CurrentUser
|
||||||
from models.agent_chat_message import AgentChatMessage, AgentChatMessageRole
|
from models.agent_chat_message import AgentChatMessage, AgentChatMessageRole
|
||||||
from models.agent_chat_session import AgentChatSession, AgentChatSessionStatus
|
from models.agent_chat_session import AgentChatSession, AgentChatSessionStatus
|
||||||
from v1.agent_chat.schemas import AgentChatRunRequest
|
from v1.agent.schemas import AgentChatRunRequest
|
||||||
from v1.agent_chat.service import AgentChatService
|
from v1.agent.service import AgentChatService
|
||||||
|
|
||||||
|
|
||||||
class _FakeAsyncSession:
|
class _FakeAsyncSession:
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ from uuid import UUID
|
|||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
from app import app
|
from app import app
|
||||||
from v1.agent_chat.dependencies import get_agent_chat_service
|
from v1.agent.dependencies import get_agent_service
|
||||||
from v1.agent_chat.schemas import (
|
from v1.agent.schemas import (
|
||||||
AgentChatEvent,
|
AgentChatEvent,
|
||||||
AgentChatRunRequest,
|
AgentChatRunRequest,
|
||||||
AgentChatRunResponse,
|
AgentChatRunResponse,
|
||||||
)
|
)
|
||||||
from v1.agent_chat.service import AgentChatService
|
from v1.agent.service import AgentChatService
|
||||||
|
|
||||||
|
|
||||||
class FakeAgentChatService:
|
class FakeAgentChatService:
|
||||||
@@ -46,7 +46,7 @@ def _override_agent_chat_service(
|
|||||||
|
|
||||||
|
|
||||||
def test_run_route_returns_response() -> None:
|
def test_run_route_returns_response() -> None:
|
||||||
app.dependency_overrides[get_agent_chat_service] = _override_agent_chat_service(
|
app.dependency_overrides[get_agent_service] = _override_agent_chat_service(
|
||||||
FakeAgentChatService()
|
FakeAgentChatService()
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ def test_run_route_returns_response() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_run_route_validates_payload() -> None:
|
def test_run_route_validates_payload() -> None:
|
||||||
app.dependency_overrides[get_agent_chat_service] = _override_agent_chat_service(
|
app.dependency_overrides[get_agent_service] = _override_agent_chat_service(
|
||||||
FakeAgentChatService()
|
FakeAgentChatService()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from v1.agent_chat.service import aggregate_session_cost
|
from v1.agent.service import aggregate_session_cost
|
||||||
|
|
||||||
|
|
||||||
def test_aggregate_session_cost_sums_non_negative_values() -> None:
|
def test_aggregate_session_cost_sums_non_negative_values() -> None:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from decimal import Decimal
|
|||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from models.agent_chat_session import AgentChatSession, AgentChatSessionStatus
|
from models.agent_chat_session import AgentChatSession, AgentChatSessionStatus
|
||||||
from v1.agent_chat.service import select_recent_session
|
from v1.agent.service import select_recent_session
|
||||||
|
|
||||||
|
|
||||||
def test_select_recent_session_uses_last_activity_desc() -> None:
|
def test_select_recent_session_uses_last_activity_desc() -> None:
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.agent.agui_adapter import AguiAdapter
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_command_maps_payload_fields() -> None:
|
||||||
|
adapter = AguiAdapter()
|
||||||
|
|
||||||
|
command = adapter.to_command(
|
||||||
|
{
|
||||||
|
"message": "hello",
|
||||||
|
"session_id": "00000000-0000-0000-0000-000000000001",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert command["message"] == "hello"
|
||||||
|
assert command["session_id"] == "00000000-0000-0000-0000-000000000001"
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_protocol_event_maps_internal_event() -> None:
|
||||||
|
adapter = AguiAdapter()
|
||||||
|
|
||||||
|
mapped = adapter.to_protocol_event(
|
||||||
|
{
|
||||||
|
"kind": "run_completed",
|
||||||
|
"session_id": "run-1",
|
||||||
|
"output": "done",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mapped == {"type": "run.completed", "run_id": "run-1", "output": "done"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_protocol_event_raises_for_invalid_event() -> None:
|
||||||
|
adapter = AguiAdapter()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
adapter.to_protocol_event({"kind": "unknown"})
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.agent.tools.asr_fun_asr import FunASRTool
|
||||||
|
|
||||||
|
|
||||||
|
def test_transcribe_uses_injected_dashscope_callable() -> None:
|
||||||
|
def fake_transcribe(*, audio_bytes: bytes, filename: str) -> dict[str, str]:
|
||||||
|
assert filename == "voice.wav"
|
||||||
|
assert audio_bytes == b"audio"
|
||||||
|
return {"text": "你好", "request_id": "req-1"}
|
||||||
|
|
||||||
|
tool = FunASRTool(transcribe_callable=fake_transcribe)
|
||||||
|
|
||||||
|
result = tool.transcribe(audio_bytes=b"audio", filename="voice.wav")
|
||||||
|
|
||||||
|
assert result["text"] == "你好"
|
||||||
|
assert result["request_id"] == "req-1"
|
||||||
|
assert result["model"] == "fun-asr-realtime-2025-11-07"
|
||||||
|
|
||||||
|
|
||||||
|
def test_transcribe_raises_runtime_error_when_provider_fails() -> None:
|
||||||
|
def fake_transcribe(*, audio_bytes: bytes, filename: str) -> dict[str, str]:
|
||||||
|
raise RuntimeError("upstream timeout")
|
||||||
|
|
||||||
|
tool = FunASRTool(transcribe_callable=fake_transcribe)
|
||||||
|
|
||||||
|
with pytest.raises(RuntimeError):
|
||||||
|
tool.transcribe(audio_bytes=b"audio", filename="voice.wav")
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.agent.cost_tracker import CostTracker
|
||||||
|
|
||||||
|
|
||||||
|
def test_normalize_usage_and_cost_aggregation() -> None:
|
||||||
|
tracker = CostTracker()
|
||||||
|
|
||||||
|
tracker.add_usage(
|
||||||
|
{
|
||||||
|
"prompt_tokens": 7,
|
||||||
|
"completion_tokens": 5,
|
||||||
|
"cost": "0.002500",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
tracker.add_usage(
|
||||||
|
{
|
||||||
|
"input_tokens": 5,
|
||||||
|
"output_tokens": 3,
|
||||||
|
"cost": "0.003000",
|
||||||
|
"currency": "USD",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
snapshot = tracker.snapshot()
|
||||||
|
|
||||||
|
assert snapshot["input_tokens"] == 12
|
||||||
|
assert snapshot["output_tokens"] == 8
|
||||||
|
assert snapshot["total_tokens"] == 20
|
||||||
|
assert snapshot["cost"] == Decimal("0.005500")
|
||||||
|
assert snapshot["currency"] == "USD"
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_usage_rejects_negative_values() -> None:
|
||||||
|
tracker = CostTracker()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
tracker.add_usage({"input_tokens": -1})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
tracker.add_usage({"cost": "-0.010000"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_snapshot_is_zero_before_any_usage() -> None:
|
||||||
|
tracker = CostTracker()
|
||||||
|
|
||||||
|
snapshot = tracker.snapshot()
|
||||||
|
|
||||||
|
assert snapshot["input_tokens"] == 0
|
||||||
|
assert snapshot["output_tokens"] == 0
|
||||||
|
assert snapshot["total_tokens"] == 0
|
||||||
|
assert snapshot["cost"] == Decimal("0")
|
||||||
|
assert snapshot["currency"] == "USD"
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_usage_rejects_currency_mismatch() -> None:
|
||||||
|
tracker = CostTracker(currency="USD")
|
||||||
|
tracker.add_usage({"input_tokens": 1, "output_tokens": 1, "cost": "0.001000"})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
tracker.add_usage(
|
||||||
|
{
|
||||||
|
"input_tokens": 1,
|
||||||
|
"output_tokens": 1,
|
||||||
|
"cost": "0.001000",
|
||||||
|
"currency": "CNY",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_usage_rejects_non_integral_token_values() -> None:
|
||||||
|
tracker = CostTracker()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
tracker.add_usage({"input_tokens": 1.5})
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
tracker.add_usage({"output_tokens": True})
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.agent.event_bridge import map_internal_event
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_run_started_event() -> None:
|
||||||
|
event = {"kind": "run_started", "session_id": "s1"}
|
||||||
|
|
||||||
|
mapped = map_internal_event(event)
|
||||||
|
|
||||||
|
assert mapped == {"type": "run.started", "run_id": "s1"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_message_delta_event() -> None:
|
||||||
|
event = {"kind": "message_delta", "message_id": "m1", "delta": "hello"}
|
||||||
|
|
||||||
|
mapped = map_internal_event(event)
|
||||||
|
|
||||||
|
assert mapped == {"type": "message.delta", "message_id": "m1", "delta": "hello"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_tool_events() -> None:
|
||||||
|
started = {
|
||||||
|
"kind": "tool_started",
|
||||||
|
"message_id": "m2",
|
||||||
|
"tool_name": "asr_fun_asr",
|
||||||
|
}
|
||||||
|
completed = {
|
||||||
|
"kind": "tool_completed",
|
||||||
|
"message_id": "m2",
|
||||||
|
"tool_name": "asr_fun_asr",
|
||||||
|
"result": "ok",
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped_started = map_internal_event(started)
|
||||||
|
mapped_completed = map_internal_event(completed)
|
||||||
|
|
||||||
|
assert mapped_started["type"] == "tool.started"
|
||||||
|
assert mapped_started["tool_name"] == "asr_fun_asr"
|
||||||
|
assert mapped_completed["type"] == "tool.completed"
|
||||||
|
assert mapped_completed["result"] == "ok"
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_run_completed_event() -> None:
|
||||||
|
event = {"kind": "run_completed", "session_id": "s1", "output": "done"}
|
||||||
|
|
||||||
|
mapped = map_internal_event(event)
|
||||||
|
|
||||||
|
assert mapped == {"type": "run.completed", "run_id": "s1", "output": "done"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_unknown_event_raises() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
map_internal_event({"kind": "unknown"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_map_event_missing_required_field_raises_value_error() -> None:
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
map_internal_event({"kind": "message_delta", "message_id": "m1"})
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from core.agent.multimodal import AttachmentInput, MultimodalProcessor
|
||||||
|
from core.agent.storage_adapter import StorageAdapter
|
||||||
|
from core.agent.tools.asr_fun_asr import FunASRTool
|
||||||
|
|
||||||
|
|
||||||
|
def test_multimodal_processes_audio_and_builds_attachment_context() -> None:
|
||||||
|
storage = StorageAdapter(bucket="agent-chat-attachments")
|
||||||
|
|
||||||
|
def fake_transcribe(*, audio_bytes: bytes, filename: str) -> dict[str, str]:
|
||||||
|
assert audio_bytes == b"audio"
|
||||||
|
assert filename == "voice.wav"
|
||||||
|
return {"text": "hello world", "request_id": "req-1"}
|
||||||
|
|
||||||
|
processor = MultimodalProcessor(
|
||||||
|
storage=storage,
|
||||||
|
asr_tool=FunASRTool(transcribe_callable=fake_transcribe),
|
||||||
|
max_file_size_mb=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = processor.process(
|
||||||
|
user_id="u1",
|
||||||
|
session_id="s1",
|
||||||
|
message_seq=4,
|
||||||
|
attachments=[
|
||||||
|
AttachmentInput(
|
||||||
|
filename="voice.wav",
|
||||||
|
mime_type="audio/wav",
|
||||||
|
content=b"audio",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(result.attachments) == 1
|
||||||
|
metadata = result.attachments[0]
|
||||||
|
assert (
|
||||||
|
metadata["object_path"]
|
||||||
|
== "agent-chat/u1/s1/4/6ed8919ce20490a5e3ad8630a4fab69475297abd07db73918dd5f36fcfaeb11b.wav"
|
||||||
|
)
|
||||||
|
assert metadata["mime_type"] == "audio/wav"
|
||||||
|
assert result.preview_texts == ["hello world"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_multimodal_rejects_unsupported_mime_type() -> None:
|
||||||
|
storage = StorageAdapter(bucket="agent-chat-attachments")
|
||||||
|
processor = MultimodalProcessor(
|
||||||
|
storage=storage, asr_tool=FunASRTool(lambda **_: {})
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
processor.process(
|
||||||
|
user_id="u1",
|
||||||
|
session_id="s1",
|
||||||
|
message_seq=1,
|
||||||
|
attachments=[
|
||||||
|
AttachmentInput(
|
||||||
|
filename="malware.exe",
|
||||||
|
mime_type="application/octet-stream",
|
||||||
|
content=b"bad",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_multimodal_rejects_attachment_over_max_size() -> None:
|
||||||
|
storage = StorageAdapter(bucket="agent-chat-attachments")
|
||||||
|
processor = MultimodalProcessor(
|
||||||
|
storage=storage,
|
||||||
|
asr_tool=FunASRTool(lambda **_: {}),
|
||||||
|
max_file_size_mb=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
oversized = b"x" * (1024 * 1024 + 1)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
processor.process(
|
||||||
|
user_id="u1",
|
||||||
|
session_id="s1",
|
||||||
|
message_seq=1,
|
||||||
|
attachments=[
|
||||||
|
AttachmentInput(
|
||||||
|
filename="big.wav",
|
||||||
|
mime_type="audio/wav",
|
||||||
|
content=oversized,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from core.agent.orchestrator import AgentChatOrchestrator
|
||||||
|
|
||||||
|
|
||||||
|
async def _intent_stage(
|
||||||
|
*, message: str, context: dict[str, object]
|
||||||
|
) -> dict[str, object]:
|
||||||
|
sequence = context.setdefault("sequence", [])
|
||||||
|
if isinstance(sequence, list):
|
||||||
|
sequence.append("intent")
|
||||||
|
return {
|
||||||
|
"content": f"intent:{message}",
|
||||||
|
"usage": {"input_tokens": 2, "output_tokens": 1, "cost": "0.001000"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _execution_stage(
|
||||||
|
*, message: str, context: dict[str, object]
|
||||||
|
) -> dict[str, object]:
|
||||||
|
sequence = context.setdefault("sequence", [])
|
||||||
|
if isinstance(sequence, list):
|
||||||
|
sequence.append("execution")
|
||||||
|
return {
|
||||||
|
"content": f"execution:{message}",
|
||||||
|
"usage": {"input_tokens": 3, "output_tokens": 2, "cost": "0.002000"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _organization_stage(
|
||||||
|
*, message: str, context: dict[str, object]
|
||||||
|
) -> dict[str, object]:
|
||||||
|
sequence = context.setdefault("sequence", [])
|
||||||
|
if isinstance(sequence, list):
|
||||||
|
sequence.append("organization")
|
||||||
|
return {
|
||||||
|
"content": "final answer",
|
||||||
|
"usage": {"input_tokens": 4, "output_tokens": 1, "cost": "0.001500"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_orchestrator_runs_three_stages_in_order() -> None:
|
||||||
|
orchestrator = AgentChatOrchestrator(
|
||||||
|
intent_stage=_intent_stage,
|
||||||
|
execution_stage=_execution_stage,
|
||||||
|
organization_stage=_organization_stage,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = orchestrator.run_sync(run_id="run-1", user_message="hello")
|
||||||
|
|
||||||
|
assert result.context["sequence"] == ["intent", "execution", "organization"]
|
||||||
|
assert result.output == "final answer"
|
||||||
|
assert result.usage["total_tokens"] == 13
|
||||||
|
assert result.events[0]["type"] == "run.started"
|
||||||
|
assert result.events[-1]["type"] == "run.completed"
|
||||||
|
|
||||||
|
|
||||||
|
async def _failing_execution_stage(
|
||||||
|
*, message: str, context: dict[str, object]
|
||||||
|
) -> dict[str, object]:
|
||||||
|
sequence = context.setdefault("sequence", [])
|
||||||
|
if isinstance(sequence, list):
|
||||||
|
sequence.append("execution")
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
|
||||||
|
def test_orchestrator_stops_and_marks_failed_when_middle_stage_raises() -> None:
|
||||||
|
orchestrator = AgentChatOrchestrator(
|
||||||
|
intent_stage=_intent_stage,
|
||||||
|
execution_stage=_failing_execution_stage,
|
||||||
|
organization_stage=_organization_stage,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = orchestrator.run_sync(run_id="run-2", user_message="hello")
|
||||||
|
|
||||||
|
assert result.context["sequence"] == ["intent", "execution"]
|
||||||
|
assert result.events[-1]["type"] == "run.failed"
|
||||||
|
assert result.events[-1]["run_id"] == "run-2"
|
||||||
|
assert "boom" in (result.events[-1].get("error") or "")
|
||||||
|
assert result.failed is True
|
||||||
|
assert "boom" in (result.error or "")
|
||||||
|
|
||||||
|
|
||||||
|
def test_orchestrator_emits_stage_event_payload_shape() -> None:
|
||||||
|
orchestrator = AgentChatOrchestrator(
|
||||||
|
intent_stage=_intent_stage,
|
||||||
|
execution_stage=_execution_stage,
|
||||||
|
organization_stage=_organization_stage,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = orchestrator.run_sync(run_id="run-3", user_message="hello")
|
||||||
|
|
||||||
|
for event in result.events:
|
||||||
|
assert "type" in event
|
||||||
|
assert event.get("run_id") == "run-3"
|
||||||
|
|
||||||
|
stage_events = [
|
||||||
|
event for event in result.events if event["type"] == "stage.completed"
|
||||||
|
]
|
||||||
|
assert [event["stage"] for event in stage_events] == [
|
||||||
|
"intent",
|
||||||
|
"execution",
|
||||||
|
"organization",
|
||||||
|
]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from v1.agent.service import build_session_title
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_session_title_truncates_first_message() -> None:
|
||||||
|
now = datetime(2026, 2, 25, 10, 30)
|
||||||
|
|
||||||
|
title = build_session_title(
|
||||||
|
"这是一个非常长的标题会被截断到二十四个可见字符用于会话摘要", now=now
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(title) == 24
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_session_title_falls_back_when_message_empty() -> None:
|
||||||
|
now = datetime(2026, 2, 25, 10, 30)
|
||||||
|
|
||||||
|
title = build_session_title("\n ", now=now)
|
||||||
|
|
||||||
|
assert title == "新对话 2026-02-25 10:30"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from core.agent.storage_adapter import StorageAdapter
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_object_path_uses_expected_pattern() -> None:
|
||||||
|
adapter = StorageAdapter(bucket="agent-chat-attachments")
|
||||||
|
|
||||||
|
path = adapter.build_object_path(
|
||||||
|
user_id="u1",
|
||||||
|
session_id="s1",
|
||||||
|
message_seq=3,
|
||||||
|
checksum_sha256="abc123",
|
||||||
|
extension="wav",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert path == "agent-chat/u1/s1/3/abc123.wav"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_attachment_metadata_contains_required_fields() -> None:
|
||||||
|
adapter = StorageAdapter(bucket="agent-chat-attachments")
|
||||||
|
|
||||||
|
metadata = adapter.build_attachment_metadata(
|
||||||
|
object_path="agent-chat/u1/s1/3/abc123.wav",
|
||||||
|
mime_type="audio/wav",
|
||||||
|
size=1024,
|
||||||
|
checksum_sha256="abc123",
|
||||||
|
origin="user_upload",
|
||||||
|
preview_text="hello",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert metadata["object_path"] == "agent-chat/u1/s1/3/abc123.wav"
|
||||||
|
assert metadata["mime_type"] == "audio/wav"
|
||||||
|
assert metadata["size"] == 1024
|
||||||
|
assert metadata["checksum_sha256"] == "abc123"
|
||||||
|
assert metadata["origin"] == "user_upload"
|
||||||
|
assert metadata["preview_text"] == "hello"
|
||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.agent_chat.agui_adapter import AguiAdapter
|
from core.agent.agui_adapter import AguiAdapter
|
||||||
|
|
||||||
|
|
||||||
def test_to_command_maps_payload_fields() -> None:
|
def test_to_command_maps_payload_fields() -> None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.agent_chat.tools.asr_fun_asr import FunASRTool
|
from core.agent.tools.asr_fun_asr import FunASRTool
|
||||||
|
|
||||||
|
|
||||||
def test_transcribe_uses_injected_dashscope_callable() -> None:
|
def test_transcribe_uses_injected_dashscope_callable() -> None:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from decimal import Decimal
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.agent_chat.cost_tracker import CostTracker
|
from core.agent.cost_tracker import CostTracker
|
||||||
|
|
||||||
|
|
||||||
def test_normalize_usage_and_cost_aggregation() -> None:
|
def test_normalize_usage_and_cost_aggregation() -> None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.agent_chat.event_bridge import map_internal_event
|
from core.agent.event_bridge import map_internal_event
|
||||||
|
|
||||||
|
|
||||||
def test_map_run_started_event() -> None:
|
def test_map_run_started_event() -> None:
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.agent_chat.multimodal import AttachmentInput, MultimodalProcessor
|
from core.agent.multimodal import AttachmentInput, MultimodalProcessor
|
||||||
from core.agent_chat.storage_adapter import StorageAdapter
|
from core.agent.storage_adapter import StorageAdapter
|
||||||
from core.agent_chat.tools.asr_fun_asr import FunASRTool
|
from core.agent.tools.asr_fun_asr import FunASRTool
|
||||||
|
|
||||||
|
|
||||||
def test_multimodal_processes_audio_and_builds_attachment_context() -> None:
|
def test_multimodal_processes_audio_and_builds_attachment_context() -> None:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from core.agent_chat.orchestrator import AgentChatOrchestrator
|
from core.agent.orchestrator import AgentChatOrchestrator
|
||||||
|
|
||||||
|
|
||||||
async def _intent_stage(
|
async def _intent_stage(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from v1.agent_chat.service import build_session_title
|
from v1.agent.service import build_session_title
|
||||||
|
|
||||||
|
|
||||||
def test_build_session_title_truncates_first_message() -> None:
|
def test_build_session_title_truncates_first_message() -> None:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from core.agent_chat.storage_adapter import StorageAdapter
|
from core.agent.storage_adapter import StorageAdapter
|
||||||
|
|
||||||
|
|
||||||
def test_build_object_path_uses_expected_pattern() -> None:
|
def test_build_object_path_uses_expected_pattern() -> None:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from core.agent_chat.crewai.template_loader import (
|
from core.agent.crewai.template_loader import (
|
||||||
load_crewai_template,
|
load_crewai_template,
|
||||||
load_tools_whitelist,
|
load_tools_whitelist,
|
||||||
validate_workflow_stages,
|
validate_workflow_stages,
|
||||||
@@ -63,7 +63,7 @@ tools:
|
|||||||
|
|
||||||
|
|
||||||
def test_load_crewai_template_success_when_all_files_valid(tmp_path: Path) -> None:
|
def test_load_crewai_template_success_when_all_files_valid(tmp_path: Path) -> None:
|
||||||
static_root = _prepare_static_root(tmp_path / "agent_chat")
|
static_root = _prepare_static_root(tmp_path / "agent")
|
||||||
|
|
||||||
template = load_crewai_template(static_root)
|
template = load_crewai_template(static_root)
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ def test_load_crewai_template_success_when_all_files_valid(tmp_path: Path) -> No
|
|||||||
def test_load_crewai_template_raises_file_not_found_when_required_file_missing(
|
def test_load_crewai_template_raises_file_not_found_when_required_file_missing(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
static_root = _prepare_static_root(tmp_path / "agent_chat")
|
static_root = _prepare_static_root(tmp_path / "agent")
|
||||||
(static_root / "crewai" / "tasks.yaml").unlink()
|
(static_root / "crewai" / "tasks.yaml").unlink()
|
||||||
|
|
||||||
with pytest.raises(FileNotFoundError):
|
with pytest.raises(FileNotFoundError):
|
||||||
@@ -89,7 +89,7 @@ def test_load_crewai_template_raises_file_not_found_when_required_file_missing(
|
|||||||
def test_load_crewai_template_raises_value_error_when_workflow_stages_invalid(
|
def test_load_crewai_template_raises_value_error_when_workflow_stages_invalid(
|
||||||
tmp_path: Path,
|
tmp_path: Path,
|
||||||
) -> None:
|
) -> None:
|
||||||
static_root = _prepare_static_root(tmp_path / "agent_chat")
|
static_root = _prepare_static_root(tmp_path / "agent")
|
||||||
_write(
|
_write(
|
||||||
static_root / "crewai" / "workflow.yaml",
|
static_root / "crewai" / "workflow.yaml",
|
||||||
"""
|
"""
|
||||||
@@ -105,7 +105,7 @@ stages:
|
|||||||
|
|
||||||
|
|
||||||
def test_load_tools_whitelist_from_tools_yaml(tmp_path: Path) -> None:
|
def test_load_tools_whitelist_from_tools_yaml(tmp_path: Path) -> None:
|
||||||
static_root = _prepare_static_root(tmp_path / "agent_chat")
|
static_root = _prepare_static_root(tmp_path / "agent")
|
||||||
|
|
||||||
whitelist = load_tools_whitelist(static_root)
|
whitelist = load_tools_whitelist(static_root)
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ def test_validate_workflow_stages_rejects_extra_or_missing_stage() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def test_load_tools_whitelist_rejects_non_string_item(tmp_path: Path) -> None:
|
def test_load_tools_whitelist_rejects_non_string_item(tmp_path: Path) -> None:
|
||||||
static_root = _prepare_static_root(tmp_path / "agent_chat")
|
static_root = _prepare_static_root(tmp_path / "agent")
|
||||||
_write(
|
_write(
|
||||||
static_root / "tools.yaml",
|
static_root / "tools.yaml",
|
||||||
"""
|
"""
|
||||||
|
|||||||
+2
-2
@@ -19,7 +19,7 @@ def test_llm_catalog_file_exists_and_has_required_fields() -> None:
|
|||||||
/ "core"
|
/ "core"
|
||||||
/ "config"
|
/ "config"
|
||||||
/ "static"
|
/ "static"
|
||||||
/ "agent_chat"
|
/ "agent"
|
||||||
/ "llm_catalog.yaml"
|
/ "llm_catalog.yaml"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ def test_llm_catalog_file_exists_and_has_required_fields() -> None:
|
|||||||
assert len(catalog["factories"]) == 6
|
assert len(catalog["factories"]) == 6
|
||||||
assert len(catalog["llms"]) == 2
|
assert len(catalog["llms"]) == 2
|
||||||
assert set(catalog["factories"][0].keys()) == {"name", "request_url", "avatar"}
|
assert set(catalog["factories"][0].keys()) == {"name", "request_url", "avatar"}
|
||||||
assert set(catalog["llms"][0].keys()) == {"model_code", "factory_id"}
|
assert set(catalog["llms"][0].keys()) == {"model_code", "factory_name"}
|
||||||
|
|
||||||
|
|
||||||
def test_load_llm_catalog_raises_on_invalid_structure(tmp_path: Path) -> None:
|
def test_load_llm_catalog_raises_on_invalid_structure(tmp_path: Path) -> None:
|
||||||
@@ -10,12 +10,12 @@ from fastapi import HTTPException
|
|||||||
from sqlalchemy.exc import SQLAlchemyError
|
from sqlalchemy.exc import SQLAlchemyError
|
||||||
|
|
||||||
from core.auth.models import CurrentUser
|
from core.auth.models import CurrentUser
|
||||||
from core.agent_chat.orchestrator import OrchestratorResult
|
from core.agent.orchestrator import OrchestratorResult
|
||||||
from core.db.base import Base
|
from core.db.base import Base
|
||||||
from models.agent_chat_message import AgentChatMessage
|
from models.agent_chat_message import AgentChatMessage
|
||||||
from models.agent_chat_session import AgentChatSession
|
from models.agent_chat_session import AgentChatSession
|
||||||
from v1.agent_chat.schemas import AgentChatRunRequest
|
from v1.agent.schemas import AgentChatRunRequest
|
||||||
from v1.agent_chat.service import AgentChatService
|
from v1.agent.service import AgentChatService
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -786,9 +786,9 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Agent Chat
|
## Agent
|
||||||
|
|
||||||
### POST /agent-chat
|
### POST /agent
|
||||||
|
|
||||||
运行 Agent 对话(需要认证)。
|
运行 Agent 对话(需要认证)。
|
||||||
|
|
||||||
|
|||||||
@@ -166,12 +166,12 @@ PYTHONPATH=backend/src uv run pytest backend/tests/unit -k agent_chat -q
|
|||||||
PYTHONPATH=backend/src uv run pytest backend/tests/integration -k agent_chat -q
|
PYTHONPATH=backend/src uv run pytest backend/tests/integration -k agent_chat -q
|
||||||
PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_agent_chat_flow.py backend/tests/e2e/test_agent_chat_recent_session_home.py -q
|
PYTHONPATH=backend/src uv run pytest backend/tests/e2e/test_agent_chat_flow.py backend/tests/e2e/test_agent_chat_recent_session_home.py -q
|
||||||
|
|
||||||
curl -sS -X POST "${WEB_BASE_URL}/api/v1/agent-chat" \
|
curl -sS -X POST "${WEB_BASE_URL}/api/v1/agent" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
-d '{"message":"hello"}'
|
-d '{"message":"hello"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
通过标准:测试通过,`/api/v1/agent-chat/run` 返回有效 `session_id` 与事件序列。
|
通过标准:测试通过,`/api/v1/agent` 返回有效 `session_id` 与事件序列。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ curl -sS -X POST "${WEB_BASE_URL}/api/v1/agent-chat" \
|
|||||||
|
|
||||||
### 4) Agent Chat 启动后异常
|
### 4) Agent Chat 启动后异常
|
||||||
|
|
||||||
- 症状:`/api/v1/agent-chat/run` 返回 5xx 或事件不完整。
|
- 症状:`/api/v1/agent` 返回 5xx 或事件不完整。
|
||||||
- 定位:先跑 L3 测试,再看 `logs/errors/web.error.log`。
|
- 定位:先跑 L3 测试,再看 `logs/errors/web.error.log`。
|
||||||
- 修复:先恢复到可用版本,再排查迁移、配置与依赖差异。
|
- 修复:先恢复到可用版本,再排查迁移、配置与依赖差异。
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user