feat: migrate web search from tatlock_core to Librarian
Move web search functionality to The Librarian agent, integrating with the library-desk /rag/search endpoint for enhanced search capabilities. Changes: - Add search_web, read_url, read_urls_batch tools to Librarian - Add WebSearchResult, ContentExtractionResult models to client - Add search_web, extract_content, extract_content_batch client methods - Update Librarian capability with web/url/internet domains - Remove search_web from tatlock_core tools and toolset - Update Tatlock system prompt to delegate web search to Librarian - Add comprehensive unit tests for new Librarian tools - Clean up legacy src/agents/tools.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+4
-184
@@ -1,17 +1,18 @@
|
||||
"""
|
||||
Tests for Tatlock's permanent tools (calculator, date/time, search).
|
||||
Tests for Tatlock's permanent tools (calculator, date/time).
|
||||
|
||||
Note: Web search has been moved to The Librarian agent.
|
||||
See tests/agents/librarian/test_tools.py for search tests.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from src.agents.tools import (
|
||||
calculate,
|
||||
get_current_datetime,
|
||||
calculate_time_offset,
|
||||
time_difference,
|
||||
search_web,
|
||||
)
|
||||
|
||||
|
||||
@@ -188,184 +189,3 @@ class TestDateTime:
|
||||
"""Test error handling for invalid dates."""
|
||||
result = time_difference("invalid-date", "now")
|
||||
assert "Error" in result
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Search Tests
|
||||
# ============================================================================
|
||||
|
||||
class TestSearch:
|
||||
"""Tests for web search tool."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_web_success(self):
|
||||
"""Test successful web search."""
|
||||
mock_response = {
|
||||
"results": [
|
||||
{
|
||||
"title": "Test Result 1",
|
||||
"url": "https://example.com/1",
|
||||
"content": "This is a test result"
|
||||
},
|
||||
{
|
||||
"title": "Test Result 2",
|
||||
"url": "https://example.com/2",
|
||||
"content": "Another test result"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with patch("src.agents.tools.httpx.AsyncClient") as mock_client_class:
|
||||
# Create mock response
|
||||
mock_response_obj = type('MockResponse', (), {
|
||||
'status_code': 200,
|
||||
'json': lambda *args, **kwargs: mock_response
|
||||
})()
|
||||
|
||||
# Create mock client with async get method
|
||||
async def mock_get(*args, **kwargs):
|
||||
return mock_response_obj
|
||||
|
||||
mock_client_instance = type('MockClient', (), {
|
||||
'get': mock_get
|
||||
})()
|
||||
|
||||
# Setup async context manager
|
||||
async def mock_aenter(*args, **kwargs):
|
||||
return mock_client_instance
|
||||
|
||||
async def mock_aexit(*args, **kwargs):
|
||||
return None
|
||||
|
||||
mock_client_class.return_value.__aenter__ = mock_aenter
|
||||
mock_client_class.return_value.__aexit__ = mock_aexit
|
||||
|
||||
result = await search_web("test query", num_results=2)
|
||||
|
||||
assert "Test Result 1" in result
|
||||
assert "https://example.com/1" in result
|
||||
assert "Test Result 2" in result
|
||||
assert "https://example.com/2" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_web_no_results(self):
|
||||
"""Test web search with no results."""
|
||||
mock_response_data = {"results": []}
|
||||
|
||||
with patch("src.agents.tools.httpx.AsyncClient") as mock_client_class:
|
||||
mock_response_obj = type('MockResponse', (), {
|
||||
'status_code': 200,
|
||||
'json': lambda *args, **kwargs: mock_response_data
|
||||
})()
|
||||
|
||||
async def mock_get(*args, **kwargs):
|
||||
return mock_response_obj
|
||||
|
||||
mock_client_instance = type('MockClient', (), {
|
||||
'get': mock_get
|
||||
})()
|
||||
|
||||
async def mock_aenter(*args, **kwargs):
|
||||
return mock_client_instance
|
||||
|
||||
async def mock_aexit(*args, **kwargs):
|
||||
return None
|
||||
|
||||
mock_client_class.return_value.__aenter__ = mock_aenter
|
||||
mock_client_class.return_value.__aexit__ = mock_aexit
|
||||
|
||||
result = await search_web("test query")
|
||||
|
||||
assert "No results found" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_web_connection_error(self):
|
||||
"""Test web search with connection error."""
|
||||
with patch("httpx.AsyncClient") as mock_client:
|
||||
mock_client_instance = AsyncMock()
|
||||
mock_client_instance.get.side_effect = Exception("Connection failed")
|
||||
mock_client.return_value.__aenter__.return_value = mock_client_instance
|
||||
|
||||
result = await search_web("test query")
|
||||
|
||||
assert "Error searching" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_web_limits_results(self):
|
||||
"""Test that search limits results to max 10."""
|
||||
mock_response_data = {
|
||||
"results": [
|
||||
{"title": f"Result {i}", "url": f"https://example.com/{i}", "content": "Test"}
|
||||
for i in range(20)
|
||||
]
|
||||
}
|
||||
|
||||
with patch("src.agents.tools.httpx.AsyncClient") as mock_client_class:
|
||||
mock_response_obj = type('MockResponse', (), {
|
||||
'status_code': 200,
|
||||
'json': lambda *args, **kwargs: mock_response_data
|
||||
})()
|
||||
|
||||
async def mock_get(*args, **kwargs):
|
||||
return mock_response_obj
|
||||
|
||||
mock_client_instance = type('MockClient', (), {
|
||||
'get': mock_get
|
||||
})()
|
||||
|
||||
async def mock_aenter(*args, **kwargs):
|
||||
return mock_client_instance
|
||||
|
||||
async def mock_aexit(*args, **kwargs):
|
||||
return None
|
||||
|
||||
mock_client_class.return_value.__aenter__ = mock_aenter
|
||||
mock_client_class.return_value.__aexit__ = mock_aexit
|
||||
|
||||
result = await search_web("test query", num_results=15)
|
||||
|
||||
# Should only return 10 results (max limit)
|
||||
result_count = result.count("URL:")
|
||||
assert result_count == 10
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_web_formats_results(self):
|
||||
"""Test that search results are properly formatted."""
|
||||
mock_response_data = {
|
||||
"results": [
|
||||
{
|
||||
"title": "Test Title",
|
||||
"url": "https://example.com",
|
||||
"content": "Test content description"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
with patch("src.agents.tools.httpx.AsyncClient") as mock_client_class:
|
||||
mock_response_obj = type('MockResponse', (), {
|
||||
'status_code': 200,
|
||||
'json': lambda *args, **kwargs: mock_response_data
|
||||
})()
|
||||
|
||||
async def mock_get(*args, **kwargs):
|
||||
return mock_response_obj
|
||||
|
||||
mock_client_instance = type('MockClient', (), {
|
||||
'get': mock_get
|
||||
})()
|
||||
|
||||
async def mock_aenter(*args, **kwargs):
|
||||
return mock_client_instance
|
||||
|
||||
async def mock_aexit(*args, **kwargs):
|
||||
return None
|
||||
|
||||
mock_client_class.return_value.__aenter__ = mock_aenter
|
||||
mock_client_class.return_value.__aexit__ = mock_aexit
|
||||
|
||||
result = await search_web("test query")
|
||||
|
||||
# Check formatting
|
||||
assert "1. Test Title" in result
|
||||
assert "URL: https://example.com" in result
|
||||
assert "Test content description" in result
|
||||
|
||||
Reference in New Issue
Block a user