Initial import of Shirobana bot backend
Include utility modules, shared channels, prompts, read-only skills, wardrobe memory and regression tests. Externalize legacy credentials and exclude private runtime state.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user