5c62a21f32
全局重命名: - 包名: agent_eyes → agent_reach - CLI: agent-eyes → agent-reach - 类名: AgentEyes → AgentReach - 显示名: Agent Eyes → Agent Reach - GitHub: Panniantong/agent-eyes → Panniantong/Agent-Reach 所有 36 个测试通过,CLI/doctor/read/search 全部正常。
70 lines
2.1 KiB
Python
70 lines
2.1 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Exa semantic search — the search backbone for Agent Reach.
|
|
|
|
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 = "全网语义搜索(同时支持 Reddit/Twitter 搜索)"
|
|
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-reach 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
|