150 lines
4.4 KiB
Python
150 lines
4.4 KiB
Python
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
from llmai.openai.client import OpenAIClient # type: ignore[import-not-found]
|
|
from llmai.shared.configs import OpenAIClientConfig # type: ignore[import-not-found]
|
|
from llmai.shared.schema import get_schema_as_dict # type: ignore[import-not-found]
|
|
from llmai.shared import Tool # type: ignore[import-not-found]
|
|
|
|
from enums.llm_provider import LLMProvider
|
|
from services.chat.llm_tools import build_chat_llm_tools
|
|
from services.chat.tools import ChatTools
|
|
|
|
|
|
def _sample_function_tools() -> list[Tool]:
|
|
return [
|
|
Tool(
|
|
name="getSlideAtIndex",
|
|
description="Read a slide",
|
|
input_schema={"type": "object", "properties": {}},
|
|
)
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("provider", "web_search_provider"),
|
|
[
|
|
(LLMProvider.OPENAI, "auto"),
|
|
(LLMProvider.OPENAI, "native"),
|
|
(LLMProvider.ANTHROPIC, "auto"),
|
|
(LLMProvider.GOOGLE, "auto"),
|
|
(LLMProvider.VERTEX, "auto"),
|
|
(LLMProvider.CUSTOM, "searxng"),
|
|
],
|
|
)
|
|
def test_build_chat_llm_tools_returns_only_function_tools(
|
|
monkeypatch,
|
|
provider,
|
|
web_search_provider,
|
|
):
|
|
monkeypatch.setenv("LLM", provider.value)
|
|
monkeypatch.setenv("WEB_SEARCH_PROVIDER", web_search_provider)
|
|
function_tools = _sample_function_tools()
|
|
|
|
tools = build_chat_llm_tools(function_tools)
|
|
|
|
assert len(tools) == 1
|
|
assert tools[0].name == "getSlideAtIndex"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("provider", "web_search_provider"),
|
|
[
|
|
(LLMProvider.OPENAI, "auto"),
|
|
(LLMProvider.OPENAI, "native"),
|
|
(LLMProvider.CUSTOM, "searxng"),
|
|
(LLMProvider.GOOGLE, "auto"),
|
|
],
|
|
)
|
|
def test_chat_tool_definitions_do_not_expose_web_search(
|
|
monkeypatch,
|
|
provider,
|
|
web_search_provider,
|
|
):
|
|
monkeypatch.setenv("LLM", provider.value)
|
|
monkeypatch.setenv("WEB_SEARCH_PROVIDER", web_search_provider)
|
|
monkeypatch.setenv("SEARXNG_BASE_URL", "http://127.0.0.1:8080")
|
|
|
|
tools = ChatTools(Mock()).get_tool_definitions()
|
|
|
|
assert not any(tool.name == "webSearch" for tool in tools)
|
|
|
|
|
|
def test_chat_tool_handler_rejects_web_search():
|
|
chat_tools = ChatTools(Mock())
|
|
|
|
assert "webSearch" not in chat_tools._tool_handlers
|
|
|
|
|
|
def test_chat_tool_parse_args_repairs_fenced_jsonish_payload():
|
|
assert ChatTools._parse_args(
|
|
"```json\n{index: 0, includeFullContent: true}\n```"
|
|
) == {"index": 0, "includeFullContent": True}
|
|
|
|
|
|
def test_chat_tools_expose_only_v2_tool_names():
|
|
assert [tool.name for tool in ChatTools(Mock()).get_tool_definitions()] == [
|
|
"addOutline",
|
|
"updateOutline",
|
|
"deleteOutline",
|
|
"addNewSlide",
|
|
"addNewSlideLayout",
|
|
"getAvailableLayouts",
|
|
"getAvailableBlocks",
|
|
"getContentSchemaFromLayoutId",
|
|
"getTemplateSummary",
|
|
"readSourceDocuments",
|
|
"searchSlide",
|
|
"getSlideAtIndex",
|
|
"saveSlide",
|
|
"updateSlide",
|
|
"deleteSlide",
|
|
"addElement",
|
|
"updateElement",
|
|
"deleteElement",
|
|
"addComponent",
|
|
"createComponent",
|
|
"updateComponent",
|
|
"deleteComponent",
|
|
"getPresentationTheme",
|
|
"setPresentationTheme",
|
|
"generateAssets",
|
|
]
|
|
|
|
|
|
def test_chat_tools_emit_openai_strict_compatible_schemas():
|
|
client = OpenAIClient(config=OpenAIClientConfig(api_key="test"))
|
|
tools = ChatTools(Mock()).get_tool_definitions()
|
|
|
|
for tool in tools:
|
|
schema = client._openai_schema(
|
|
get_schema_as_dict(tool.input_schema, strict=tool.strict),
|
|
strict=tool.strict,
|
|
)
|
|
_assert_openai_strict_schema(schema, tool.name)
|
|
|
|
|
|
def _assert_openai_strict_schema(node, tool_name: str):
|
|
if isinstance(node, list):
|
|
for item in node:
|
|
_assert_openai_strict_schema(item, tool_name)
|
|
return
|
|
|
|
if not isinstance(node, dict):
|
|
return
|
|
|
|
if node.get("type") == "array":
|
|
assert node.get("items") not in (None, {}), tool_name
|
|
|
|
if node.get("type") == "object" and isinstance(node.get("properties"), dict):
|
|
properties = set(node["properties"])
|
|
assert set(node.get("required") or []) == properties, tool_name
|
|
assert node.get("additionalProperties") is False, tool_name
|
|
|
|
for variant in node.get("anyOf") or []:
|
|
if isinstance(variant, dict):
|
|
assert "type" in variant or "$ref" in variant, tool_name
|
|
|
|
for value in node.values():
|
|
_assert_openai_strict_schema(value, tool_name)
|