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:
2025-12-15 18:30:16 +01:00
co-authored by Claude Opus 4.5
parent 3e432d662e
commit 100ebeae52
13 changed files with 1287 additions and 425 deletions
+4 -184
View File
@@ -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