🐛(rag-websearch) fix the Albert API search query

This was not properly tested and the code changed just before
last commit... boom.
This commit is contained in:
Quentin BEY
2025-07-31 23:46:33 +02:00
parent f1c8a09378
commit 8358ca9490
6 changed files with 182 additions and 18 deletions
@@ -1,9 +1,9 @@
"""Constants and schemas for the Albert RAG agent from Albert API codebase."""
from enum import Enum
from typing import Any, Dict, List, Literal, Optional
from typing import Annotated, Any, Dict, List, Literal, Optional, Self
from pydantic import BaseModel, Field, constr, model_validator
from pydantic import BaseModel, Field, StringConstraints, model_validator
# - app/schemas/chunks.py
@@ -98,15 +98,13 @@ class SearchArgs(BaseModel):
rff_k: int = Field(default=20, description="k constant in RFF algorithm")
k: int = Field(gt=0, default=4, description="Number of results to return")
method: SearchMethod = Field(default=SearchMethod.SEMANTIC)
score_threshold: Optional[float] = (
Field(
default=0.0,
ge=0.0,
le=1.0,
description=(
"Score of cosine similarity threshold for filtering results, "
"only available for semantic search method."
),
score_threshold: Optional[float] = Field(
default=0.0,
ge=0.0,
le=1.0,
description=(
"Score of cosine similarity threshold for filtering results, "
"only available for semantic search method."
),
)
web_search: bool = Field(
@@ -115,24 +113,25 @@ class SearchArgs(BaseModel):
web_search_k: int = Field(default=5, description="Number of results to return for web search.")
@model_validator(mode="after")
def score_threshold_filter(self, values):
def score_threshold_filter(self) -> Self:
"""Validate the score threshold based on the search method."""
if values.score_threshold and values.method not in (
if self.score_threshold and self.method not in (
SearchMethod.SEMANTIC,
SearchMethod.MULTIAGENT,
):
raise ValueError(
"Score threshold is only available for semantic and multiagent search methods."
)
return values
return self
class SearchRequest(SearchArgs):
"""Model representing a search request in the Albert API."""
prompt: constr(strip_whitespace=True, min_length=1) = Field(
description="Prompt related to the search"
)
prompt: Annotated[
str,
StringConstraints(strip_whitespace=True, min_length=1),
] = Field(description="Prompt related to the search")
class Search(BaseModel):
@@ -90,7 +90,7 @@ class AlbertWebSearchManager(BaseWebSearchManager):
response = requests.post(
self._search_endpoint,
headers=self._headers,
json=search_request.model_dump(),
json=search_request.model_dump(mode="json", exclude_unset=True),
timeout=settings.ALBERT_API_TIMEOUT,
)
response.raise_for_status()
@@ -0,0 +1,60 @@
"""Test suite for albert_api_constants.py."""
import pytest
from pydantic import ValidationError
from chat.agent_rag.albert_api_constants import (
SearchArgs,
SearchMethod,
SearchRequest,
)
def test_search_request_model_dump_json():
"""Test that SearchRequest.model_dump(mode='json') works correctly."""
search_request = SearchRequest(prompt="test prompt")
assert search_request.model_dump(mode="json") == {
"collections": [],
"k": 4,
"method": "semantic",
"prompt": "test prompt",
"rff_k": 20,
"score_threshold": 0.0,
"web_search": False,
"web_search_k": 5,
}
def test_search_args_score_threshold_valid():
"""Test that score_threshold is valid for semantic and multiagent search methods."""
try:
SearchArgs(method=SearchMethod.SEMANTIC, score_threshold=0.5)
SearchArgs(method=SearchMethod.MULTIAGENT, score_threshold=0.5)
except ValidationError:
pytest.fail("ValidationError was raised unexpectedly for valid search methods.")
def test_search_args_score_threshold_invalid():
"""Test that score_threshold raises ValueError for hybrid and lexical search methods."""
with pytest.raises(ValidationError) as excinfo:
SearchArgs(method=SearchMethod.HYBRID, score_threshold=0.5)
assert "Score threshold is only available for semantic and multiagent search methods." in str(
excinfo.value
)
with pytest.raises(ValidationError) as excinfo:
SearchArgs(method=SearchMethod.LEXICAL, score_threshold=0.5)
assert "Score threshold is only available for semantic and multiagent search methods." in str(
excinfo.value
)
def test_search_args_no_score_threshold():
"""Test that no error is raised when score_threshold is not set."""
try:
SearchArgs(method=SearchMethod.HYBRID)
SearchArgs(method=SearchMethod.LEXICAL)
SearchArgs(method=SearchMethod.SEMANTIC)
SearchArgs(method=SearchMethod.MULTIAGENT)
except ValidationError:
pytest.fail("ValidationError was raised unexpectedly when score_threshold is not set.")
@@ -0,0 +1,105 @@
"""Unit tests for the Albert API web search manager."""
import json
import pytest
import requests
import responses
from chat.agent_rag.constants import RAGWebResult, RAGWebResults, RAGWebUsage
from chat.agent_rag.web_search.albert_api import AlbertWebSearchManager
@pytest.fixture(autouse=True)
def albert_api_settings(settings):
"""Fixture to set Albert API settings for tests."""
settings.ALBERT_API_URL = "http://test-albert-api.com"
settings.ALBERT_API_KEY = "test-key"
@pytest.mark.parametrize(
"url, expected",
[
("http://example.com/page.html", "http://example.com/page"),
("http://example.com/page", "http://example.com/page"),
("http://example.com/.html", "http://example.com/"),
],
)
def test_clean_url(url, expected):
"""Test the _clean_url static method."""
assert AlbertWebSearchManager._clean_url(url) == expected # pylint: disable=protected-access
@responses.activate
def test_web_search_success(settings):
"""Test a successful web search."""
settings.RAG_WEB_SEARCH_MAX_RESULTS = 20
settings.RAG_WEB_SEARCH_CHUNK_NUMBER = 10
mock_albert_api = responses.post(
"http://test-albert-api.com/v1/search",
json={
"data": [
{
"method": "semantic",
"chunk": {
"id": 123,
"content": "This is a test chunk.",
"metadata": {
"document_name": "http://example.com/test.html",
"document_type": "html",
},
},
"score": 0.9,
}
],
"usage": {"prompt_tokens": 10, "completion_tokens": 20},
},
status=200,
content_type="application/json",
)
results = AlbertWebSearchManager().web_search("test query")
assert results == RAGWebResults(
data=[
RAGWebResult(url="http://example.com/test", content="This is a test chunk.", score=0.9)
],
usage=RAGWebUsage(prompt_tokens=10, completion_tokens=20),
)
# Verify the request payload
request = mock_albert_api.calls[0].request
assert json.loads(request.body) == {
"prompt": "test query",
"web_search": True,
"web_search_k": 20, # Default value from settings
"k": 10, # Default value from settings
}
def test_web_search_empty_query():
"""Test web_search with an empty query."""
with pytest.raises(ValueError, match="Search query cannot be empty."):
AlbertWebSearchManager().web_search(" ")
@responses.activate
def test_web_search_http_error():
"""Test web_search with an HTTP error from the API."""
responses.post("http://test-albert-api.com/v1/search", status=500)
with pytest.raises(requests.HTTPError):
AlbertWebSearchManager().web_search("test query")
@responses.activate
def test_web_search_json_decode_error():
"""Test web_search with a JSON decode error from the API."""
responses.post(
"http://test-albert-api.com/v1/search",
body="invalid json",
status=200,
content_type="application/json",
)
with pytest.raises(requests.exceptions.JSONDecodeError):
AlbertWebSearchManager().web_search("test query")