Include utility modules, shared channels, prompts, read-only skills, wardrobe memory and regression tests. Externalize legacy credentials and exclude private runtime state.
103 lines
3.9 KiB
Python
103 lines
3.9 KiB
Python
import asyncio
|
|
import unittest
|
|
from pathlib import Path
|
|
import sys
|
|
from unittest.mock import patch
|
|
|
|
BASE = Path(__file__).resolve().parents[1]
|
|
sys.path[:0] = [str(BASE / "bot-utility"), str(BASE)]
|
|
|
|
import agent_loop
|
|
from agent import ToolResult
|
|
|
|
|
|
class Channel:
|
|
name = "tg"
|
|
def is_admin(self, uid):
|
|
return True
|
|
|
|
|
|
class Store:
|
|
instances = []
|
|
def __init__(self):
|
|
self.steps = []
|
|
self.finished = None
|
|
Store.instances.append(self)
|
|
def start(self, uid, conversation):
|
|
return "task"
|
|
def record(self, *args):
|
|
self.steps.append(args)
|
|
def finish(self, task, status, error=""):
|
|
self.finished = status
|
|
|
|
|
|
class Executor:
|
|
calls = []
|
|
def __init__(self):
|
|
pass
|
|
def prompt_schema(self, ctx):
|
|
return []
|
|
async def execute(self, name, args, ctx):
|
|
Executor.calls.append((name, args))
|
|
if name == "setu":
|
|
return ToolResult(True, "ok", "图片已获取", artifacts=[{"id": "a1"}])
|
|
return ToolResult(True, "ok", "邮件已发送", data={"used": args})
|
|
|
|
|
|
class Engine:
|
|
def __init__(self, responses):
|
|
self.responses = list(responses)
|
|
async def call_deepseek(self, *args, **kwargs):
|
|
return self.responses.pop(0)
|
|
def parse_json_response(self, raw):
|
|
return raw
|
|
async def run_tools(self, calls):
|
|
return "web result"
|
|
|
|
|
|
class AgentLoopTest(unittest.IsolatedAsyncioTestCase):
|
|
async def test_two_round_composition_and_final_response(self):
|
|
Executor.calls = []
|
|
initial = {"message": [], "tools": [{"name": "setu", "args": {"action": "get", "params": {"tags": ["铃兰"]}}}]}
|
|
second = {"message": [], "tools": [{"name": "mail", "args": {"action": "send", "params": {"artifact_ids": ["a1"]}}}]}
|
|
final = {"message": [{"content": "完成了"}], "tools": []}
|
|
engine = Engine([second, final])
|
|
with patch.object(agent_loop, "AgentTaskStore", Store), \
|
|
patch.object(agent_loop, "ToolExecutor", Executor):
|
|
result = await agent_loop.run_agent_loop(
|
|
engine, initial, "system", "user", Channel(), "private", 1, 2)
|
|
self.assertEqual(result, final)
|
|
self.assertEqual([x[0] for x in Executor.calls], ["setu", "mail"])
|
|
self.assertEqual(Store.instances[-1].finished, "done")
|
|
self.assertEqual(len(Store.instances[-1].steps), 2)
|
|
|
|
async def test_not_understood_never_executes(self):
|
|
initial = {"understood": False, "message": [], "tools": [{"name": "mail"}]}
|
|
with patch.object(agent_loop, "ToolExecutor", Executor):
|
|
result = await agent_loop.run_agent_loop(
|
|
Engine([]), initial, "s", "u", Channel(), "private", 1, 2)
|
|
self.assertEqual(result["tools"], [])
|
|
self.assertIn("明白", result["message"][0]["content"])
|
|
self.assertEqual(Executor.calls, [])
|
|
|
|
async def test_model_failure_includes_specific_safe_error(self):
|
|
Executor.calls = []
|
|
initial = {"message": [], "tools": [{"name": "pixiv", "args": {"action": "get"}}]}
|
|
with patch.object(agent_loop, "AgentTaskStore", Store), \
|
|
patch.object(agent_loop, "ToolExecutor", Executor):
|
|
# Force an observation containing a raw-looking failure, then no model response.
|
|
original = Executor.execute
|
|
async def failure(self, name, args, ctx):
|
|
return ToolResult(False, "send_failed", "多次尝试发送失败。请手动访问 https://example.test/image")
|
|
with patch.object(Executor, "execute", failure):
|
|
result = await agent_loop.run_agent_loop(
|
|
Engine([None]), initial, "s", "u", Channel(), "private", 1, 2)
|
|
text = result["message"][0]["content"]
|
|
self.assertIn("多次尝试发送失败", text)
|
|
self.assertIn("send_failed", text)
|
|
self.assertIn("https://example.test/image", text)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|