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,114 @@
|
||||
"""Native deepseek-flash multimodal chat tests; no real API calls."""
|
||||
|
||||
import base64
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
BASE = Path(__file__).resolve().parents[1]
|
||||
sys.path[:0] = [str(BASE / "bot-utility"), str(BASE)]
|
||||
|
||||
import agent_loop
|
||||
from agent import ToolResult
|
||||
from chat import engine
|
||||
|
||||
|
||||
class MultimodalContentTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_local_image_and_task_share_one_user_content(self):
|
||||
payload = b"\x89PNG\r\n\x1a\nsmall-test-image"
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "task.png"
|
||||
path.write_bytes(payload)
|
||||
content = await engine._build_user_content(
|
||||
"总结图片,并读出其中要做的任务",
|
||||
[{"url": str(path), "file_size": str(len(payload))}],
|
||||
)
|
||||
self.assertIsInstance(content, list)
|
||||
self.assertEqual(content[0]["type"], "image_url")
|
||||
self.assertEqual(content[1]["type"], "text")
|
||||
self.assertEqual(content[1]["text"], "总结图片,并读出其中要做的任务")
|
||||
data_url = content[0]["image_url"]["url"]
|
||||
self.assertTrue(data_url.startswith("data:image/png;base64,"))
|
||||
self.assertEqual(base64.b64decode(data_url.split(",", 1)[1]), payload)
|
||||
|
||||
async def test_text_only_content_is_unchanged(self):
|
||||
content = await engine._build_user_content("普通聊天", None)
|
||||
self.assertEqual(content, "普通聊天")
|
||||
|
||||
async def test_main_call_passes_same_multimodal_content_to_official_model(self):
|
||||
parts = [
|
||||
{"type": "image_url", "image_url": {"url": "data:image/png;base64,eA=="}},
|
||||
{"type": "text", "text": "读图"},
|
||||
]
|
||||
official = AsyncMock(return_value="ok")
|
||||
with patch.object(engine, "_modelscope_available", return_value=False), \
|
||||
patch.object(engine, "_build_user_content", new=AsyncMock(return_value=parts)), \
|
||||
patch.object(engine, "_call_deepseek_official", new=official):
|
||||
result = await engine.call_deepseek("system", "读图", image_infos=[{"url": "x"}])
|
||||
self.assertEqual(result, "ok")
|
||||
self.assertIs(official.await_args.args[1], parts)
|
||||
self.assertEqual(engine.MODEL_NAME, "deepseek-flash")
|
||||
|
||||
async def test_private_pending_flow_forwards_original_images_without_description_step(self):
|
||||
images = [{"url": "/tmp/original.png"}]
|
||||
reply = AsyncMock()
|
||||
engine._user_pending_messages[123] = ["按图执行"]
|
||||
engine._user_pending_images[123] = images
|
||||
try:
|
||||
with patch.object(engine, "_call_model_and_reply", new=reply):
|
||||
await engine._process_pending_messages(123, 0)
|
||||
finally:
|
||||
engine._user_pending_messages.pop(123, None)
|
||||
engine._user_pending_images.pop(123, None)
|
||||
engine._user_pending_tasks.pop(123, None)
|
||||
self.assertEqual(reply.await_args.args[2], "按图执行")
|
||||
self.assertIs(reply.await_args.kwargs["image_infos"], images)
|
||||
|
||||
|
||||
class FakeStore:
|
||||
def start(self, uid, conversation):
|
||||
return "task"
|
||||
def record(self, *args):
|
||||
pass
|
||||
def finish(self, *args):
|
||||
pass
|
||||
|
||||
|
||||
class FakeExecutor:
|
||||
async def execute(self, name, args, ctx):
|
||||
return ToolResult(True, "ok", "done")
|
||||
|
||||
|
||||
class MultiRoundEngine:
|
||||
def __init__(self):
|
||||
self.image_kwargs = []
|
||||
self.reasoning_kwargs = []
|
||||
async def call_deepseek(self, *args, **kwargs):
|
||||
self.image_kwargs.append(kwargs.get("image_infos"))
|
||||
self.reasoning_kwargs.append(kwargs.get("reasoning"))
|
||||
return {"message": [{"content": "完成"}], "tools": []}
|
||||
def parse_json_response(self, raw):
|
||||
return raw
|
||||
|
||||
|
||||
class AgentMultimodalTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_tool_followup_keeps_original_images(self):
|
||||
images = [{"url": "/tmp/original.png"}]
|
||||
model = MultiRoundEngine()
|
||||
initial = {"message": [], "tools": [{"name": "mail", "args": {"action": "list"}}]}
|
||||
channel = type("Channel", (), {"name": "tg", "is_admin": lambda self, uid: True})()
|
||||
with patch.object(agent_loop, "AgentTaskStore", FakeStore), \
|
||||
patch.object(agent_loop, "ToolExecutor", FakeExecutor):
|
||||
result = await agent_loop.run_agent_loop(
|
||||
model, initial, "system", "按图执行", channel, "private", 1, 2,
|
||||
image_infos=images,
|
||||
)
|
||||
self.assertEqual(result["message"][0]["content"], "完成")
|
||||
self.assertEqual(model.image_kwargs, [images])
|
||||
self.assertEqual(model.reasoning_kwargs, [True])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user