Files
Agent-Reach/agent_eyes/channels/exa_search.py
T
Panniantong 74c3df5c3d v2.0.0 — Pure glue architecture: zero copied code, pluggable channels
BREAKING: Complete architectural rewrite.

Before: Copied x-reader's fetcher code into readers/ (1205 lines of borrowed code)
After: Pluggable channel system where each channel is a thin wrapper (~50 lines)
       around the best external tool for that platform. Zero copied code.

Architecture:
- channels/base.py — Universal Channel interface (read, search, check)
- channels/web.py — Jina Reader API (swappable)
- channels/github.py — GitHub API (swappable)
- channels/twitter.py — birdx + Jina fallback (swappable)
- channels/youtube.py — yt-dlp (swappable)
- channels/reddit.py — Reddit JSON API + proxy (swappable)
- channels/rss.py — feedparser (swappable)
- channels/bilibili.py — Bilibili API (swappable)
- channels/exa_search.py — Exa semantic search (swappable)

Key design: every backend can be swapped by changing ONE file.
YouTube dies? Change youtube.py. Exa sucks? Swap exa_search.py for Tavily.
Nothing else changes.

Removed: reader.py, schema.py, readers/, search/, utils/ (all x-reader code)
Tests: 36/36 passing
2026-02-24 05:38:21 +01:00

70 lines
2.1 KiB
Python

# -*- coding: utf-8 -*-
"""Exa semantic search — the search backbone for Agent Eyes.
Backend: Exa API (https://exa.ai) — free 1000 searches/month
Swap to: Tavily, SerpAPI, or any search API
"""
import os
import requests
from .base import Channel, SearchResult
from typing import List
class ExaSearchChannel(Channel):
name = "exa_search"
description = "Semantic web search (powers Reddit/Twitter search too)"
backends = ["Exa API"]
requires_config = ["exa_api_key"]
tier = 1
API_URL = "https://api.exa.ai/search"
def can_handle(self, url: str) -> bool:
return False # Search-only channel, doesn't read URLs
async def read(self, url: str, config=None) -> None:
raise NotImplementedError("Exa is a search engine, not a reader")
def _get_key(self, config=None) -> str:
if config:
key = config.get("exa_api_key")
if key:
return key
key = os.environ.get("EXA_API_KEY")
if key:
return key
raise ValueError(
"Exa API key not configured.\n"
"Get a free key at https://exa.ai (1000 searches/month free)\n"
"Then run: agent-eyes setup"
)
async def search(self, query: str, config=None, **kwargs) -> List[SearchResult]:
api_key = self._get_key(config)
limit = kwargs.get("limit", 5)
resp = requests.post(
self.API_URL,
headers={"Content-Type": "application/json", "x-api-key": api_key},
json={
"query": query,
"numResults": min(limit, 10),
"type": "auto",
"contents": {"text": {"maxCharacters": 500}},
},
timeout=15,
)
resp.raise_for_status()
results = []
for item in resp.json().get("results", []):
results.append(SearchResult(
title=item.get("title", ""),
url=item.get("url", ""),
snippet=item.get("text", ""),
date=item.get("publishedDate", ""),
score=item.get("score", 0),
))
return results