Skip to content

feat: add Keenable as a configurable internet search backend - #2072

Open
ilya-bogin-keenable wants to merge 5 commits into
MemTensor:mainfrom
keenableai:feat/keenable-web-search
Open

feat: add Keenable as a configurable internet search backend#2072
ilya-bogin-keenable wants to merge 5 commits into
MemTensor:mainfrom
keenableai:feat/keenable-web-search

Conversation

@ilya-bogin-keenable

Copy link
Copy Markdown

Summary

Adds Keenable as a new internet search backend alongside the existing Bocha / Tavily / Google / Bing / Xinyu retrievers, following the Tavily backend pattern (#1357). Additive and opt-in via INTERNET_SEARCH_BACKEND=keenable; existing backends are untouched.

Keenable is a web search API built for AI agents. Unlike the key-required backends it is keyless by default: with no key it calls the public endpoint (rate-limited), and an optional KEENABLE_API_KEY only lifts the cap.

Files changed

  • src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (new): InternetKeenableRetriever. No SDK dependency, a thin requests call. Keyless requests hit /v1/search/public; a configured key switches to /v1/search with an X-API-Key header. Attribution via X-Keenable-Title. Results map into TextualMemoryItem the same way the Tavily retriever does.
  • src/memos/configs/internet_retriever.py: KeenableSearchConfig (API key optional) + registration in InternetRetrieverConfigFactory.
  • src/memos/memories/textual/tree_text_memory/retrieve/internet_retriever_factory.py: register keenable + constructor branch.
  • src/memos/api/config.py: INTERNET_SEARCH_BACKEND=keenable branch (KEENABLE_API_KEY optional).

Testing

  • python -m py_compile on all changed files: passes.
  • ruff check on all changed files: passes.

Bryunyon and others added 2 commits July 7, 2026 16:16
Add Keenable alongside the existing bocha / tavily / google / bing / xinyu
internet retrievers, following the Tavily backend pattern.

- retrieve/keenablesearch.py: InternetKeenableRetriever. Keyless by default
  (no SDK, a thin requests call): with no key it hits /v1/search/public
  (rate-limited); a key switches to /v1/search with an X-API-Key header.
  Attribution via X-Keenable-Title. Results map into TextualMemoryItem
  exactly like the Tavily retriever.
- configs/internet_retriever.py: KeenableSearchConfig (api_key optional) and
  registration in InternetRetrieverConfigFactory.
- retrieve/internet_retriever_factory.py: register "keenable" + constructor.
- api/config.py: INTERNET_SEARCH_BACKEND=keenable branch (KEENABLE_API_KEY
  optional, keyless by default).

py_compile and ruff pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Memtensor-AI Memtensor-AI added area:memory 记忆存储、检索、更新、召回逻辑 area:api 云服务 / FastAPI / OpenAPI / MCP labels Jul 8, 2026
@Memtensor-AI
Memtensor-AI requested a review from bittergreen July 8, 2026 11:43
@ilya-bogin-keenable

Copy link
Copy Markdown
Author

Hey @bittergreen could you please take a look? Thanks!

@Memtensor-AI Memtensor-AI removed the area:api 云服务 / FastAPI / OpenAPI / MCP label Jul 13, 2026
@Memtensor-AI Memtensor-AI added area:api 云服务 / FastAPI / OpenAPI / MCP area:core MOS 编排层 / 框架底座 / 跨模块问题 status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 31, 2026
@Memtensor-AI
Memtensor-AI requested a review from WeiminLee July 31, 2026 19:09
@Memtensor-AI

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

Target: PR #2072
Task: 33586be3c2b3f89c
Base: main
Head: feat/keenable-web-search

🔍 OpenCodeReview found 5 issue(s) in this PR.


1. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L283)

The _process_result method unconditionally calls self.embedder.embed([content])[0] even when content is an empty string (e.g., when the API result has no description field). This has two risks:

  1. Passing an empty string to the embedder may produce a degenerate zero-vector or raise an exception depending on the implementation.
  2. If embed() returns an empty list for empty input, the [0] indexing raises an IndexError that will be caught silently by the outer try/except in _convert_to_mem_items, dropping the result entirely with only a generic error log.

Suggestion: guard against empty content before embedding, e.g.:

embedding = self.embedder.embed([content])[0] if content else []
💡 Suggested Change

Before:

                    embedding=self.embedder.embed([content])[0],

After:

                    embedding=self.embedder.embed([content])[0] if content else [],

2. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L179)

The json={"query": query, "mode": "pro"} payload hardcodes "mode": "pro" regardless of the mode parameter accepted by retrieve_from_internet. This means a caller passing mode='fast' still gets a pro-mode API response (potentially slower and more expensive), while the mode parameter only controls local memory_text formatting. This silent mismatch is misleading.

If the Keenable API's mode field is meant to map to the mode parameter, use it; otherwise rename the parameter or document the discrepancy clearly.

💡 Suggested Change

Before:

                json={"query": query, "mode": "pro"},

After:

                json={"query": query, "mode": mode},  # or document why mode is always 'pro'

3. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L66-L68)

Uppercase keyword strings "GDP" and "AI" in the keyword lists will never match because text is fully lowercased via .lower() before the membership tests. Those two entries in the lists are effectively dead code.

Fix: lowercase them to "gdp" and "ai".

💡 Suggested Change

Before:

            "economy": [
                "economy",
                "GDP",

After:

            "economy": [
                "economy",
                "gdp",

4. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L93-L95)

Same case-mismatch issue in the technology category: "AI" will never match the lowercased text. Should be "ai".

💡 Suggested Change

Before:

                "internet",
                "AI",
                "artificial intelligence",

After:

                "internet",
                "ai",
                "artificial intelligence",

5. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L52-L54)

import jieba.analyse and jieba.analyse.TextRank() are executed unconditionally in __init__, making jieba a hard runtime dependency of the entire retriever. However, zh_fast_keywords_extractor is only used inside _process_result when lang == "zh". If jieba is not installed, instantiating InternetKeenableRetriever raises ImportError even for purely English workloads.

The sibling BochaAISearchRetriever properly gates this with a @require_python_package(import_name="jieba", ...) decorator (see bochasearch.py). Apply the same decorator here for consistency and to give users a clear install message instead of a raw ImportError.

Generated by cloud-assistant via Open Code Review.

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

✅ Automated Test Results: PASSED

All tests passed (5/5 executed). memos_python_core/changed-python-source: 5/5. Duration: 4s [advisory, non-gating] AI-generated tests on branch test/auto-gen-33586be3c2b3f89c-20260801031509: 107/107 passed — these do NOT affect the PR verdict; review the branch manually.

Branch: feat/keenable-web-search

@Memtensor-AI Memtensor-AI added status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 and removed status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 labels Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:api 云服务 / FastAPI / OpenAPI / MCP area:core MOS 编排层 / 框架底座 / 跨模块问题 area:memory 记忆存储、检索、更新、召回逻辑 status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants