Skip to content

feat(search): add Keenable as an opt-in keyless search provider - #347

Open
ilya-bogin-keenable wants to merge 3 commits into
OpenByteInc:mainfrom
keenableai:feat/keenable-search
Open

ilya-bogin-keenable wants to merge 3 commits into
OpenByteInc:mainfrom
keenableai:feat/keenable-search

Conversation

@ilya-bogin-keenable

Copy link
Copy Markdown

Summary

Adds Keenable as an optional web search provider in backend_api_python/app/services/search.py, next to GDELT and DuckDuckGo as a free option. It needs no API key: set SEARCH_PROVIDER=keenable (it is added to the setting's options in routes/settings.py) and AI analysis and the research agent get web results with page text snippets and publish dates. An optional KEENABLE_API_KEY only raises the rate limits. Tavily stays the default and the existing fallback order is unchanged.

Related issue

  • None. No new runtime dependency (uses requests like the other REST providers), no route, schema or migration change.

Changes

  • KeenableSearchProvider(BaseSearchProvider): POST https://api.keenable.ai/v1/search/public without a key, POST https://api.keenable.ai/v1/search with X-API-Key when a key is set. Results map title, url, snippet (whitespace collapsed, capped at 500 characters like Tavily/SerpAPI/SearXNG) and published_at. days <= 30 becomes published_after (Google CSE applies dateRestrict up to the same 30-day cutoff); longer windows such as search_research send no date filter. HTTP errors and empty results return success=False, so the chain moves on to the next provider.
  • SearchService._init_providers: Keenable is registered only when SEARCH_PROVIDER=keenable or KEENABLE_API_KEY is set. When selected it goes first through the existing preferred logic; with only a key it is appended before DuckDuckGo. No other provider moves.
  • provider_status() and _search_provider_note(): a Keenable row and a setup note.
  • routes/settings.py: keenable in the SEARCH_PROVIDER options and an optional KEENABLE_API_KEY password field (Advanced tab, next to SERPAPI_KEYS).
  • utils/config_loader.py, .env.example, backend_api_python/env.example: KEENABLE_API_KEY mapping and an empty entry.
  • tests/test_keenable_search.py: 7 offline tests (keyless and keyed request shape, snippet mapping and cap, date window, error and empty fall-through, not registered by default, selected provider first, key-only placement).

What is sent to Keenable: the search query, max_results, and published_after for short windows, with an X-Keenable-Title: QuantDinger header (required by the keyless endpoint), plus the key only if one is set.

Test plan

  • Tested locally with docker compose up -d --build (not run; the search service was exercised directly, see below)

  • Backend logs show no errors (server not started)

  • Relevant pytest tests pass

  • python -m pytest -m "not integration and not stress" on Python 3.12 (requirements.lock + requirements-dev.txt), macOS: 3688 passed, 286 failed. The same 286 fail on main here (3681 passed); 233 of them are strategy sandbox memory limit could not be applied, which macOS does not support. The 7 new tests pass.

  • ruff check app scripts tests: passed.

  • Live run without a key through get_search_service() with SEARCH_PROVIDER=keenable (provider order Keenable -> GDELT -> DuckDuckGo): search_stock_news for NVDA and BTC, search_with_fallback (7 days), search_stock_events (30 days) and search_research each returned 5 results from Keenable in 0.4 to 1.9 s. Every result of the four news calls carried a published_at inside the requested window.

API documentation (if routes/schemas changed)

  • Not applicable: no route or OpenAPI schema change.

Disclosure: I work at Keenable, which runs this API.

Add a KeenableSearchProvider that is registered only when SEARCH_PROVIDER=keenable
or KEENABLE_API_KEY is set. Without a key it calls the keyless /v1/search/public
endpoint; a key switches to /v1/search and only raises rate limits. Default
provider and the existing fallback order are unchanged.

Signed-off-by: Ilya Bogin <ilya.bogin@keenable.ai>
Signed-off-by: Ilya Bogin <ilya.bogin@keenable.ai>
Signed-off-by: Ilya Bogin <ilya.bogin@keenable.ai>
@sonarqubecloud

sonarqubecloud Bot commented Nov 7, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant