fix: delete agent 500 error + dynamic personality + deployment guide
- Fix delete agent 500: clean up FK records (agent_llm_logs, permissions, schedules, executions, team_members) and unbind goals/tasks before delete - Remove hardcoded personality templates in Android, replace with dynamic system prompt generation from name + description - Set promptSectionsEnabled=false to bypass PromptComposer for personality - Add Tencent Cloud Linux deployment guide (Docker Compose) - Accumulated backend service updates, frontend UI fixes, Android app changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
666
backend/tests/test_agent_runtime.py
Normal file
666
backend/tests/test_agent_runtime.py
Normal file
@@ -0,0 +1,666 @@
|
||||
"""
|
||||
Agent ReAct 循环集成测试
|
||||
|
||||
覆盖 core.py 核心路径:正常对话 / 工具调用 / 预算熔断 / max_iterations 截断
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from app.agent_runtime.core import AgentRuntime
|
||||
from app.agent_runtime.schemas import (
|
||||
AgentConfig,
|
||||
AgentBudgetConfig,
|
||||
AgentLLMConfig,
|
||||
AgentToolConfig,
|
||||
AgentMemoryConfig,
|
||||
)
|
||||
from app.services.tool_registry import tool_registry as _global_registry
|
||||
|
||||
|
||||
def make_config(**kwargs) -> AgentConfig:
|
||||
"""快捷创建测试用 AgentConfig"""
|
||||
defaults = {
|
||||
"name": "test_agent",
|
||||
"system_prompt": "You are a test assistant.",
|
||||
"llm": AgentLLMConfig(
|
||||
provider="openai",
|
||||
model="gpt-4o-mini",
|
||||
max_iterations=5,
|
||||
),
|
||||
"budget": AgentBudgetConfig(
|
||||
max_llm_invocations=10,
|
||||
max_tool_calls=20,
|
||||
),
|
||||
"tools": AgentToolConfig(),
|
||||
"memory": AgentMemoryConfig(
|
||||
enabled=False,
|
||||
persist_to_db=False,
|
||||
vector_memory_enabled=False,
|
||||
learning_enabled=False,
|
||||
),
|
||||
}
|
||||
defaults.update(kwargs)
|
||||
return AgentConfig(**defaults)
|
||||
|
||||
|
||||
# ── Mock helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
class MockFunction:
|
||||
"""模拟 OpenAI SDK tool_call.function 对象"""
|
||||
def __init__(self, name="", arguments="{}"):
|
||||
self.name = name
|
||||
self.arguments = arguments
|
||||
|
||||
|
||||
class MockToolCall:
|
||||
"""模拟 OpenAI SDK tool_call 对象"""
|
||||
def __init__(self, id="call_1", name="", arguments="{}"):
|
||||
self.id = id
|
||||
self.type = "function"
|
||||
self.function = MockFunction(name=name, arguments=arguments)
|
||||
|
||||
|
||||
class MockLLMResponse:
|
||||
"""模拟 LLM 返回的 response 对象"""
|
||||
def __init__(self, content="", tool_calls=None, reasoning_content=None):
|
||||
self.content = content
|
||||
self.tool_calls = tool_calls
|
||||
self.reasoning_content = reasoning_content
|
||||
|
||||
def get(self, key, default=None):
|
||||
return getattr(self, key, default)
|
||||
|
||||
|
||||
def tc(id="call_1", name="", arguments="{}"):
|
||||
"""快捷构造 tool_call"""
|
||||
return MockToolCall(id=id, name=name, arguments=arguments)
|
||||
|
||||
|
||||
def _base_mocks(runtime):
|
||||
"""返回 (patches, mock_llm_class) 以便统一管理 mock 生命周期。"""
|
||||
mock_llm = patch("app.agent_runtime.core._LLMClient")
|
||||
return {
|
||||
"runtime": patch.multiple(
|
||||
runtime,
|
||||
_fire_execution_log=MagicMock(),
|
||||
_inject_memory_context=AsyncMock(),
|
||||
_inject_knowledge_context=AsyncMock(),
|
||||
),
|
||||
"memory": patch.object(runtime.memory, "save_context", new=AsyncMock()),
|
||||
"llm": mock_llm,
|
||||
}, mock_llm
|
||||
|
||||
|
||||
# ── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestAgentRuntimeNormalConversation:
|
||||
"""正常对话流程测试"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_simple_text_response(self):
|
||||
config = make_config()
|
||||
runtime = AgentRuntime(config=config)
|
||||
mock_response = MockLLMResponse(content="Hello! How can I help you?")
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = AsyncMock(return_value=mock_response)
|
||||
result = await runtime.run("Hello!")
|
||||
|
||||
assert result.success is True
|
||||
assert "Hello!" in result.content
|
||||
assert result.iterations_used == 1
|
||||
assert result.tool_calls_made == 0
|
||||
assert len(result.steps) >= 1
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_multi_turn_conversation_context(self):
|
||||
config = make_config()
|
||||
runtime = AgentRuntime(config=config)
|
||||
call_count = [0]
|
||||
|
||||
async def mock_chat(messages, tools, iteration, on_completion=None):
|
||||
call_count[0] += 1
|
||||
if call_count[0] == 1:
|
||||
return MockLLMResponse(content="I processed your first message.")
|
||||
return MockLLMResponse(content="I processed your second message.")
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = mock_chat
|
||||
|
||||
result1 = await runtime.run("First message")
|
||||
assert result1.success
|
||||
assert "first" in result1.content
|
||||
|
||||
result2 = await runtime.run("Second message")
|
||||
assert result2.success
|
||||
assert "second" in result2.content
|
||||
|
||||
assert len(runtime.context.messages) >= 4
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_llm_response_handling(self):
|
||||
config = make_config()
|
||||
runtime = AgentRuntime(config=config)
|
||||
mock_response = MockLLMResponse(content="")
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = AsyncMock(return_value=mock_response)
|
||||
result = await runtime.run("Test")
|
||||
|
||||
assert result.success is True
|
||||
assert result.content is not None
|
||||
|
||||
|
||||
class TestAgentRuntimeToolCalling:
|
||||
"""工具调用流程测试"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_single_tool_call(self):
|
||||
config = make_config()
|
||||
runtime = AgentRuntime(config=config)
|
||||
|
||||
def get_weather(**kwargs):
|
||||
return json.dumps({"city": kwargs.get("city", "Beijing"), "temp": 25})
|
||||
|
||||
_global_registry.register_builtin_tool(
|
||||
"get_weather", get_weather,
|
||||
{"type": "function", "function": {
|
||||
"name": "get_weather", "description": "Get weather",
|
||||
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}},
|
||||
}},
|
||||
)
|
||||
|
||||
llm_responses = [
|
||||
MockLLMResponse(content=None, tool_calls=[
|
||||
tc(id="call_1", name="get_weather", arguments='{"city": "Beijing"}'),
|
||||
]),
|
||||
MockLLMResponse(content="Beijing is 25C today."),
|
||||
]
|
||||
it = iter(llm_responses)
|
||||
|
||||
async def mock_chat(messages, tools, iteration, on_completion=None):
|
||||
return next(it)
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = mock_chat
|
||||
result = await runtime.run("What's the weather?")
|
||||
|
||||
assert result.success is True
|
||||
assert "Beijing" in result.content
|
||||
assert result.tool_calls_made == 1
|
||||
tool_steps = [s for s in result.steps if s.type == "tool_result"]
|
||||
assert len(tool_steps) == 1
|
||||
assert tool_steps[0].tool_name == "get_weather"
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_args_json_parse_failure(self):
|
||||
config = make_config()
|
||||
runtime = AgentRuntime(config=config)
|
||||
|
||||
def echo(**kwargs):
|
||||
return json.dumps(kwargs)
|
||||
|
||||
_global_registry.register_builtin_tool(
|
||||
"echo", echo,
|
||||
{"type": "function", "function": {"name": "echo"}},
|
||||
)
|
||||
|
||||
# invalid JSON in arguments → should be caught by _extract_tool_calls / json.loads
|
||||
llm_responses = [
|
||||
MockLLMResponse(content=None, tool_calls=[
|
||||
tc(id="call_1", name="echo", arguments="not valid json {{{"),
|
||||
]),
|
||||
MockLLMResponse(content="Done."),
|
||||
]
|
||||
it = iter(llm_responses)
|
||||
|
||||
async def mock_chat(messages, tools, iteration, on_completion=None):
|
||||
return next(it)
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = mock_chat
|
||||
result = await runtime.run("echo something")
|
||||
|
||||
# Should not crash, should complete successfully
|
||||
assert result.success is True
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_execution_exception_protection(self):
|
||||
config = make_config()
|
||||
runtime = AgentRuntime(config=config)
|
||||
|
||||
def broken_tool(**kwargs):
|
||||
raise RuntimeError("Tool internal failure!")
|
||||
|
||||
_global_registry.register_builtin_tool(
|
||||
"broken_tool", broken_tool,
|
||||
{"type": "function", "function": {"name": "broken_tool"}},
|
||||
)
|
||||
|
||||
llm_responses = [
|
||||
MockLLMResponse(content=None, tool_calls=[
|
||||
tc(id="call_1", name="broken_tool", arguments="{}"),
|
||||
]),
|
||||
MockLLMResponse(content="The tool failed, let me try something else."),
|
||||
]
|
||||
it = iter(llm_responses)
|
||||
|
||||
async def mock_chat(messages, tools, iteration, on_completion=None):
|
||||
return next(it)
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = mock_chat
|
||||
result = await runtime.run("Use the broken tool")
|
||||
|
||||
assert result.success is True
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_multiple_parallel_tool_calls(self):
|
||||
config = make_config()
|
||||
runtime = AgentRuntime(config=config)
|
||||
|
||||
def tool_a(**kwargs):
|
||||
return json.dumps({"a": 1})
|
||||
|
||||
def tool_b(**kwargs):
|
||||
return json.dumps({"b": 2})
|
||||
|
||||
_global_registry.register_builtin_tool(
|
||||
"tool_a", tool_a, {"type": "function", "function": {"name": "tool_a"}},
|
||||
)
|
||||
_global_registry.register_builtin_tool(
|
||||
"tool_b", tool_b, {"type": "function", "function": {"name": "tool_b"}},
|
||||
)
|
||||
|
||||
llm_responses = [
|
||||
MockLLMResponse(content=None, tool_calls=[
|
||||
tc(id="call_1", name="tool_a", arguments="{}"),
|
||||
tc(id="call_2", name="tool_b", arguments="{}"),
|
||||
]),
|
||||
MockLLMResponse(content="Both tools executed."),
|
||||
]
|
||||
it = iter(llm_responses)
|
||||
|
||||
async def mock_chat(messages, tools, iteration, on_completion=None):
|
||||
return next(it)
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = mock_chat
|
||||
result = await runtime.run("Run both tools")
|
||||
|
||||
assert result.success is True
|
||||
assert result.tool_calls_made == 2
|
||||
|
||||
|
||||
class TestAgentRuntimeBudget:
|
||||
"""预算熔断测试"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_invocation_budget_exceeded(self):
|
||||
config = make_config()
|
||||
config.budget.max_llm_invocations = 0 # no LLM calls allowed
|
||||
runtime = AgentRuntime(config=config)
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
mock_chat = AsyncMock()
|
||||
llm_class.return_value.chat = mock_chat
|
||||
result = await runtime.run("Hello")
|
||||
|
||||
assert result.success is False
|
||||
assert "预算" in result.content
|
||||
mock_chat.assert_not_called()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_tool_call_budget_exceeded(self):
|
||||
config = make_config()
|
||||
config.budget.max_tool_calls = 1
|
||||
runtime = AgentRuntime(config=config)
|
||||
|
||||
def echo(**kwargs):
|
||||
return json.dumps(kwargs)
|
||||
|
||||
_global_registry.register_builtin_tool(
|
||||
"echo", echo, {"type": "function", "function": {"name": "echo"}},
|
||||
)
|
||||
|
||||
# two tool calls → second one triggers budget exceeded
|
||||
llm_responses = [
|
||||
MockLLMResponse(content=None, tool_calls=[
|
||||
tc(id="call_1", name="echo", arguments='{"msg": "hello"}'),
|
||||
]),
|
||||
MockLLMResponse(content=None, tool_calls=[
|
||||
tc(id="call_2", name="echo", arguments='{"msg": "world"}'),
|
||||
]),
|
||||
]
|
||||
it = iter(llm_responses)
|
||||
|
||||
async def mock_chat(messages, tools, iteration, on_completion=None):
|
||||
return next(it)
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = mock_chat
|
||||
result = await runtime.run("Echo twice")
|
||||
|
||||
# budget exceeded → tool_calls_made exceeds max_tool_calls
|
||||
assert result.truncated is True or result.success is False
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_llm_invocation_callback_budget(self):
|
||||
config = make_config()
|
||||
runtime = AgentRuntime(config=config)
|
||||
runtime.on_llm_invocation = MagicMock(side_effect=Exception("workflow budget exceeded"))
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = AsyncMock()
|
||||
result = await runtime.run("Hello")
|
||||
|
||||
assert result.success is False
|
||||
assert "工作流预算" in result.content
|
||||
|
||||
|
||||
class TestAgentRuntimeMaxIterations:
|
||||
"""max_iterations 截断测试"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_iterations_truncation(self):
|
||||
config = make_config()
|
||||
config.llm.max_iterations = 2
|
||||
runtime = AgentRuntime(config=config)
|
||||
|
||||
def echo(**kwargs):
|
||||
return json.dumps(kwargs)
|
||||
|
||||
_global_registry.register_builtin_tool(
|
||||
"echo", echo, {"type": "function", "function": {"name": "echo"}},
|
||||
)
|
||||
|
||||
mock_response = MockLLMResponse(content=None, tool_calls=[
|
||||
tc(id="call_1", name="echo", arguments='{"msg": "hello"}'),
|
||||
])
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = AsyncMock(return_value=mock_response)
|
||||
result = await runtime.run("Echo forever")
|
||||
|
||||
assert result.success is False
|
||||
assert result.truncated is True
|
||||
assert result.iterations_used == 2
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_max_iterations_minimum_one(self):
|
||||
config = make_config()
|
||||
config.llm.max_iterations = 0
|
||||
runtime = AgentRuntime(config=config)
|
||||
mock_response = MockLLMResponse(content="Response")
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = AsyncMock(return_value=mock_response)
|
||||
result = await runtime.run("Test")
|
||||
|
||||
assert result.iterations_used >= 1
|
||||
|
||||
|
||||
class TestAgentRuntimeLLMRetry:
|
||||
"""LLM 调用重试测试"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_retryable_error_triggers_retry(self):
|
||||
config = make_config()
|
||||
config.llm.max_iterations = 3
|
||||
runtime = AgentRuntime(config=config)
|
||||
call_count = [0]
|
||||
|
||||
async def mock_chat(messages, tools, iteration, on_completion=None):
|
||||
call_count[0] += 1
|
||||
if call_count[0] == 1:
|
||||
raise Exception("request timed out")
|
||||
return MockLLMResponse(content="Retry succeeded!")
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = mock_chat
|
||||
result = await runtime.run("Test")
|
||||
|
||||
assert result.success is True
|
||||
assert "Retry succeeded" in result.content
|
||||
assert call_count[0] == 2
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_retryable_error_stops(self):
|
||||
config = make_config()
|
||||
runtime = AgentRuntime(config=config)
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = AsyncMock(
|
||||
side_effect=Exception("invalid API key"))
|
||||
result = await runtime.run("Test")
|
||||
|
||||
assert result.success is False
|
||||
assert "invalid API key" in result.error
|
||||
|
||||
|
||||
class TestAgentRuntimeSelfReview:
|
||||
"""Self-review 自检测试"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_self_review_passed(self):
|
||||
config = make_config()
|
||||
config.self_review_enabled = True
|
||||
runtime = AgentRuntime(config=config)
|
||||
mock_response = MockLLMResponse(content="A well-crafted response.")
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class, \
|
||||
patch.object(runtime, "_self_review", new=AsyncMock(
|
||||
return_value={"score": 0.85, "passed": True, "threshold": 0.6,
|
||||
"issues": [], "suggestions": []})) as mock_review:
|
||||
llm_class.return_value.chat = AsyncMock(return_value=mock_response)
|
||||
result = await runtime.run("Test")
|
||||
|
||||
assert result.success is True
|
||||
mock_review.assert_called_once()
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_self_review_failed_triggers_correction(self):
|
||||
config = make_config()
|
||||
config.self_review_enabled = True
|
||||
config.llm.max_iterations = 5
|
||||
runtime = AgentRuntime(config=config)
|
||||
|
||||
llm_responses = [
|
||||
MockLLMResponse(content="Bad answer."),
|
||||
MockLLMResponse(content="Corrected answer, much better now."),
|
||||
]
|
||||
llm_it = iter(llm_responses)
|
||||
|
||||
review_responses = [
|
||||
{"score": 0.3, "passed": False, "threshold": 0.6,
|
||||
"issues": ["Too short"], "suggestions": ["Add more context"]},
|
||||
{"score": 0.8, "passed": True, "threshold": 0.6,
|
||||
"issues": [], "suggestions": []},
|
||||
]
|
||||
review_it = iter(review_responses)
|
||||
|
||||
async def mock_chat(messages, tools, iteration, on_completion=None):
|
||||
return next(llm_it)
|
||||
|
||||
async def mock_review(text, task_context=None):
|
||||
return next(review_it)
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class, \
|
||||
patch.object(runtime, "_self_review", side_effect=mock_review):
|
||||
llm_class.return_value.chat = mock_chat
|
||||
result = await runtime.run("Test")
|
||||
|
||||
assert result.success is True
|
||||
assert "Corrected" in result.content
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_self_review_exception_fallback(self):
|
||||
"""self_review 返回 error 结果时不影响主流程"""
|
||||
config = make_config()
|
||||
config.self_review_enabled = True
|
||||
runtime = AgentRuntime(config=config)
|
||||
mock_response = MockLLMResponse(content="Some answer.")
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class, \
|
||||
patch.object(runtime, "_self_review", new=AsyncMock(
|
||||
return_value={"score": 0.0, "passed": False, "threshold": 0.6,
|
||||
"issues": ["review error"],
|
||||
"suggestions": ["check configuration"], "error": "service down"})):
|
||||
llm_class.return_value.chat = AsyncMock(return_value=mock_response)
|
||||
result = await runtime.run("Test")
|
||||
|
||||
# review returns passed=False, correction gets appended, LLM responds again...
|
||||
# The result should still be success=True since the correction loop works
|
||||
assert result.success is True
|
||||
|
||||
|
||||
class TestAgentRuntimeStreaming:
|
||||
"""流式执行测试"""
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_stream_yields_events(self):
|
||||
config = make_config()
|
||||
config.llm.max_iterations = 2
|
||||
runtime = AgentRuntime(config=config)
|
||||
mock_response = MockLLMResponse(content="Streamed response.")
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = AsyncMock(return_value=mock_response)
|
||||
events = []
|
||||
async for event in runtime.run_stream("Hello"):
|
||||
events.append(event)
|
||||
|
||||
assert len(events) > 0
|
||||
last = events[-1]
|
||||
assert last["type"] in ("final", "error")
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_stream_with_tool_calls(self):
|
||||
config = make_config()
|
||||
config.llm.max_iterations = 3
|
||||
runtime = AgentRuntime(config=config)
|
||||
|
||||
def echo(**kwargs):
|
||||
return json.dumps(kwargs)
|
||||
|
||||
_global_registry.register_builtin_tool(
|
||||
"echo", echo, {"type": "function", "function": {"name": "echo"}},
|
||||
)
|
||||
|
||||
llm_responses = [
|
||||
MockLLMResponse(content=None, tool_calls=[
|
||||
tc(id="call_1", name="echo", arguments='{"msg": "hi"}'),
|
||||
]),
|
||||
MockLLMResponse(content="Done streaming."),
|
||||
]
|
||||
it = iter(llm_responses)
|
||||
|
||||
async def mock_chat(messages, tools, iteration, on_completion=None):
|
||||
return next(it)
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = mock_chat
|
||||
events = []
|
||||
async for event in runtime.run_stream("Hello"):
|
||||
events.append(event)
|
||||
|
||||
event_types = [e["type"] for e in events]
|
||||
assert "tool_call" in event_types
|
||||
assert "tool_result" in event_types
|
||||
assert "final" in event_types
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_stream_budget_error(self):
|
||||
config = make_config()
|
||||
config.budget.max_llm_invocations = 0
|
||||
runtime = AgentRuntime(config=config)
|
||||
runtime._llm_invocations = 0
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = AsyncMock()
|
||||
events = []
|
||||
async for event in runtime.run_stream("Test"):
|
||||
events.append(event)
|
||||
|
||||
assert any(e["type"] == "error" for e in events)
|
||||
|
||||
|
||||
class TestAgentRuntimeConfig:
|
||||
"""配置相关测试"""
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_default_config_values(self):
|
||||
config = AgentConfig()
|
||||
assert config.name == "default_agent"
|
||||
assert config.llm.max_iterations == 10
|
||||
assert config.budget.max_llm_invocations == 200
|
||||
assert config.budget.max_tool_calls == 500
|
||||
assert config.memory.enabled is True
|
||||
assert config.self_review_enabled is False
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_minimal_config_creates_runtime(self):
|
||||
config = AgentConfig(name="minimal")
|
||||
runtime = AgentRuntime(config=config)
|
||||
assert runtime.config.name == "minimal"
|
||||
assert runtime._llm_invocations == 0
|
||||
assert runtime._memory_context_loaded is False
|
||||
|
||||
@pytest.mark.unit
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_invocation_counter_resets_per_run(self):
|
||||
config = make_config()
|
||||
runtime = AgentRuntime(config=config)
|
||||
mock_response = MockLLMResponse(content="OK")
|
||||
|
||||
mocks, mock_llm = _base_mocks(runtime)
|
||||
with mocks["runtime"], mocks["memory"], mock_llm as llm_class:
|
||||
llm_class.return_value.chat = AsyncMock(return_value=mock_response)
|
||||
await runtime.run("First")
|
||||
assert runtime._llm_invocations >= 1
|
||||
|
||||
await runtime.run("Second")
|
||||
assert runtime._llm_invocations == 1
|
||||
Reference in New Issue
Block a user