Initial commit
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import shutil
|
||||
import sys
|
||||
import types
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from opc.core.attachment_store import AttachmentRef
|
||||
from opc.core.config import LLMConfig
|
||||
from opc.plugins.office_ui.dispatcher import DispatchResult, Dispatcher, Intent
|
||||
from opc.plugins.office_ui.ws_handler import WSHandler
|
||||
|
||||
|
||||
def _load_provider_module():
|
||||
fake_litellm = types.ModuleType("litellm")
|
||||
fake_litellm.suppress_debug_info = False
|
||||
fake_litellm.drop_params = False
|
||||
fake_litellm.get_max_tokens = lambda *_args, **_kwargs: None
|
||||
fake_litellm.token_counter = lambda *_args, **_kwargs: 0
|
||||
fake_litellm.completion_cost = lambda **_kwargs: 0.0
|
||||
|
||||
async def _unused_completion(**_kwargs):
|
||||
raise AssertionError("acompletion should not be called in attachment capability tests")
|
||||
|
||||
fake_litellm.acompletion = _unused_completion
|
||||
fake_litellm.exceptions = types.SimpleNamespace(ContextWindowExceededError=RuntimeError)
|
||||
|
||||
with patch.dict(sys.modules, {"litellm": fake_litellm}):
|
||||
sys.modules.pop("opc.llm.provider", None)
|
||||
provider_module = importlib.import_module("opc.llm.provider")
|
||||
return importlib.reload(provider_module)
|
||||
|
||||
|
||||
class TestDispatcherAttachmentRouting(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self) -> None:
|
||||
self.engine = MagicMock()
|
||||
self.engine.store = MagicMock()
|
||||
self.engine.memory = MagicMock()
|
||||
self.engine.llm = None
|
||||
self.dispatcher = Dispatcher(self.engine, MagicMock())
|
||||
|
||||
async def test_handle_routes_messages_with_attachments_to_engine(self) -> None:
|
||||
self.dispatcher._classify_intent = AsyncMock(return_value=Intent.CONVERSATION)
|
||||
|
||||
result = await self.dispatcher.handle(
|
||||
"task-1",
|
||||
"Sent with attachments",
|
||||
has_attachments=True,
|
||||
)
|
||||
|
||||
self.assertEqual(result.route, "engine")
|
||||
self.assertEqual(result.intent, Intent.TASK_REQUEST)
|
||||
self.dispatcher._classify_intent.assert_not_awaited()
|
||||
|
||||
async def test_classify_intent_uses_fast_path_for_chinese_task_request(self) -> None:
|
||||
self.engine.llm = MagicMock()
|
||||
self.engine.llm.simple_chat = AsyncMock(return_value='{"intent":"CONVERSATION"}')
|
||||
|
||||
intent = await self.dispatcher._classify_intent(
|
||||
"帮我修复这个 bug",
|
||||
"task-1",
|
||||
None,
|
||||
)
|
||||
|
||||
self.assertEqual(intent, Intent.TASK_REQUEST)
|
||||
self.engine.llm.simple_chat.assert_not_awaited()
|
||||
|
||||
async def test_classify_intent_uses_fast_path_for_chinese_status_query(self) -> None:
|
||||
self.engine.llm = MagicMock()
|
||||
self.engine.llm.simple_chat = AsyncMock(return_value='{"intent":"TASK_REQUEST"}')
|
||||
|
||||
intent = await self.dispatcher._classify_intent(
|
||||
"现在进度怎么样了?",
|
||||
"task-1",
|
||||
None,
|
||||
)
|
||||
|
||||
self.assertEqual(intent, Intent.STATUS_QUERY)
|
||||
self.engine.llm.simple_chat.assert_not_awaited()
|
||||
|
||||
async def test_classify_intent_uses_fast_path_for_chinese_greeting(self) -> None:
|
||||
self.engine.llm = MagicMock()
|
||||
self.engine.llm.simple_chat = AsyncMock(return_value='{"intent":"TASK_REQUEST"}')
|
||||
|
||||
intent = await self.dispatcher._classify_intent(
|
||||
"你好",
|
||||
"task-1",
|
||||
None,
|
||||
)
|
||||
|
||||
self.assertEqual(intent, Intent.CONVERSATION)
|
||||
self.engine.llm.simple_chat.assert_not_awaited()
|
||||
|
||||
|
||||
class TestWSHandlerAttachmentDispatch(unittest.IsolatedAsyncioTestCase):
|
||||
async def asyncSetUp(self) -> None:
|
||||
self.engine = MagicMock()
|
||||
self.engine.project_id = "test-project"
|
||||
self.engine.store = None
|
||||
self.engine.memory = None
|
||||
self.engine.escalation = None
|
||||
self.handler = WSHandler(self.engine, MagicMock(), MagicMock(), MagicMock())
|
||||
self.handler.broadcast = AsyncMock()
|
||||
self.handler._process_session_message = AsyncMock()
|
||||
self.handler.dispatcher.handle = AsyncMock(
|
||||
return_value=DispatchResult(route="engine", intent=Intent.TASK_REQUEST)
|
||||
)
|
||||
|
||||
async def test_dispatch_session_message_marks_attachment_turns_for_engine(self) -> None:
|
||||
attachment_refs = [{
|
||||
"attachment_id": "att-image",
|
||||
"filename": "image.png",
|
||||
"mime_type": "image/png",
|
||||
"size_bytes": 4,
|
||||
"disk_path": "projects/test-project/attachments/att-image/image.png",
|
||||
}]
|
||||
|
||||
await self.handler._dispatch_session_message(
|
||||
"task-1",
|
||||
"Please inspect the image",
|
||||
session_id="session-1",
|
||||
attachment_refs=attachment_refs,
|
||||
)
|
||||
|
||||
self.handler.dispatcher.handle.assert_awaited_once_with(
|
||||
"task-1",
|
||||
"Please inspect the image",
|
||||
session_id="session-1",
|
||||
has_attachments=True,
|
||||
)
|
||||
self.handler._process_session_message.assert_awaited_once_with(
|
||||
"task-1",
|
||||
"Please inspect the image",
|
||||
session_id="session-1",
|
||||
attachment_refs=attachment_refs,
|
||||
)
|
||||
|
||||
|
||||
class TestLLMProviderAttachmentSupport(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.provider_module = _load_provider_module()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
sys.modules.pop("opc.llm.provider", None)
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.test_root = Path.cwd() / ".test_llm_provider_attachments"
|
||||
self.test_root.mkdir(parents=True, exist_ok=True)
|
||||
self.ref = AttachmentRef(
|
||||
attachment_id="att-image",
|
||||
filename="image.png",
|
||||
mime_type="image/png",
|
||||
size_bytes=4,
|
||||
disk_path="projects/test-project/attachments/att-image/image.png",
|
||||
)
|
||||
image_path = self.test_root / self.ref.disk_path
|
||||
image_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
image_path.write_bytes(b"\x89PNG")
|
||||
self.video_ref = AttachmentRef(
|
||||
attachment_id="att-video",
|
||||
filename="clip.mp4",
|
||||
mime_type="video/mp4",
|
||||
size_bytes=4,
|
||||
disk_path="projects/test-project/attachments/att-video/clip.mp4",
|
||||
)
|
||||
video_path = self.test_root / self.video_ref.disk_path
|
||||
video_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
video_path.write_bytes(b"\x00\x00\x00\x18ftyp")
|
||||
|
||||
def tearDown(self) -> None:
|
||||
shutil.rmtree(self.test_root, ignore_errors=True)
|
||||
|
||||
def test_prepare_user_message_content_supports_providerless_multimodal_models(self) -> None:
|
||||
provider = self.provider_module.LLMProvider(
|
||||
LLMConfig(default_model="gpt-5.4"),
|
||||
opc_home=self.test_root,
|
||||
)
|
||||
|
||||
content = provider.prepare_user_message_content(
|
||||
"Describe the image",
|
||||
attachment_refs=[self.ref.to_dict()],
|
||||
)
|
||||
|
||||
self.assertIsInstance(content, list)
|
||||
self.assertEqual(content[0]["type"], "text")
|
||||
self.assertEqual(content[1]["type"], "image_url")
|
||||
self.assertTrue(content[1]["image_url"]["url"].startswith("data:image/png;base64,"))
|
||||
|
||||
def test_prepare_user_message_content_supports_azure_prefixed_models(self) -> None:
|
||||
provider = self.provider_module.LLMProvider(
|
||||
LLMConfig(default_model="azure/gpt-4.1-mini"),
|
||||
opc_home=self.test_root,
|
||||
)
|
||||
|
||||
content = provider.prepare_user_message_content(
|
||||
"Describe the image",
|
||||
attachment_refs=[self.ref.to_dict()],
|
||||
)
|
||||
|
||||
self.assertIsInstance(content, list)
|
||||
self.assertEqual(content[1]["type"], "image_url")
|
||||
|
||||
def test_prepare_user_message_content_supports_poe_video_file_inputs(self) -> None:
|
||||
provider = self.provider_module.LLMProvider(
|
||||
LLMConfig(
|
||||
default_model="openai/gpt-5.4",
|
||||
api_base="https://api.poe.com/v1",
|
||||
),
|
||||
opc_home=self.test_root,
|
||||
)
|
||||
|
||||
content = provider.prepare_user_message_content(
|
||||
"Describe the video",
|
||||
attachment_refs=[self.video_ref.to_dict()],
|
||||
)
|
||||
|
||||
self.assertIsInstance(content, list)
|
||||
self.assertEqual(content[0]["type"], "text")
|
||||
self.assertEqual(content[1]["type"], "file")
|
||||
self.assertEqual(content[1]["file"]["filename"], "clip.mp4")
|
||||
self.assertTrue(content[1]["file"]["file_data"].startswith("data:video/mp4;base64,"))
|
||||
|
||||
def test_prepare_user_message_content_supports_openrouter_video_urls(self) -> None:
|
||||
provider = self.provider_module.LLMProvider(
|
||||
LLMConfig(
|
||||
default_model="google/gemini-2.5-flash",
|
||||
api_base="https://openrouter.ai/api/v1",
|
||||
),
|
||||
opc_home=self.test_root,
|
||||
)
|
||||
|
||||
content = provider.prepare_user_message_content(
|
||||
"Describe the video",
|
||||
attachment_refs=[self.video_ref.to_dict()],
|
||||
)
|
||||
|
||||
self.assertIsInstance(content, list)
|
||||
self.assertEqual(content[0]["type"], "text")
|
||||
self.assertEqual(content[1]["type"], "video_url")
|
||||
self.assertTrue(content[1]["video_url"]["url"].startswith("data:video/mp4;base64,"))
|
||||
Reference in New Issue
Block a user