178 lines
6.4 KiB
Python
178 lines
6.4 KiB
Python
# Copyright (c) Alibaba, Inc. and its affiliates.
|
|
"""T1 骨架 - 接口层 + Registry 统一验证.
|
|
|
|
Plan 覆盖点:
|
|
- ``api/agent/`` 接口公民一等 API 全部可导入
|
|
- Registry 合并至 ``api/registry.py``; ``evalscope.agent`` 不 re-export
|
|
- NativeAgentConfig / AgentTrace / ParsedAction / ExecResult 基础行为
|
|
"""
|
|
|
|
import importlib
|
|
import os
|
|
import unittest
|
|
|
|
# Trigger auto-registration (function_calling strategy).
|
|
import evalscope # noqa: F401
|
|
from evalscope.api.agent import (
|
|
AgentContext,
|
|
AgentEnvironment,
|
|
AgentLoop,
|
|
AgentLoopResult,
|
|
AgentStrategy,
|
|
AgentTrace,
|
|
AgentTraceEvent,
|
|
EventType,
|
|
ExecResult,
|
|
NativeAgentConfig,
|
|
ParsedAction,
|
|
ToolExecutor,
|
|
ToolHandler,
|
|
ToolSchemaMode,
|
|
)
|
|
from evalscope.api.registry import (
|
|
AGENT_TOOL_REGISTRY,
|
|
ENVIRONMENT_REGISTRY,
|
|
STRATEGY_REGISTRY,
|
|
get_agent_tool,
|
|
get_strategy,
|
|
list_agent_tools,
|
|
list_environments,
|
|
list_strategies,
|
|
register_agent_tool,
|
|
register_environment,
|
|
register_strategy,
|
|
resolve_tools,
|
|
)
|
|
|
|
|
|
class TestAgentApiSurface(unittest.TestCase):
|
|
"""``api.agent.__init__`` 必须导出 T1 接口层全部类型."""
|
|
|
|
def test_public_symbols_importable(self):
|
|
# 仅触达一次即可确认 import 层完整;上方 import 成功即通过.
|
|
self.assertTrue(issubclass(AgentEnvironment, object))
|
|
self.assertTrue(callable(AgentLoop))
|
|
self.assertTrue(callable(AgentTrace))
|
|
self.assertIsInstance(EventType.MODEL_GENERATE, EventType)
|
|
# ToolSchemaMode 是 Literal, 仅保证可引用.
|
|
self.assertEqual(ToolSchemaMode.__args__, ('function_calling', 'textual_block', 'none'))
|
|
|
|
|
|
class TestRegistryUnification(unittest.TestCase):
|
|
"""Registry 统一到 api/registry.py, agent 包禁止 re-export."""
|
|
|
|
def test_legacy_agent_registry_file_removed(self):
|
|
# 对应 important_decision: 删除 evalscope/agent/registry.py
|
|
path = os.path.join(os.path.dirname(importlib.import_module('evalscope.agent').__file__), 'registry.py')
|
|
self.assertFalse(os.path.exists(path), f'legacy registry file should be removed: {path}')
|
|
|
|
def test_legacy_agent_registry_module_not_importable(self):
|
|
with self.assertRaises(ModuleNotFoundError):
|
|
importlib.import_module('evalscope.agent.registry')
|
|
|
|
def test_agent_package_does_not_reexport(self):
|
|
agent_pkg = importlib.import_module('evalscope.agent')
|
|
for name in ('get_strategy', 'register_strategy', 'resolve_tools', 'STRATEGY_REGISTRY'):
|
|
self.assertFalse(hasattr(agent_pkg, name), f'evalscope.agent must not re-export {name!r}')
|
|
|
|
def test_builtin_strategy_registered(self):
|
|
self.assertIn('function_calling', list_strategies())
|
|
self.assertIn('function_calling', STRATEGY_REGISTRY)
|
|
# 字段一致性.
|
|
cls = get_strategy('function_calling')
|
|
self.assertIs(STRATEGY_REGISTRY['function_calling'], cls)
|
|
|
|
def test_register_strategy_rejects_duplicate(self):
|
|
with self.assertRaises(ValueError):
|
|
|
|
@register_strategy('function_calling')
|
|
class _Dup:
|
|
pass
|
|
|
|
def test_register_environment_and_agent_tool(self):
|
|
# 使用临时名字以免污染全局 registry.
|
|
|
|
@register_environment('_unit_env')
|
|
class _Env(AgentEnvironment):
|
|
name = '_unit_env'
|
|
|
|
async def exec(self, cmd, *, cwd=None, input=None, timeout=None):
|
|
raise NotImplementedError
|
|
|
|
async def close(self):
|
|
pass
|
|
|
|
try:
|
|
self.assertIn('_unit_env', list_environments())
|
|
self.assertIn('_unit_env', ENVIRONMENT_REGISTRY)
|
|
finally:
|
|
ENVIRONMENT_REGISTRY.pop('_unit_env', None)
|
|
|
|
@register_agent_tool('_unit_tool')
|
|
async def _tool_handler(call, env):
|
|
return 'ok'
|
|
|
|
try:
|
|
self.assertIs(get_agent_tool('_unit_tool'), _tool_handler)
|
|
self.assertIn('_unit_tool', list_agent_tools())
|
|
self.assertIn('_unit_tool', AGENT_TOOL_REGISTRY)
|
|
finally:
|
|
AGENT_TOOL_REGISTRY.pop('_unit_tool', None)
|
|
|
|
def test_resolve_tools_empty_and_missing(self):
|
|
self.assertEqual(resolve_tools(None), {})
|
|
self.assertEqual(resolve_tools([]), {})
|
|
with self.assertRaises(ValueError):
|
|
resolve_tools(['definitely_missing_tool'])
|
|
|
|
|
|
class TestAgentTypesBehavior(unittest.TestCase):
|
|
"""NativeAgentConfig / AgentTrace / ParsedAction / ExecResult 基础字段."""
|
|
|
|
def test_agent_config_defaults(self):
|
|
cfg = NativeAgentConfig()
|
|
self.assertEqual(cfg.strategy, 'function_calling')
|
|
self.assertEqual(cfg.tools, [])
|
|
self.assertEqual(cfg.max_steps, 10)
|
|
self.assertIsNone(cfg.environment)
|
|
self.assertEqual(cfg.kwargs, {})
|
|
|
|
def test_agent_config_dict_validate(self):
|
|
cfg = NativeAgentConfig.model_validate({'strategy': 'function_calling', 'max_steps': 5})
|
|
self.assertEqual(cfg.max_steps, 5)
|
|
|
|
def test_parsed_action_dataclass_defaults(self):
|
|
pa = ParsedAction()
|
|
self.assertEqual(pa.tool_calls, [])
|
|
self.assertIsNone(pa.final_answer)
|
|
self.assertIsNone(pa.error)
|
|
self.assertIsNone(pa.raw_text)
|
|
|
|
def test_exec_result_defaults(self):
|
|
r = ExecResult()
|
|
self.assertEqual(r.returncode, 0)
|
|
self.assertEqual(r.stdout, '')
|
|
self.assertEqual(r.stderr, '')
|
|
self.assertFalse(r.timed_out)
|
|
self.assertEqual(r.duration, 0.0)
|
|
|
|
def test_agent_trace_add_event_and_step_count(self):
|
|
trace = AgentTrace(strategy='function_calling', environment=None, max_steps=3)
|
|
ev = trace.add_event(step=0, type=EventType.MODEL_GENERATE, latency_ms=12.5)
|
|
self.assertIsInstance(ev, AgentTraceEvent)
|
|
trace.add_event(step=0, type=EventType.SUBMIT, payload={'final_answer': 'x'})
|
|
trace.add_event(step=1, type=EventType.MODEL_GENERATE)
|
|
self.assertEqual(len(trace.events), 3)
|
|
self.assertEqual(trace.step_count, 2) # step 0 与 step 1 两个独立 step
|
|
|
|
def test_agent_trace_json_roundtrip(self):
|
|
# 为 ReviewResult 持久化设计, 必须 pydantic 可序列化.
|
|
trace = AgentTrace(strategy='function_calling', environment=None, max_steps=1)
|
|
trace.add_event(step=0, type=EventType.MODEL_GENERATE)
|
|
loaded = AgentTrace.model_validate_json(trace.model_dump_json())
|
|
self.assertEqual(loaded.events[0].type, EventType.MODEL_GENERATE)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|