You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
134 lines
6.0 KiB
134 lines
6.0 KiB
import base64
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from mulit_agent_app.domain.attachments import TaskAttachment
|
|
from mulit_agent_app.infrastructure.controller_session import ControllerSession
|
|
from mulit_agent_app.infrastructure.openai_compatible_provider import OpenAICompatibleProvider
|
|
from mulit_agent_app.infrastructure.provider_factory import AnthropicProvider, GeminiProvider
|
|
|
|
|
|
def image(media_type="image/png"):
|
|
return {"name": "sample.png", "media_type": media_type, "data_url": f"data:{media_type};base64,{base64.b64encode(b'image').decode()}"}
|
|
|
|
|
|
def test_session_rejects_malformed_excessive_and_oversized_attachments():
|
|
with pytest.raises((TypeError, ValueError)):
|
|
ControllerSession._validated_attachments([{"name": "x"}])
|
|
with pytest.raises((TypeError, ValueError)):
|
|
ControllerSession._validated_attachments([image()] * 11)
|
|
huge = base64.b64encode(b"x" * (12 * 1024 * 1024 + 1)).decode()
|
|
with pytest.raises((TypeError, ValueError)):
|
|
ControllerSession._validated_attachments([{**image(), "data_url": f"data:image/png;base64,{huge}"}])
|
|
|
|
|
|
class Credentials:
|
|
def load_llm_api_key(self):
|
|
return "test-key"
|
|
|
|
|
|
class Response:
|
|
def __enter__(self): return self
|
|
def __exit__(self, *_): return False
|
|
def read(self): return json.dumps({"choices": [{"message": {"content": "ok"}}], "content": [{"text": "ok"}], "candidates": [{"content": {"parts": [{"text": "ok"}]}}]}).encode()
|
|
|
|
|
|
def test_provider_image_payloads(monkeypatch):
|
|
captured = []
|
|
def fake_urlopen(request, timeout):
|
|
captured.append(json.loads(request.data.decode()))
|
|
return Response()
|
|
monkeypatch.setattr("mulit_agent_app.infrastructure.openai_compatible_provider.urlopen", fake_urlopen)
|
|
monkeypatch.setattr("mulit_agent_app.infrastructure.provider_factory.urlopen", fake_urlopen)
|
|
attachment = TaskAttachment("sample.png", "image/png", image()["data_url"])
|
|
assert OpenAICompatibleProvider("https://example/v1", "model", Credentials()).generate("look", (attachment,)) == "ok"
|
|
assert AnthropicProvider("https://example", "model", Credentials()).generate("look", (attachment,)) == "ok"
|
|
assert GeminiProvider("https://example", "model", Credentials()).generate("look", (attachment,)) == "ok"
|
|
assert captured[0]["messages"][0]["content"][1]["type"] == "image_url"
|
|
assert captured[1]["messages"][0]["content"][1]["source"]["type"] == "base64"
|
|
assert captured[2]["contents"][0]["parts"][1]["inline_data"]["mime_type"] == "image/png"
|
|
|
|
|
|
def test_anthropic_and_gemini_receive_the_conversation_history(monkeypatch):
|
|
captured: list[dict[str, object]] = []
|
|
|
|
class Response:
|
|
def __init__(self, payload):
|
|
self.payload = payload
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_):
|
|
return False
|
|
|
|
def read(self):
|
|
return json.dumps(self.payload).encode()
|
|
|
|
def fake_urlopen(request, timeout):
|
|
captured.append(json.loads(request.data.decode()))
|
|
if request.full_url.endswith("/messages"):
|
|
return Response({"content": [{"type": "text", "text": "好的"}]})
|
|
return Response({"candidates": [{"content": {"parts": [{"text": "好的"}]}}]})
|
|
|
|
monkeypatch.setattr("mulit_agent_app.infrastructure.provider_factory.urlopen", fake_urlopen)
|
|
history = (("user", "下载到 D:\\demo"), ("assistant", "已下载到 D:\\demo\\a.zip"))
|
|
|
|
assert AnthropicProvider("https://example", "model", Credentials()).generate("把它复制到 D:\\target", history=history) == "好的"
|
|
assert GeminiProvider("https://example", "model", Credentials()).generate("把它复制到 D:\\target", history=history) == "好的"
|
|
|
|
assert captured[0]["messages"] == [
|
|
{"role": "user", "content": "下载到 D:\\demo"},
|
|
{"role": "assistant", "content": "已下载到 D:\\demo\\a.zip"},
|
|
{"role": "user", "content": [{"type": "text", "text": "把它复制到 D:\\target"}]},
|
|
]
|
|
assert captured[1]["contents"] == [
|
|
{"role": "user", "parts": [{"text": "下载到 D:\\demo"}]},
|
|
{"role": "model", "parts": [{"text": "已下载到 D:\\demo\\a.zip"}]},
|
|
{"role": "user", "parts": [{"text": "把它复制到 D:\\target"}]},
|
|
]
|
|
|
|
|
|
def test_anthropic_and_gemini_complete_local_tool_calls(monkeypatch):
|
|
responses = {
|
|
"anthropic": [
|
|
{"content": [{"type": "tool_use", "id": "anthropic-call", "name": "run_powershell", "input": {"command": "Write-Output anth"}}]},
|
|
{"content": [{"type": "text", "text": "Anthropic 已执行。"}]},
|
|
],
|
|
"gemini": [
|
|
{"candidates": [{"content": {"parts": [{"functionCall": {"name": "run_powershell", "args": {"command": "Write-Output gem"}}}]}}]},
|
|
{"candidates": [{"content": {"parts": [{"text": "Gemini 已执行。"}]}}]},
|
|
],
|
|
}
|
|
captured: list[dict[str, object]] = []
|
|
|
|
class ToolResponse:
|
|
def __init__(self, payload):
|
|
self.payload = payload
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *_):
|
|
return False
|
|
|
|
def read(self):
|
|
return json.dumps(self.payload).encode()
|
|
|
|
def fake_urlopen(request, timeout):
|
|
body = json.loads(request.data.decode())
|
|
captured.append(body)
|
|
provider = "anthropic" if request.full_url.endswith("/messages") else "gemini"
|
|
return ToolResponse(responses[provider].pop(0))
|
|
|
|
monkeypatch.setattr("mulit_agent_app.infrastructure.provider_factory.urlopen", fake_urlopen)
|
|
invoked: list[str] = []
|
|
anthropic = AnthropicProvider("https://example", "model", Credentials())
|
|
gemini = GeminiProvider("https://example", "model", Credentials())
|
|
|
|
assert anthropic.generate("执行", tool_executor=lambda command: invoked.append(command) or "已执行") == "Anthropic 已执行。"
|
|
assert gemini.generate("执行", tool_executor=lambda command: invoked.append(command) or "已执行") == "Gemini 已执行。"
|
|
assert invoked == ["Write-Output anth", "Write-Output gem"]
|
|
assert captured[0]["tools"]
|
|
assert captured[2]["tools"]
|
|
|