mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
162 lines
7.1 KiB
Python
162 lines
7.1 KiB
Python
import asyncio
|
|
import json
|
|
|
|
import src.search as search
|
|
from src.agent_tools import TOOL_HANDLERS
|
|
from src.agent_tools.weather_tools import WeatherTool, weather_location_from_query
|
|
from src.agent_tools.web_tools import WebSearchTool
|
|
from src.agent_loop import _should_retry_empty_search_in_browser, _weather_tool_relevant
|
|
from src.tool_policy import WEB_TOOL_NAMES, ToolPolicy
|
|
from src.tool_schemas import FUNCTION_TOOL_SCHEMAS
|
|
from src.tool_types import ToolBlock
|
|
from src.tool_execution import NO_TOOL_SECURITY_CONTEXT, execute_tool_block
|
|
from tests.runtime_evidence_helpers import server_authorized_executor
|
|
|
|
execute_tool_block = server_authorized_executor(execute_tool_block)
|
|
from src.turn_contract import FAMILY_TOOLS
|
|
|
|
|
|
def test_weather_tool_is_registered_and_web_scoped():
|
|
assert "get_weather" in TOOL_HANDLERS
|
|
assert "get_weather" in WEB_TOOL_NAMES
|
|
assert "get_weather" in FAMILY_TOOLS["search_browser"]
|
|
assert any(item["function"]["name"] == "get_weather" for item in FUNCTION_TOOL_SCHEMAS)
|
|
|
|
|
|
def test_weather_tool_returns_timestamped_forecast(monkeypatch):
|
|
from src.agent_tools import weather_tools
|
|
|
|
def fake_get(url):
|
|
if "geocoding-api" in url:
|
|
return {"results": [{"name": "Tokyo", "admin1": "Tokyo", "country": "Japan",
|
|
"latitude": 35.68, "longitude": 139.69, "timezone": "Asia/Tokyo"}]}
|
|
return {"timezone": "Asia/Tokyo", "current": {"time": "2026-09-23T11:15", "temperature_2m": 22},
|
|
"daily": {"time": ["2026-09-23"], "temperature_2m_max": [25]}}
|
|
|
|
monkeypatch.setattr(weather_tools, "_get_json", fake_get)
|
|
result = asyncio.run(WeatherTool().execute('{"location":"Tokyo, Japan"}', {}))
|
|
data = json.loads(result["output"])
|
|
assert result["evidence_status"] == "available"
|
|
assert data["location"] == "Tokyo, Japan"
|
|
assert data["current"]["time"] == "2026-09-23T11:15"
|
|
assert data["source"].startswith("https://api.open-meteo.com/v1/forecast?")
|
|
|
|
|
|
def test_empty_dated_search_retries_without_filter(monkeypatch):
|
|
calls = []
|
|
|
|
def fake_search(query, **kwargs):
|
|
calls.append(kwargs["time_filter"])
|
|
if kwargs["time_filter"] is None:
|
|
return "Found a source", [{"title": "Source", "url": "https://example.org/source"}]
|
|
return "No search results found", []
|
|
|
|
monkeypatch.setattr(search, "comprehensive_web_search", fake_search)
|
|
result = asyncio.run(WebSearchTool().execute('{"query":"Tokyo weather today","time_filter":"day"}', {}))
|
|
assert calls == ["day", None]
|
|
assert result["evidence_status"] == "available"
|
|
|
|
|
|
def test_empty_news_search_does_not_accept_old_articles(monkeypatch):
|
|
calls = []
|
|
|
|
def fake_search(query, **kwargs):
|
|
calls.append(kwargs["time_filter"])
|
|
return "No search results found", []
|
|
|
|
monkeypatch.setattr(search, "comprehensive_web_search", fake_search)
|
|
result = asyncio.run(WebSearchTool().execute('{"query":"Tokyo news today","time_filter":"day"}', {}))
|
|
assert calls == ["day"]
|
|
assert result["evidence_status"] == "empty"
|
|
|
|
|
|
def test_weather_search_recovers_from_empty_results(monkeypatch):
|
|
from src.agent_tools import weather_tools
|
|
|
|
monkeypatch.setattr(search, "comprehensive_web_search", lambda *args, **kwargs: ("No search results found", []))
|
|
monkeypatch.setattr(weather_tools.WeatherTool, "execute", lambda self, content, ctx: _weather_result(content))
|
|
|
|
result = asyncio.run(WebSearchTool().execute(
|
|
'{"query":"Tokyo weather today September 23 2026 current weather","time_filter":"day"}', {}
|
|
))
|
|
assert result["evidence_status"] == "available"
|
|
assert json.loads(result["output"])["location"] == "Tokyo"
|
|
|
|
|
|
async def _weather_result(content):
|
|
return {"output": json.dumps({"location": json.loads(content)["location"]}),
|
|
"exit_code": 0, "evidence_status": "available"}
|
|
|
|
|
|
def test_weather_query_location_extraction_is_bounded():
|
|
assert weather_location_from_query("Tokyo weather today September 23 2026 current weather") == "Tokyo"
|
|
assert weather_location_from_query("What is the weather in Tokyo today?") == "Tokyo"
|
|
assert weather_location_from_query("weather today") is None
|
|
|
|
|
|
def test_weather_tool_is_only_relevant_for_weather_turns_and_followups():
|
|
assert _weather_tool_relevant([], "Weather Tokyo")
|
|
assert _weather_tool_relevant([], "Use get_weather for Tokyo")
|
|
assert not _weather_tool_relevant([], "Summarize the release notes")
|
|
history = [{"role": "user", "content": "Weather Tokyo"},
|
|
{"role": "assistant", "content": "Current conditions in Tokyo..."}]
|
|
assert _weather_tool_relevant(history, "What about tomorrow?")
|
|
assert _weather_tool_relevant(history, "And tmrw?")
|
|
assert _weather_tool_relevant(history, "Tmr?")
|
|
assert not _weather_tool_relevant(history, "Review my Python code")
|
|
assert not _weather_tool_relevant([], "And tmrw?")
|
|
|
|
|
|
def test_weather_followup_uses_recent_assistant_forecast_context():
|
|
from src.agent_loop import _looks_like_contextual_weather_status_followup
|
|
|
|
history = [
|
|
{"role": "user", "content": "What's weather today"},
|
|
{"role": "assistant", "content": "Today in Tokyo: 21 C and light rain."},
|
|
{"role": "user", "content": "And tmrw?"},
|
|
]
|
|
assert _looks_like_contextual_weather_status_followup(history, "And tmrw?")
|
|
assert _weather_tool_relevant(history, "And tmrw?")
|
|
assert not _weather_tool_relevant(history, "Review my Python code")
|
|
|
|
|
|
def test_web_search_does_not_bypass_disabled_weather_tool(monkeypatch):
|
|
from src.agent_tools import weather_tools
|
|
|
|
monkeypatch.setattr(search, "comprehensive_web_search", lambda *args, **kwargs: ("No search results found", []))
|
|
|
|
async def unexpected_weather(self, content, ctx):
|
|
raise AssertionError("weather tool was disabled")
|
|
|
|
monkeypatch.setattr(weather_tools.WeatherTool, "execute", unexpected_weather)
|
|
result = asyncio.run(WebSearchTool().execute(
|
|
'{"query":"Tokyo weather"}', {"disabled_tools": {"get_weather"}},
|
|
))
|
|
assert result["evidence_status"] == "empty"
|
|
|
|
|
|
def test_weather_and_browser_fallback_respect_explicit_tool_denial():
|
|
for tool, content in (
|
|
("get_weather", '{"location":"Tokyo"}'),
|
|
("private_browser", '{"action":"open","url":"https://www.bing.com"}'),
|
|
):
|
|
_, result = asyncio.run(execute_tool_block(
|
|
ToolBlock(tool, content), disabled_tools={tool},
|
|
security_context=NO_TOOL_SECURITY_CONTEXT,
|
|
))
|
|
assert result["exit_code"] == 1
|
|
assert "disabled by user" in result["error"]
|
|
|
|
|
|
def test_empty_search_browser_retry_is_bounded_and_policy_checked():
|
|
empty = {"evidence_status": "empty", "exit_code": 0}
|
|
assert _should_retry_empty_search_in_browser(empty, set(), None, False)
|
|
assert not _should_retry_empty_search_in_browser(empty, set(), None, True)
|
|
assert not _should_retry_empty_search_in_browser(empty, {"private_browser"}, None, False)
|
|
assert not _should_retry_empty_search_in_browser(
|
|
empty, set(), ToolPolicy(disabled_tools=frozenset({"private_browser"})), False,
|
|
)
|
|
assert not _should_retry_empty_search_in_browser(
|
|
{"evidence_status": "available"}, set(), None, False,
|
|
)
|