Skip to content

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

Open
ilya-bogin-keenable wants to merge 21 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 21 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

Memtensor-AI commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

🤖 Open Code Review

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

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


1. src/memos/configs/internet_retriever.py (L90-L92)

The api_key field in KeenableSearchConfig overrides the base class BaseInternetRetrieverConfig.api_key: str = Field(...) (required, non-nullable) with str | None and a None default. This is a Liskov Substitution Principle violation: any code that receives a BaseInternetRetrieverConfig and accesses .api_key expecting a str will silently get None for Keenable instances, which can cause AttributeError/TypeError at runtime in generic consumers.

Consider either:

  1. Widening the base class declaration to api_key: str | None = Field(default=None, ...) (consistent with how search_engine_id is already declared), or
  2. Keeping the override but adding a Pydantic model_validator / @field_validator in KeenableSearchConfig to document and enforce the None-is-valid contract explicitly.
💡 Suggested Change

Before:

    api_key: str | None = Field(
        default=None, description="Keenable API key (optional; keyless by default)"
    )

After:

# Option 1: widen the base class (preferred, keeps LSP intact)
# In BaseInternetRetrieverConfig:
#   api_key: str | None = Field(default=None, description="API key for the search service")

# Option 2: keep the override, add an explicit validator in KeenableSearchConfig
    api_key: str | None = Field(
        default=None, description="Keenable API key (optional; keyless by default)"
    )

    @field_validator("api_key", mode="before")
    @classmethod
    def _coerce_api_key(cls, v: str | None) -> str | None:
        return (v or "").strip() or None

2. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L195-L199)

The broad except Exception silently swallows all failures — connection errors, timeouts, HTTP 4xx/5xx, and JSON decode errors — and returns an empty list indistinguishable from a legitimate zero-result response. The caller in internet_retriever_factory.py has no way to distinguish a real error from an empty result set, so failures are invisible at the application level.

Split the try block so each failure point is guarded separately, and either re-raise or use a typed sentinel so the caller can react:

try:
    resp = requests.post(...)
    resp.raise_for_status()
except requests.exceptions.Timeout:
    logger.error("Keenable search timed out for query: %s", query)
    return []
except requests.exceptions.RequestException as e:
    logger.error("Keenable search request failed: %s", e)
    return []

try:
    raw_results = resp.json().get("results", [])[:limit]
except ValueError as e:
    logger.error("Keenable search returned invalid JSON: %s", e)
    return []

This makes the origin of each failure obvious and avoids accidentally catching unrelated exceptions like MemoryError or KeyboardInterrupt.


3. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L60)

TextRank is instantiated once at __init__ time and then called concurrently from multiple threads via ContextThreadPoolExecutor in _convert_to_mem_items. jieba's TextRank maintains internal mutable state (segment caches, graph structures), so concurrent textrank() calls on the same instance can produce incorrect results or raise exceptions under load.

Either create a new TextRank() instance per call inside _process_result, or protect the shared instance with a threading.Lock:

from threading import Lock
# in __init__:
self._textrank_lock = Lock()

# in _process_result:
if lang == "zh":
    with self._textrank_lock:
        tags = self.zh_fast_keywords_extractor.textrank(summary, topK=3)[:3]

4. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L297)

embed([content])[0] assumes the returned list is non-empty. If embed() returns an empty list for any reason (internal error, tokenization producing zero tokens, etc.), this raises an IndexError inside the thread pool, which is then silently swallowed by the except Exception as e in _convert_to_mem_items.

Guard the call:

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

5. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L254-L255)

The except Exception around the ISO datetime parse is broader than necessary. Only ValueError (invalid format string) is expected here; catching all Exception types masks programming errors like AttributeError if publish_time is not a string. Narrow it:

except (ValueError, TypeError):
    publish_time = datetime.now().strftime("%Y-%m-%d")

6. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L237)

Deduplication keyed on item.memory (the full memory_text string) is fragile. Two results from the same URL with minor whitespace or date differences will both survive, while two different articles that happen to produce the same formatted string will be incorrectly merged. URL is a more stable and semantically correct deduplication key:

def _url(item):
    sources = item.metadata.sources
    return sources[0].url if sources else item.memory

unique_memory_items = {_url(item): item for item in memory_items}

7. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L240-L242)

parsed_goal is annotated as str but is used as an object with a .tags attribute in _extract_tags (hasattr(parsed_goal, 'tags')). The type annotation is incorrect and misleading. Change it to Any or the actual goal type used in the rest of the codebase:

