108 lines
3.9 KiB
Python
108 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from opc.layer3_agent.runtime_v2.runtime import NativeRuntimeV2
|
|
from opc.layer4_tools.registry import ToolDefinition, ToolRegistry
|
|
from opc.llm.provider import _parse_tool_arguments
|
|
|
|
|
|
class ToolArgumentParsingTests(unittest.TestCase):
|
|
def test_parse_tool_arguments_returns_error_context_for_invalid_json(self) -> None:
|
|
parsed, raw, error = _parse_tool_arguments("shell_exec", '{"command": "echo hi"')
|
|
self.assertEqual(parsed, '{"command": "echo hi"')
|
|
self.assertEqual(raw, '{"command": "echo hi"')
|
|
self.assertIsNotNone(error)
|
|
self.assertIn("Invalid tool arguments JSON for `shell_exec`", error)
|
|
|
|
def test_runtime_v2_tool_argument_finalize_preserves_unicode(self) -> None:
|
|
runtime = NativeRuntimeV2.__new__(NativeRuntimeV2)
|
|
calls = runtime._finalize_tool_calls(
|
|
{
|
|
0: {
|
|
"id": "call-1",
|
|
"function": "todo_write",
|
|
"arguments_chunks": [
|
|
'{"todos":[{"id":"1","title":"梳理需求","status":"in_progress"}]}'
|
|
],
|
|
}
|
|
}
|
|
)
|
|
|
|
self.assertEqual(calls[0]["function"], "todo_write")
|
|
self.assertIsNone(calls[0]["arguments_parse_error"])
|
|
self.assertEqual(calls[0]["arguments"]["todos"][0]["title"], "梳理需求")
|
|
|
|
|
|
class ToolRegistryUnknownArgumentTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_unknown_argument_returns_structured_error_with_valid_params(self) -> None:
|
|
"""Unknown tool arguments must surface as a tool-call error, not be
|
|
silently dropped. The error message must list the valid parameters
|
|
so the agent can retry with the right names instead of the LLM
|
|
hallucination (e.g. `digest` vs `summary`)."""
|
|
registry = ToolRegistry()
|
|
|
|
async def example_stub(summary: str = "", parent: str = "") -> dict[str, str]:
|
|
return {"summary": summary, "parent": parent}
|
|
|
|
registry.register(
|
|
ToolDefinition(
|
|
name="example_tool",
|
|
description="stub",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {
|
|
"summary": {"type": "string"},
|
|
"parent": {"type": "string"},
|
|
},
|
|
},
|
|
func=example_stub,
|
|
)
|
|
)
|
|
|
|
result = await registry.invoke(
|
|
"example_tool",
|
|
{"digest": "3 children done"},
|
|
)
|
|
|
|
self.assertFalse(result.get("success", True))
|
|
error = str(result.get("error", ""))
|
|
self.assertIn("example_tool", error)
|
|
self.assertIn("'digest'", error)
|
|
# Error message must enumerate valid params so the agent can retry.
|
|
self.assertIn("summary", error)
|
|
self.assertIn("retry", error.lower())
|
|
|
|
async def test_known_arguments_still_execute_normally(self) -> None:
|
|
"""Guardrail: making unknown-arg errors strict must not reject
|
|
legitimate calls or the `task` auto-injection path."""
|
|
registry = ToolRegistry()
|
|
|
|
async def echo_tool(message: str, task=None) -> dict[str, str]:
|
|
return {"echoed": message, "task_injected": str(bool(task))}
|
|
|
|
registry.register(
|
|
ToolDefinition(
|
|
name="echo",
|
|
description="stub",
|
|
parameters={
|
|
"type": "object",
|
|
"properties": {"message": {"type": "string"}},
|
|
},
|
|
func=echo_tool,
|
|
)
|
|
)
|
|
|
|
result = await registry.invoke(
|
|
"echo",
|
|
{"message": "hello"},
|
|
task=object(),
|
|
)
|
|
|
|
self.assertTrue(result.get("success"))
|
|
self.assertEqual(result["result"]["echoed"], "hello")
|
|
self.assertEqual(result["result"]["task_injected"], "True")
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|