Skip to content

Python: Add Keenable web search connector - #14390

Open
Ilya Bogin (ilya-bogin-keenable) wants to merge 2 commits into
microsoft:mainfrom
keenableai:python/keenable-text-search
Open

Python: Add Keenable web search connector#14390
Ilya Bogin (ilya-bogin-keenable) wants to merge 2 commits into
microsoft:mainfrom
keenableai:python/keenable-text-search

Conversation

@ilya-bogin-keenable

Copy link
Copy Markdown

Motivation and Context

Adds a Python TextSearch connector for Keenable web search, next to the Brave and Google connectors.

The difference from the two existing connectors is that it works without an API key. With no KEENABLE_API_KEY set it calls the public endpoint (POST https://api.keenable.ai/v1/search/public); setting a key switches to the authenticated endpoint and only lifts the rate limits. So a text search plugin can run on a fresh install with nothing but an LLM key.

I work at Keenable.

Description

semantic_kernel/connectors/keenable.py follows the shape of brave.py:

  • KeenableSearch(KernelBaseModel, TextSearch) with the same search() signature and the three output types (str, TextSearchResult, "Any" -> KeenableWebPage). TextSearchResult maps name=title, value=snippet (falling back to description), link=url.
  • KeenableSettings(KernelBaseSettings) with prefix KEENABLE_ and an optional api_key; an unset or empty key means the public endpoint.
  • Filters go through the existing SearchLambdaVisitor; supported fields are site, published_after, published_before (lambda x: x.site == 'learn.microsoft.com').
  • Same httpx.AsyncClient usage and the same ServiceInvalidRequestError on non-2xx. A 429 raises with a hint to set the key when running keyless, instead of returning an empty result.
  • The API has no offset parameter, so skip is applied client side: the request asks for top + skip results and drops the first skip; top + skip is capped at the API maximum of 50.
  • Every request carries X-Keenable-Title: semantic-kernel, which the public endpoint requires to identify the calling application.

Also: KeenableSearch and friends added to the connectors/search.py and search.pyi shims, KEENABLE_API_KEY in .env.example, a keenable_unit_test_env fixture in tests/unit/connectors/conftest.py, a keenable_text_search_as_plugin.py sample mirroring the Brave one, and a line in the samples README.

Tests: tests/unit/connectors/search/test_keenable_search.py (mocked AsyncClient: keyless endpoint and headers, keyed endpoint and X-API-Key, empty key treated as keyless, result mapping with the description fallback, client-side skip, site and date filters, 429 and other error paths, filter lambda parsing). ruff, ruff format, the pre-commit hooks and mypy on the connector module pass. A live keyless query through the connector returned 5 results in under a second.

No new dependencies.

Contribution Checklist

Adds KeenableSearch, a TextSearch connector for Keenable web search,
following the shape of the Brave connector: same search() signature and
output types, TextSearchResult mapped as name=title, value=snippet (or
description), link=url, filters through SearchLambdaVisitor (site,
published_after, published_before), httpx AsyncClient and
ServiceInvalidRequestError on failures.

The API key is optional. Without KEENABLE_API_KEY the connector calls the
public endpoint, which is rate limited per IP; a key switches to the
authenticated endpoint and lifts the limits. A 429 raises with a hint to
set the key. The API has no offset, so skip is applied client side.

Also adds the shim entries in connectors/search.py and search.pyi, a
conftest fixture, unit tests, a sample mirroring the Brave one, the
samples README line and KEENABLE_API_KEY in .env.example.
Copilot AI lite review requested due to automatic review settings September 7, 2026 15:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A couple of small but concrete issues were found (redundant max_results capping vs validation, and a sample logging argument mismatch) that should be addressed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds a new Python TextSearch connector for Keenable web search (including keyless/public operation), aligning it with the existing Brave/Google search connector patterns and integrating it into the connector shims, samples, and unit-test suite.

Changes:

  • Introduces semantic_kernel.connectors.keenable with settings, response/result models, filter support via SearchLambdaVisitor, and client-side skip handling.
  • Exposes the new connector via the connectors/search.py + search.pyi shims and adds KEENABLE_API_KEY to .env.example.
  • Adds a sample plugin integration and comprehensive unit tests + env fixture.
File summaries
File Description
python/semantic_kernel/connectors/keenable.py New Keenable web search connector implementation (keyless/public + keyed endpoints, mapping, filters, skip/top handling).
python/semantic_kernel/connectors/search.py Adds lazy-export mappings for Keenable connector symbols.
python/semantic_kernel/connectors/search.pyi Adds stub exports/imports for Keenable connector symbols.
python/tests/unit/connectors/conftest.py Adds keenable_unit_test_env fixture to set up test environment variables.
python/tests/unit/connectors/search/test_keenable_search.py New unit tests covering endpoints/headers, mapping, filters, skip behavior, and error paths.
python/samples/concepts/search/keenable_text_search_as_plugin.py New sample showing Keenable search as an auto-invoked plugin in chat.
python/samples/concepts/README.md Adds the Keenable sample to the concepts index.
python/.env.example Adds KEENABLE_API_KEY placeholder for configuration.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +265 to +269
def _build_request_body(self, query: str, options: SearchOptions) -> dict[str, str | int]:
body: dict[str, str | int] = {
"query": query or "",
"max_results": min(options.top + options.skip, MAX_RESULTS),
}
Comment on lines +89 to +90
if "count" in context.arguments:
print(f' Count: "{context.arguments["count"]}"')

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAF Automated Review — Iteration 1

Result: Findings reported
Scope: full PR (1 commit(s)): 1f8b2bb7857a
Model: gpt-5.6-sol-fast

Overview

The connector has clear keyed/keyless routing, bounded request sizes, typed response validation, request-scoped HTTP cleanup, AST-based filter parsing, and focused tests for its main result and error paths. One Medium compatibility defect remains: the newly advertised lazy exports resolve Keenable beneath the search module rather than the connectors package, so the facade import fails at runtime.

Reviewed the supplied pull-request change set across correctness, security/reliability, architecture, and failure behavior.
1 verified finding remained after source verification (1 medium) across 1 file. Details are attached to the affected lines below.

Affected areas: python/semantic_kernel/connectors/search.py

"BraveWebPages": ".brave",
"BraveWebPage": ".brave",
"BraveSearchResponse": ".brave",
"KeenableSearch": ".keenable",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new facade exports cannot be imported: __getattr__ resolves these relative names with package=__name__, so Python looks for semantic_kernel.connectors.search.keenable, but search is a module and the implementation is semantic_kernel.connectors.keenable. Consequently, from semantic_kernel.connectors.search import KeenableSearch raises ModuleNotFoundError even though the stub advertises it. Please resolve the lazy import against the connectors package and add a runtime facade-import test.

…able connector

- connectors/search.py: resolve the lazy relative imports against the
  connectors package instead of the facade module itself, so
  `from semantic_kernel.connectors.search import KeenableSearch` works;
  this also repairs the existing Brave and Google entries, and points the
  Google names at google_search.py. Adds a facade import test.
- connectors/keenable.py: send top + skip as max_results without the extra
  cap; _validate_options already rejects values over the API maximum.
- samples: log `top` in the sample filter; the function has no `count`
  parameter.
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.

2 participants