def _process_result(
    self, result: dict, query: str, parsed_goal: Any, info: dict[str, Any], mode="fast"
) -> list[TextualMemoryItem]:

8. src/memos/memories/textual/tree_text_memory/retrieve/keenablesearch.py (L196-L198)

import traceback inside the except block is unconventional; it should be a top-level module import. Additionally, logger.error(f"...") eagerly formats the string even when the error level is filtered. Prefer the lazy %s style:

import traceback  # at the top of the file

# in the except block:
logger.error("Keenable search error: %s", traceback.format_exc())

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
- gate jieba behind require_python_package, like the bocha retriever, so an
  English-only install no longer fails at construction
- skip embedding when the content is empty
- lowercase GDP/AI in the keyword lists; the text is lowercased before matching
@Memtensor-AI Memtensor-AI added status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Aug 1, 2026
@Memtensor-AI
Memtensor-AI requested a review from WeiminLee August 1, 2026 05:56
@ilya-bogin-keenable

Copy link
Copy Markdown
Author

Аixed 4 of the 5: jieba is now gated behind require_python_package like the bocha retriever (an English-only install used to fail at construction), empty content is no longer embedded, and GDP/AI are lowercased so they can actually match.

Will not fix for #2. Forwarding the local mode to the API would break the default path: the search endpoint returns 400 for mode: "fast", and fast is the default the caller passes. The API's mode and this method's mode are different things, so I added a comment saying so instead.

@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-b3c50e54d556e024-20260804132254: 86/87 passed, 1 failed — these do NOT affect the PR verdict; review the branch manually.

Branch: feat/keenable-web-search

@Memtensor-AI Memtensor-AI added the status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 label Aug 4, 2026
@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-d929796b8f2bd83f-20260819141815: 136/136 passed — these do NOT affect the PR verdict; review the branch manually.

Branch: feat/keenable-web-search

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: ENV ISSUE

The test environment encountered an issue that requires manual attention.

Details: Executor error: Command failed: git clone --depth 1 --branch feat/keenable-web-search git@github.com:keenableai/MemOS.git /data/test-workspaces/0bb2eb3c8566c1ed/repo
Cloning into '/data/test-workspaces/0bb2eb3c8566c1ed/repo'...
Branch: feat/keenable-web-search

@Memtensor-AI

Copy link
Copy Markdown
Collaborator

⚠️ Automated Test Results: ENV ISSUE

The test environment encountered an issue that requires manual attention.

Details: Executor error: Command failed: git clone --depth 1 --branch feat/keenable-web-search git@github.com:keenableai/MemOS.git /data/test-workspaces/1a3c8d0e9d69e77a/repo
Cloning into '/data/test-workspaces/1a3c8d0e9d69e77a/repo'...
kex_exchange_identification: Connection closed by remote host
Connection closed by UNKNOWN port 65535
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
Branch: feat/keenable-web-search

@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: 5s [advisory, non-gating] AI-generated tests on branch test/auto-gen-6b2de16b7c284ae6-20260828124956: 123/125 passed, 2 failed — 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 Aug 28, 2026
@Memtensor-AI Memtensor-AI added status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Aug 29, 2026
@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-ffe1c10a605ed5dd-20260829140808: 106/107 passed, 1 failed — 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 Aug 29, 2026
@Memtensor-AI Memtensor-AI added status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Sep 1, 2026
@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-aa46054cf1f71848-20260901114424: 90/92 passed, 2 failed — 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 Sep 1, 2026
@Memtensor-AI Memtensor-AI added status:in-progress Someone or AI is working on it | 人工或 AI 正在处理 and removed status:ready Ready for implementation; waiting for assignee or AI dispatch | 可进入实现,等待认领或派发 labels Sep 2, 2026
@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-e42b97dfdcb50f4d-20260902191710: 109/111 passed, 2 failed — 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 Sep 2, 2026
@ilya-bogin-keenable

Copy link
Copy Markdown
Author

@WeiminLee @syzsunshine219 this has been approved twice and MERGEABLE since 2026-08-01, and the approval has now survived fourteen base merges; the diff is unchanged at 4 files, +346/-1.

I would rather stop refreshing it than keep it warm indefinitely, so any of three answers ends this for me: someone merges it, someone requests changes, or you tell me MemOS does not want a keyless search provider and I close it myself. This is my last bump either way.

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.