✨(back) add Staan web search tool#595
Conversation
|
Warning Review limit reached
Next review available in: 45 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
WalkthroughThis PR adds a new "Staan" web search tool integrated with PydanticAI, including market resolution from language, an authenticated Staan API client, response formatting/snippet filtering, and a tool entrypoint with retry-aware error handling. It also adds StaanSettings configuration, propagates conversation language into ContextDeps, updates tool description text, and adds tests. ChangesStaan web search tool
Estimated code review effort: 3 (Moderate) | ~25 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
src/backend/conversations/staan_settings.py (2)
34-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
PositiveIntegerValueforSTAAN_MAX_RESULTSandSTAAN_MAX_SNIPPET_LENGTH.Both represent counts/lengths that should never be zero or negative.
IntegerValuepermits invalid values. Other comparable settings (RAG_WEB_SEARCH_MAX_RESULTS,TAVILY_MAX_RESULTS) usePositiveIntegerValue.♻️ Proposed fix
- STAAN_MAX_RESULTS = values.IntegerValue( + STAAN_MAX_RESULTS = values.PositiveIntegerValue( default=10, environ_name="STAAN_MAX_RESULTS", environ_prefix=None, ) - STAAN_MAX_SNIPPET_LENGTH = values.IntegerValue( + STAAN_MAX_SNIPPET_LENGTH = values.PositiveIntegerValue( default=5000, help_text="Maximum length of the snippets per url to return (in characters)", environ_name="STAAN_MAX_SNIPPET_LENGTH", environ_prefix=None, )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/conversations/staan_settings.py` around lines 34 - 44, Use PositiveIntegerValue for STAAN_MAX_RESULTS and STAAN_MAX_SNIPPET_LENGTH in staan_settings.py because both settings must stay strictly positive. Update the two declarations in the settings class to match the pattern used by RAG_WEB_SEARCH_MAX_RESULTS and TAVILY_MAX_RESULTS, keeping the same defaults and environment names while switching from IntegerValue to PositiveIntegerValue.
14-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
PositiveIntegerValueforSTAAN_API_TIMEOUTfor consistency and safety.Other timeout settings in the codebase (
TAVILY_API_TIMEOUT,ALBERT_API_TIMEOUT,FIND_API_TIMEOUT) all usePositiveIntegerValue. UsingIntegerValueallows zero or negative timeouts, which would cause undefined behavior in the HTTP request.♻️ Proposed fix
- STAAN_API_TIMEOUT = values.IntegerValue( + STAAN_API_TIMEOUT = values.PositiveIntegerValue( default=20, environ_name="STAAN_API_TIMEOUT", environ_prefix=None, )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/conversations/staan_settings.py` around lines 14 - 18, STAAN_API_TIMEOUT is using IntegerValue, which allows invalid zero or negative timeout values. Update the STAAN_API_TIMEOUT setting in the standings/conversations config to use PositiveIntegerValue, matching the existing timeout fields like TAVILY_API_TIMEOUT, ALBERT_API_TIMEOUT, and FIND_API_TIMEOUT, so the timeout validation stays consistent and safe.src/backend/chat/tools/web_search_staan.py (2)
68-73: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffConsider using an async HTTP client to avoid blocking the event loop.
staan_searchperforms a synchronousrequests.getcall, but it's invoked from the asyncweb_search_staanfunction. This blocks the event loop for the duration of the HTTP request (up toSTAAN_API_TIMEOUTseconds). If this is consistent with the existing Brave web search tool's pattern, this can be deferred.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/chat/tools/web_search_staan.py` around lines 68 - 73, The synchronous requests.get call inside staan_search is blocking the event loop when invoked from web_search_staan. Update this flow to use an async HTTP client or otherwise run the network call without blocking, and keep the existing timeout/header/params behavior intact. Use staan_search and web_search_staan as the main entry points to locate the change, and mirror the non-blocking pattern used by the Brave web search tool if that is the intended standard.
101-102: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueAvoid O(n²) string join inside the loop.
len(" ".join(extra_snippets))is recomputed on every iteration, joining the entire list each time. For a small number of snippets this is negligible, but a running length counter is simpler and O(n).♻️ Proposed refactor
extra_snippets: list[str] = [] + running_length = 0 for item in raw_snippets: if isinstance(item, dict): chunk = item.get("chunk", "") score = float(item.get("score", 0)) else: chunk = str(item) score = min_score if score < min_score: continue - current_length = len(" ".join(extra_snippets)) - if current_length + len(chunk) >= max_len_snippet: + if running_length + len(chunk) + (1 if extra_snippets else 0) >= max_len_snippet: break extra_snippets.append(chunk) + running_length += len(chunk) + (1 if extra_snippets else 0)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/chat/tools/web_search_staan.py` around lines 101 - 102, The snippet-length check in the extra_snippets loop recomputes the full joined string on every iteration, making it O(n²). Replace the repeated len(" ".join(extra_snippets)) call with a running length counter that is updated as chunks are appended, and use that counter in the max_len_snippet comparison. Keep the change localized to the loop that manages current_length, extra_snippets, chunk, and max_len_snippet.src/backend/chat/tests/tools/test_web_search_staan.py (1)
24-32: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a test case for prefix-only language mapping.
The parametrized cases cover exact market matches and unsupported fallback, but the prefix-mapping branch (
"fr"→"fr-fr","en"→"en-us","de"→"de-de") is never exercised. Adding a prefix-only case would cover that code path.✏️ Proposed addition
("nl-nl", "en-us"), + ("fr", "fr-fr"), + ("de", "de-de"), ], )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/backend/chat/tests/tools/test_web_search_staan.py` around lines 24 - 32, Add a parametrized test case in test_web_search_staan.py to cover the prefix-only language mapping branch in the language-to-market logic. Extend the existing test case list used by the test around the language parameterization so it includes a prefix-only input such as "fr", "en", or "de" and asserts the mapped market value returned by the relevant Web Search/Staan helper. This should exercise the branch that maps language prefixes to their corresponding markets rather than exact matches or unsupported fallbacks.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/backend/chat/tools/web_search_staan.py`:
- Line 174: The sources extraction in format_staan is using direct indexing on
response_data["web"]["results"] and a set comprehension, which can raise
KeyError on unexpected payloads and also drops ordering while keeping empty
URLs. Update the logic to use the same defensive access pattern as the rest of
format_staan, iterate over the results safely, and build sources in original
order while skipping empty URLs. Keep the fix localized around format_staan and
the sources assignment so the behavior stays consistent with the existing
response parsing.
---
Nitpick comments:
In `@src/backend/chat/tests/tools/test_web_search_staan.py`:
- Around line 24-32: Add a parametrized test case in test_web_search_staan.py to
cover the prefix-only language mapping branch in the language-to-market logic.
Extend the existing test case list used by the test around the language
parameterization so it includes a prefix-only input such as "fr", "en", or "de"
and asserts the mapped market value returned by the relevant Web Search/Staan
helper. This should exercise the branch that maps language prefixes to their
corresponding markets rather than exact matches or unsupported fallbacks.
In `@src/backend/chat/tools/web_search_staan.py`:
- Around line 68-73: The synchronous requests.get call inside staan_search is
blocking the event loop when invoked from web_search_staan. Update this flow to
use an async HTTP client or otherwise run the network call without blocking, and
keep the existing timeout/header/params behavior intact. Use staan_search and
web_search_staan as the main entry points to locate the change, and mirror the
non-blocking pattern used by the Brave web search tool if that is the intended
standard.
- Around line 101-102: The snippet-length check in the extra_snippets loop
recomputes the full joined string on every iteration, making it O(n²). Replace
the repeated len(" ".join(extra_snippets)) call with a running length counter
that is updated as chunks are appended, and use that counter in the
max_len_snippet comparison. Keep the change localized to the loop that manages
current_length, extra_snippets, chunk, and max_len_snippet.
In `@src/backend/conversations/staan_settings.py`:
- Around line 34-44: Use PositiveIntegerValue for STAAN_MAX_RESULTS and
STAAN_MAX_SNIPPET_LENGTH in staan_settings.py because both settings must stay
strictly positive. Update the two declarations in the settings class to match
the pattern used by RAG_WEB_SEARCH_MAX_RESULTS and TAVILY_MAX_RESULTS, keeping
the same defaults and environment names while switching from IntegerValue to
PositiveIntegerValue.
- Around line 14-18: STAAN_API_TIMEOUT is using IntegerValue, which allows
invalid zero or negative timeout values. Update the STAAN_API_TIMEOUT setting in
the standings/conversations config to use PositiveIntegerValue, matching the
existing timeout fields like TAVILY_API_TIMEOUT, ALBERT_API_TIMEOUT, and
FIND_API_TIMEOUT, so the timeout validation stays consistent and safe.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: f202ac06-fd3a-4bca-a332-61d33c9db9b3
📒 Files selected for processing (8)
env.d/development/kube-secret.distsrc/backend/chat/clients/pydantic_ai.pysrc/backend/chat/clients/schema.pysrc/backend/chat/tests/tools/test_web_search_staan.pysrc/backend/chat/tools/descriptions.pysrc/backend/chat/tools/web_search_staan.pysrc/backend/conversations/settings.pysrc/backend/conversations/staan_settings.py
| response_data = response.json() | ||
| api_market = response_data.get("query", {}).get("market") | ||
| logger.info("Staan API confirmed market=%s (requested=%s)", api_market, market) | ||
| sources = list({resp.get("url", "") for resp in response_data["web"]["results"]}) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use defensive key access and preserve source ordering.
Line 174 accesses response_data["web"]["results"] with direct indexing, while format_staan (line 137) uses .get("web", {}).get("results", []). If the API returns an unexpected structure, this raises KeyError, caught by the generic handler and surfaced as ModelCannotRetry — but the inconsistency is a latent bug. Additionally, the set comprehension {...} loses ordering and includes empty URL strings.
🛡️ Proposed fix
- sources = list({resp.get("url", "") for resp in response_data["web"]["results"]})
+ results = response_data.get("web", {}).get("results", [])
+ sources = list(dict.fromkeys(
+ resp.get("url", "") for resp in results if resp.get("url")
+ ))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| sources = list({resp.get("url", "") for resp in response_data["web"]["results"]}) | |
| results = response_data.get("web", {}).get("results", []) | |
| sources = list(dict.fromkeys( | |
| resp.get("url", "") for resp in results if resp.get("url") | |
| )) |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/backend/chat/tools/web_search_staan.py` at line 174, The sources
extraction in format_staan is using direct indexing on
response_data["web"]["results"] and a set comprehension, which can raise
KeyError on unexpected payloads and also drops ordering while keeping empty
URLs. Update the logic to use the same defensive access pattern as the rest of
format_staan, iterate over the results safely, and build sources in original
order while skipping empty URLs. Keep the fix localized around format_staan and
the sources assignment so the behavior stays consistent with the existing
response parsing.
ee47bcf to
eeac4c4
Compare
Add Stan as a configurable websearch
eeac4c4 to
cc08a8a
Compare
|



Purpose
Add Staan as an alternative web search provider for the chat agent, alongside the existing Brave integration. Staan is a European search API suitable for LLM/RAG use cases, with support for enriched snippets.
The provider is selectable per model via the existing
web_searchLLM configuration field, without changing the agent tool interface (web_search).Proposal
web_search_staantool calling the Staan Web Search for AI API (/v2/search/web)StaanSettings(API key, endpoint, timeout, extra snippets, max results, etc.) and wire it into Django settingsContextDeps.language), with fallback toen-usfor unsupported languagesContextDepsso tools use the same locale as the chat viewTest plan
STAAN_API_KEYin your environmentweb_searchconfig tochat.tools.web_search_staan.web_search_staanweb_searchfeature flag andallow_smart_web_searchfor the test userpytest chat/tests/tools/test_web_search_staan.pymarket=...andStaan API confirmed market=...Summary by CodeRabbit
New Features
Bug Fixes