mirror of
https://github.com/open-jarvis/OpenJarvis.git
synced 2026-07-28 13:26:48 +00:00
* fix(channels): wire channel→agent handler and fix Telegram send pipeline * format code * add supported tests
294 lines
10 KiB
Python
294 lines
10 KiB
Python
"""Tests for tool_calls extraction in _OpenAICompatibleEngine and OllamaEngine."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
import httpx
|
|
|
|
from openjarvis.core.types import Message, Role
|
|
from openjarvis.engine._openai_compat import _OpenAICompatibleEngine
|
|
from openjarvis.engine.ollama import OllamaEngine
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _OpenAICompatibleEngine tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _TestEngine(_OpenAICompatibleEngine):
|
|
engine_id = "test"
|
|
_default_host = "http://localhost:9999"
|
|
|
|
|
|
class TestOpenAICompatToolCalls:
|
|
def test_no_tool_calls(self, respx_mock):
|
|
"""When no tool_calls in response, result has no tool_calls key."""
|
|
respx_mock.post("http://localhost:9999/v1/chat/completions").mock(
|
|
return_value=httpx.Response(
|
|
200,
|
|
json={
|
|
"choices": [
|
|
{"message": {"content": "Hi"}, "finish_reason": "stop"},
|
|
],
|
|
"usage": {
|
|
"prompt_tokens": 5,
|
|
"completion_tokens": 2,
|
|
"total_tokens": 7,
|
|
},
|
|
"model": "test",
|
|
},
|
|
)
|
|
)
|
|
engine = _TestEngine()
|
|
result = engine.generate(
|
|
[Message(role=Role.USER, content="Hello")],
|
|
model="test",
|
|
)
|
|
assert "tool_calls" not in result
|
|
assert result["content"] == "Hi"
|
|
|
|
def test_with_tool_calls(self, respx_mock):
|
|
"""Extract tool_calls from OpenAI-format response."""
|
|
respx_mock.post("http://localhost:9999/v1/chat/completions").mock(
|
|
return_value=httpx.Response(
|
|
200,
|
|
json={
|
|
"choices": [
|
|
{
|
|
"message": {
|
|
"content": None,
|
|
"tool_calls": [
|
|
{
|
|
"id": "call_abc",
|
|
"type": "function",
|
|
"function": {
|
|
"name": "calculator",
|
|
"arguments": '{"expression":"2+2"}',
|
|
},
|
|
}
|
|
],
|
|
},
|
|
"finish_reason": "tool_calls",
|
|
}
|
|
],
|
|
"usage": {
|
|
"prompt_tokens": 5,
|
|
"completion_tokens": 10,
|
|
"total_tokens": 15,
|
|
},
|
|
"model": "test",
|
|
},
|
|
)
|
|
)
|
|
engine = _TestEngine()
|
|
result = engine.generate(
|
|
[Message(role=Role.USER, content="Calculate 2+2")],
|
|
model="test",
|
|
)
|
|
assert "tool_calls" in result
|
|
assert len(result["tool_calls"]) == 1
|
|
tc = result["tool_calls"][0]
|
|
assert tc["id"] == "call_abc"
|
|
assert tc["name"] == "calculator"
|
|
assert tc["arguments"] == '{"expression":"2+2"}'
|
|
assert result["content"] == "" # None → ""
|
|
|
|
def test_tools_kwarg_passthrough(self, respx_mock):
|
|
"""Tools kwarg is spread into the payload via **kwargs."""
|
|
captured = {}
|
|
|
|
def capture(request):
|
|
captured["body"] = json.loads(request.content)
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"choices": [
|
|
{"message": {"content": "ok"}, "finish_reason": "stop"}
|
|
],
|
|
"usage": {},
|
|
"model": "test",
|
|
},
|
|
)
|
|
|
|
respx_mock.post("http://localhost:9999/v1/chat/completions").mock(
|
|
side_effect=capture,
|
|
)
|
|
engine = _TestEngine()
|
|
engine.generate(
|
|
[Message(role=Role.USER, content="Hello")],
|
|
model="test",
|
|
tools=[{"type": "function", "function": {"name": "calc"}}],
|
|
)
|
|
assert "tools" in captured["body"]
|
|
|
|
def test_multiple_tool_calls(self, respx_mock):
|
|
respx_mock.post("http://localhost:9999/v1/chat/completions").mock(
|
|
return_value=httpx.Response(
|
|
200,
|
|
json={
|
|
"choices": [
|
|
{
|
|
"message": {
|
|
"content": "",
|
|
"tool_calls": [
|
|
{
|
|
"id": "c1",
|
|
"type": "function",
|
|
"function": {"name": "a", "arguments": "{}"},
|
|
},
|
|
{
|
|
"id": "c2",
|
|
"type": "function",
|
|
"function": {"name": "b", "arguments": "{}"},
|
|
},
|
|
],
|
|
},
|
|
"finish_reason": "tool_calls",
|
|
}
|
|
],
|
|
"usage": {},
|
|
"model": "test",
|
|
},
|
|
)
|
|
)
|
|
engine = _TestEngine()
|
|
result = engine.generate(
|
|
[Message(role=Role.USER, content="Use tools")],
|
|
model="test",
|
|
)
|
|
assert len(result["tool_calls"]) == 2
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# OllamaEngine tests
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestOllamaToolCalls:
|
|
def test_no_tool_calls(self, respx_mock):
|
|
respx_mock.post("http://localhost:11434/api/chat").mock(
|
|
return_value=httpx.Response(
|
|
200,
|
|
json={
|
|
"message": {"content": "Hi"},
|
|
"model": "test",
|
|
"prompt_eval_count": 5,
|
|
"eval_count": 2,
|
|
},
|
|
)
|
|
)
|
|
engine = OllamaEngine()
|
|
result = engine.generate(
|
|
[Message(role=Role.USER, content="Hello")],
|
|
model="test",
|
|
)
|
|
assert "tool_calls" not in result
|
|
|
|
def test_with_tool_calls(self, respx_mock):
|
|
respx_mock.post("http://localhost:11434/api/chat").mock(
|
|
return_value=httpx.Response(
|
|
200,
|
|
json={
|
|
"message": {
|
|
"content": "",
|
|
"tool_calls": [
|
|
{
|
|
"function": {
|
|
"name": "calculator",
|
|
"arguments": '{"expression":"3*3"}',
|
|
},
|
|
}
|
|
],
|
|
},
|
|
"model": "test",
|
|
"prompt_eval_count": 5,
|
|
"eval_count": 3,
|
|
},
|
|
)
|
|
)
|
|
engine = OllamaEngine()
|
|
result = engine.generate(
|
|
[Message(role=Role.USER, content="3*3")],
|
|
model="test",
|
|
)
|
|
assert "tool_calls" in result
|
|
assert result["tool_calls"][0]["name"] == "calculator"
|
|
|
|
def test_tools_in_payload(self, respx_mock):
|
|
captured = {}
|
|
|
|
def capture(request):
|
|
captured["body"] = json.loads(request.content)
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"message": {"content": "ok"},
|
|
"model": "test",
|
|
},
|
|
)
|
|
|
|
respx_mock.post("http://localhost:11434/api/chat").mock(side_effect=capture)
|
|
engine = OllamaEngine()
|
|
engine.generate(
|
|
[Message(role=Role.USER, content="Hello")],
|
|
model="test",
|
|
tools=[{"type": "function", "function": {"name": "calc"}}],
|
|
)
|
|
assert "tools" in captured["body"]
|
|
|
|
def test_dict_arguments_serialized_to_json(self, respx_mock):
|
|
"""Ollama returns arguments as dict — engine must serialize."""
|
|
respx_mock.post("http://localhost:11434/api/chat").mock(
|
|
return_value=httpx.Response(
|
|
200,
|
|
json={
|
|
"message": {
|
|
"content": "",
|
|
"tool_calls": [
|
|
{
|
|
"function": {
|
|
"name": "calculator",
|
|
"arguments": {"expression": "3*3"},
|
|
},
|
|
}
|
|
],
|
|
},
|
|
"model": "test",
|
|
"prompt_eval_count": 5,
|
|
"eval_count": 3,
|
|
},
|
|
)
|
|
)
|
|
engine = OllamaEngine()
|
|
result = engine.generate(
|
|
[Message(role=Role.USER, content="3*3")],
|
|
model="test",
|
|
)
|
|
assert "tool_calls" in result
|
|
tc = result["tool_calls"][0]
|
|
assert isinstance(tc["arguments"], str)
|
|
assert json.loads(tc["arguments"]) == {
|
|
"expression": "3*3",
|
|
}
|
|
|
|
def test_no_tools_no_tools_key(self, respx_mock):
|
|
captured = {}
|
|
|
|
def capture(request):
|
|
captured["body"] = json.loads(request.content)
|
|
return httpx.Response(
|
|
200,
|
|
json={
|
|
"message": {"content": "ok"},
|
|
"model": "test",
|
|
},
|
|
)
|
|
|
|
respx_mock.post("http://localhost:11434/api/chat").mock(side_effect=capture)
|
|
engine = OllamaEngine()
|
|
engine.generate(
|
|
[Message(role=Role.USER, content="Hello")],
|
|
model="test",
|
|
)
|
|
assert "tools" not in captured["body"]
|