hyperagents 1.2.3rc5.post3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (361) hide show
  1. agents/__init__.py +31 -0
  2. agents/background_monitor.py +453 -0
  3. agents/learning.py +460 -0
  4. agents/planner.py +421 -0
  5. agents/transcriber.py +341 -0
  6. civilization/__init__.py +51 -0
  7. civilization/api/__init__.py +7 -0
  8. civilization/api/civilization_api.py +361 -0
  9. civilization/graph/__init__.py +19 -0
  10. civilization/graph/knowledge_linker.py +348 -0
  11. civilization/graph/org_graph.py +348 -0
  12. civilization/graph/process_graph.py +386 -0
  13. civilization/ingestion/__init__.py +20 -0
  14. civilization/ingestion/chunker.py +273 -0
  15. civilization/ingestion/document_ingestor.py +358 -0
  16. civilization/ingestion/embedder.py +262 -0
  17. civilization/ingestion/extractor.py +224 -0
  18. civilization/interview/__init__.py +19 -0
  19. civilization/interview/gap_detector.py +380 -0
  20. civilization/interview/interviewer.py +323 -0
  21. civilization/interview/questioner.py +363 -0
  22. civilization/retrieval/__init__.py +17 -0
  23. civilization/retrieval/civilization_rag.py +316 -0
  24. civilization/retrieval/context_injector.py +342 -0
  25. civilization/retrieval/relevance_ranker.py +287 -0
  26. civilization/schema.py +299 -0
  27. civilization/store.py +177 -0
  28. civilization/sync/__init__.py +16 -0
  29. civilization/sync/confluence_sync.py +317 -0
  30. civilization/sync/gdrive_sync.py +262 -0
  31. civilization/sync/notion_sync.py +237 -0
  32. civilization/versioning/__init__.py +19 -0
  33. civilization/versioning/diff_engine.py +351 -0
  34. civilization/versioning/staleness_detector.py +303 -0
  35. civilization/versioning/version_manager.py +293 -0
  36. cli/__init__.py +1 -0
  37. cli/doctor.py +334 -0
  38. cli/hyperclaw.py +1357 -0
  39. cli/onboarding.py +469 -0
  40. core/__init__.py +1 -0
  41. core/connectors/__init__.py +119 -0
  42. core/connectors/infrastructure.py +772 -0
  43. core/connectors/messaging.py +586 -0
  44. core/connectors/productivity.py +764 -0
  45. core/connectors/smarthome.py +528 -0
  46. core/connectors/social.py +730 -0
  47. core/hyperrouter/__init__.py +23 -0
  48. core/hyperrouter/bandit.py +218 -0
  49. core/hyperrouter/fast_loop.py +68 -0
  50. core/hyperrouter/slow_loop.py +143 -0
  51. core/hyperstate/__init__.py +24 -0
  52. core/hyperstate/certifier.py +154 -0
  53. core/hyperstate/schema.py +160 -0
  54. core/hyperstate/state_manager.py +103 -0
  55. core/hyperstate/store.py +163 -0
  56. core/providers/__init__.py +121 -0
  57. core/providers/embeddings.py +389 -0
  58. core/providers/image.py +534 -0
  59. core/providers/llm.py +1935 -0
  60. core/providers/search.py +608 -0
  61. core/providers/voice.py +474 -0
  62. hyperagents-1.2.3rc5.post3.dist-info/METADATA +469 -0
  63. hyperagents-1.2.3rc5.post3.dist-info/RECORD +361 -0
  64. hyperagents-1.2.3rc5.post3.dist-info/WHEEL +5 -0
  65. hyperagents-1.2.3rc5.post3.dist-info/entry_points.txt +6 -0
  66. hyperagents-1.2.3rc5.post3.dist-info/licenses/LICENSE +21 -0
  67. hyperagents-1.2.3rc5.post3.dist-info/top_level.txt +13 -0
  68. hyperclaw/__init__.py +67 -0
  69. hyperclaw/__main__.py +220 -0
  70. hyperclaw/agent.py +775 -0
  71. hyperclaw/agent_coordinator.py +577 -0
  72. hyperclaw/anthropic_params.py +225 -0
  73. hyperclaw/api_utils.py +85 -0
  74. hyperclaw/approval_telegram.py +209 -0
  75. hyperclaw/approval_terminal.py +80 -0
  76. hyperclaw/attachments.py +177 -0
  77. hyperclaw/branding.py +55 -0
  78. hyperclaw/brief/__init__.py +11 -0
  79. hyperclaw/brief/cli.py +204 -0
  80. hyperclaw/brief/collect.py +146 -0
  81. hyperclaw/brief/compose.py +333 -0
  82. hyperclaw/brief/diff.py +110 -0
  83. hyperclaw/brief/errors.py +99 -0
  84. hyperclaw/brief/google.py +269 -0
  85. hyperclaw/brief/google_auth.py +294 -0
  86. hyperclaw/brief/links.py +124 -0
  87. hyperclaw/brief/service.py +424 -0
  88. hyperclaw/brief/store.py +205 -0
  89. hyperclaw/brief/tasks.py +200 -0
  90. hyperclaw/chat_scope.py +115 -0
  91. hyperclaw/cli_tui.py +497 -0
  92. hyperclaw/client_guard.py +717 -0
  93. hyperclaw/connection_check.py +113 -0
  94. hyperclaw/cost_guard.py +1728 -0
  95. hyperclaw/default_config/agents.yaml +534 -0
  96. hyperclaw/default_config/hyperclaw.yaml +27 -0
  97. hyperclaw/default_config/models.yaml +161 -0
  98. hyperclaw/deployment.py +39 -0
  99. hyperclaw/digest/__init__.py +9 -0
  100. hyperclaw/digest/cli.py +117 -0
  101. hyperclaw/digest/errors.py +100 -0
  102. hyperclaw/digest/gitlab.py +187 -0
  103. hyperclaw/digest/http.py +113 -0
  104. hyperclaw/digest/redact.py +32 -0
  105. hyperclaw/digest/service.py +271 -0
  106. hyperclaw/digest/slack.py +223 -0
  107. hyperclaw/digest/state.py +124 -0
  108. hyperclaw/digest/summarize.py +198 -0
  109. hyperclaw/digest/tokens.py +143 -0
  110. hyperclaw/distribution.py +46 -0
  111. hyperclaw/documents.py +351 -0
  112. hyperclaw/feeds.py +339 -0
  113. hyperclaw/google_workspace/__init__.py +5 -0
  114. hyperclaw/google_workspace/api.py +151 -0
  115. hyperclaw/google_workspace/auth.py +397 -0
  116. hyperclaw/google_workspace/calendar.py +157 -0
  117. hyperclaw/google_workspace/cli.py +72 -0
  118. hyperclaw/google_workspace/drive.py +236 -0
  119. hyperclaw/google_workspace/errors.py +58 -0
  120. hyperclaw/google_workspace/gmail.py +295 -0
  121. hyperclaw/google_workspace/meet.py +122 -0
  122. hyperclaw/google_workspace/tools.py +319 -0
  123. hyperclaw/hyperspeed.py +57 -0
  124. hyperclaw/integrations_layer.py +1427 -0
  125. hyperclaw/live_state.py +188 -0
  126. hyperclaw/managed_config.py +245 -0
  127. hyperclaw/media_hub.py +179 -0
  128. hyperclaw/memory_manager.py +584 -0
  129. hyperclaw/model_catalog.py +551 -0
  130. hyperclaw/model_policy.py +211 -0
  131. hyperclaw/model_router.py +595 -0
  132. hyperclaw/model_selector.py +72 -0
  133. hyperclaw/naming.py +284 -0
  134. hyperclaw/notices.py +65 -0
  135. hyperclaw/oai_stream.py +106 -0
  136. hyperclaw/orchestrator.py +591 -0
  137. hyperclaw/outbox.py +82 -0
  138. hyperclaw/persona.py +367 -0
  139. hyperclaw/prometheus.py +219 -0
  140. hyperclaw/prompt_assembly.py +157 -0
  141. hyperclaw/provider_setup.py +146 -0
  142. hyperclaw/providers.py +340 -0
  143. hyperclaw/qualification.py +341 -0
  144. hyperclaw/qualification_keys/hyper-d292fd127cef5a3e.pub +1 -0
  145. hyperclaw/qualification_records/anthropic--claude-sonnet-5-5.json +14 -0
  146. hyperclaw/reasoning_display.py +35 -0
  147. hyperclaw/scheduler.py +454 -0
  148. hyperclaw/server.py +847 -0
  149. hyperclaw/setup.py +627 -0
  150. hyperclaw/skills.py +717 -0
  151. hyperclaw/solomon.py +442 -0
  152. hyperclaw/swarm.py +533 -0
  153. hyperclaw/taint.py +832 -0
  154. hyperclaw/team/__init__.py +28 -0
  155. hyperclaw/team/dispatcher.py +431 -0
  156. hyperclaw/team/genesis.py +106 -0
  157. hyperclaw/team/lead.py +110 -0
  158. hyperclaw/team/model.py +135 -0
  159. hyperclaw/team/morning.py +238 -0
  160. hyperclaw/team/repos.py +66 -0
  161. hyperclaw/team/roster.py +264 -0
  162. hyperclaw/team/specialist.py +235 -0
  163. hyperclaw/team/status.py +163 -0
  164. hyperclaw/team/watchers.py +112 -0
  165. hyperclaw/teamsetup/__init__.py +17 -0
  166. hyperclaw/teamsetup/checks.py +208 -0
  167. hyperclaw/teamsetup/item.py +391 -0
  168. hyperclaw/teamsetup/oauth_loopback.py +84 -0
  169. hyperclaw/teamsetup/onboard.py +550 -0
  170. hyperclaw/teamsetup/profile.py +82 -0
  171. hyperclaw/teamsetup/secrets.py +313 -0
  172. hyperclaw/teamsetup/selfcheck.py +295 -0
  173. hyperclaw/teamsetup/service.py +495 -0
  174. hyperclaw/teamsetup/slack_oauth.py +442 -0
  175. hyperclaw/teamsetup/tty.py +62 -0
  176. hyperclaw/telegram_bot.py +1106 -0
  177. hyperclaw/telegram_intake.py +126 -0
  178. hyperclaw/telegram_pairing.py +360 -0
  179. hyperclaw/terminal_provider.py +323 -0
  180. hyperclaw/tools/__init__.py +1 -0
  181. hyperclaw/tools/approval.py +949 -0
  182. hyperclaw/tools/args.py +212 -0
  183. hyperclaw/tools/catalog.py +244 -0
  184. hyperclaw/tools/classify.py +266 -0
  185. hyperclaw/tools/effects.py +724 -0
  186. hyperclaw/tools/egress.py +210 -0
  187. hyperclaw/tools/envelope.py +188 -0
  188. hyperclaw/tools/guard.py +367 -0
  189. hyperclaw/tools/ledger.py +339 -0
  190. hyperclaw/tools/receipts.py +70 -0
  191. hyperclaw/tools/registry.py +318 -0
  192. hyperclaw/tools/service_guard.py +151 -0
  193. hyperclaw/tools/surfaces.py +54 -0
  194. hyperclaw/tools/workspace.py +87 -0
  195. hyperclaw/trading/__init__.py +77 -0
  196. hyperclaw/truth_guard.py +791 -0
  197. hyperclaw/tts.py +124 -0
  198. hyperclaw/tui.py +4255 -0
  199. hyperclaw/tui_bridge.py +938 -0
  200. hyperclaw/unattended.py +60 -0
  201. hyperclaw/updates/__init__.py +54 -0
  202. hyperclaw/updates/check.py +131 -0
  203. hyperclaw/updates/index.py +178 -0
  204. hyperclaw/updates/install.py +110 -0
  205. hyperclaw/updates/managed.py +161 -0
  206. hyperclaw/updates/manifest.py +138 -0
  207. hyperclaw/updates/notices.py +86 -0
  208. hyperclaw/updates/paths.py +53 -0
  209. hyperclaw/updates/release.py +146 -0
  210. hyperclaw/updates/upgrade.py +253 -0
  211. hyperclaw/updates/versioning.py +104 -0
  212. hyperclaw/userconfig.py +119 -0
  213. hyperclaw/xai.py +83 -0
  214. integrations/__init__.py +31 -0
  215. integrations/automation/__init__.py +1 -0
  216. integrations/automation/make.py +87 -0
  217. integrations/automation/zapier.py +87 -0
  218. integrations/base.py +476 -0
  219. integrations/communication/__init__.py +1 -0
  220. integrations/communication/twilio.py +279 -0
  221. integrations/communication/zoom.py +290 -0
  222. integrations/data/__init__.py +1 -0
  223. integrations/data/google_analytics.py +135 -0
  224. integrations/data/postgres.py +182 -0
  225. integrations/data/supabase_connector.py +202 -0
  226. integrations/data/weather_feeds.py +450 -0
  227. integrations/developer/__init__.py +9 -0
  228. integrations/developer/github.py +406 -0
  229. integrations/developer/gitlab.py +298 -0
  230. integrations/enterprise/__init__.py +13 -0
  231. integrations/enterprise/confluence.py +266 -0
  232. integrations/enterprise/hubspot.py +293 -0
  233. integrations/enterprise/jira.py +314 -0
  234. integrations/enterprise/salesforce.py +286 -0
  235. integrations/finance/__init__.py +1 -0
  236. integrations/finance/polymarket_client.py +247 -0
  237. integrations/finance/quickbooks.py +281 -0
  238. integrations/finance/stripe.py +261 -0
  239. integrations/gateway.py +238 -0
  240. integrations/messaging/__init__.py +32 -0
  241. integrations/messaging/discord.py +159 -0
  242. integrations/messaging/email.py +401 -0
  243. integrations/messaging/imessage.py +443 -0
  244. integrations/messaging/signal.py +175 -0
  245. integrations/messaging/slack.py +217 -0
  246. integrations/messaging/sms.py +175 -0
  247. integrations/messaging/teams.py +208 -0
  248. integrations/messaging/telegram.py +260 -0
  249. integrations/messaging/whatsapp.py +203 -0
  250. integrations/productivity/__init__.py +17 -0
  251. integrations/productivity/airtable.py +217 -0
  252. integrations/productivity/asana.py +248 -0
  253. integrations/productivity/google/__init__.py +19 -0
  254. integrations/productivity/google/base.py +89 -0
  255. integrations/productivity/google/calendar.py +272 -0
  256. integrations/productivity/google/docs.py +202 -0
  257. integrations/productivity/google/drive.py +268 -0
  258. integrations/productivity/google/gmail.py +279 -0
  259. integrations/productivity/google/meet.py +218 -0
  260. integrations/productivity/google/sheets.py +222 -0
  261. integrations/productivity/google/tasks.py +199 -0
  262. integrations/productivity/linear.py +279 -0
  263. integrations/productivity/microsoft/__init__.py +13 -0
  264. integrations/productivity/microsoft/base.py +93 -0
  265. integrations/productivity/microsoft/calendar.py +207 -0
  266. integrations/productivity/microsoft/onedrive.py +222 -0
  267. integrations/productivity/microsoft/outlook.py +241 -0
  268. integrations/productivity/microsoft/sharepoint.py +211 -0
  269. integrations/productivity/notion.py +243 -0
  270. integrations/productivity/todoist.py +250 -0
  271. integrations/productivity/trello.py +244 -0
  272. integrations/storage/__init__.py +1 -0
  273. integrations/storage/box.py +279 -0
  274. integrations/storage/dropbox.py +215 -0
  275. integrations/telegram_inbound.py +179 -0
  276. integrations/telegram_poller.py +151 -0
  277. memory/__init__.py +1 -0
  278. memory/causal_graph.py +125 -0
  279. memory/impact_tracker.py +88 -0
  280. memory/vector_memory.py +436 -0
  281. models/__init__.py +12 -0
  282. models/chatjimmy_client.py +210 -0
  283. models/claude_client.py +235 -0
  284. models/claude_code_subagent.py +192 -0
  285. models/openai_client.py +120 -0
  286. models/router.py +162 -0
  287. recursive/discovery_loop.py +118 -0
  288. recursive/optimization_loop.py +135 -0
  289. recursive/validation_loop.py +38 -0
  290. security/__init__.py +1 -0
  291. security/audit_logger.py +106 -0
  292. security/filesystem_guard.py +100 -0
  293. security/hypershield.py +99 -0
  294. security/network_guard.py +97 -0
  295. security/policy_engine.py +198 -0
  296. swarm/__init__.py +1 -0
  297. swarm/agents/__init__.py +0 -0
  298. swarm/agents/base.py +230 -0
  299. swarm/agents/business/__init__.py +0 -0
  300. swarm/agents/business/counsel.py +31 -0
  301. swarm/agents/business/herald.py +32 -0
  302. swarm/agents/business/ledger.py +26 -0
  303. swarm/agents/business/nexus.py +30 -0
  304. swarm/agents/business/ops.py +30 -0
  305. swarm/agents/business/pipeline.py +28 -0
  306. swarm/agents/business/revenue.py +30 -0
  307. swarm/agents/business/sovereign.py +31 -0
  308. swarm/agents/business/strategos.py +27 -0
  309. swarm/agents/business/talent.py +27 -0
  310. swarm/agents/business/venture.py +32 -0
  311. swarm/agents/comms/__init__.py +1 -0
  312. swarm/agents/comms/cipher.py +32 -0
  313. swarm/agents/comms/echo_comms.py +31 -0
  314. swarm/agents/comms/envoy.py +31 -0
  315. swarm/agents/comms/herald_scheduler.py +33 -0
  316. swarm/agents/comms/pulse.py +29 -0
  317. swarm/agents/creative/__init__.py +0 -0
  318. swarm/agents/creative/author.py +27 -0
  319. swarm/agents/creative/lens.py +25 -0
  320. swarm/agents/executive/__init__.py +1 -0
  321. swarm/agents/intelligence/__init__.py +1 -0
  322. swarm/agents/intelligence/arbiter.py +30 -0
  323. swarm/agents/intelligence/sentinel.py +30 -0
  324. swarm/agents/media/__init__.py +0 -0
  325. swarm/agents/personal/__init__.py +0 -0
  326. swarm/agents/personal/atlas.py +27 -0
  327. swarm/agents/personal/hearth.py +26 -0
  328. swarm/agents/personal/midas.py +27 -0
  329. swarm/agents/personal/navigator.py +27 -0
  330. swarm/agents/personal/nourish.py +31 -0
  331. swarm/agents/personal/vitals.py +33 -0
  332. swarm/agents/recursive/__init__.py +0 -0
  333. swarm/agents/recursive/alchemist.py +73 -0
  334. swarm/agents/recursive/calibrator.py +45 -0
  335. swarm/agents/recursive/scout.py +97 -0
  336. swarm/agents/scientific/__init__.py +0 -0
  337. swarm/agents/scientific/cosmos.py +36 -0
  338. swarm/agents/scientific/gaia.py +26 -0
  339. swarm/agents/scientific/medicus.py +33 -0
  340. swarm/agents/scientific/oracle_agent.py +39 -0
  341. swarm/agents/scientific/scribe.py +39 -0
  342. swarm/agents/talent/__init__.py +0 -0
  343. swarm/agents/talent/deal.py +28 -0
  344. swarm/agents/talent/roster.py +29 -0
  345. swarm/agents/talent/scout.py +29 -0
  346. swarm/agents/talent/stage.py +27 -0
  347. swarm/agents/tech/__init__.py +0 -0
  348. swarm/agents/tech/aegis.py +28 -0
  349. swarm/agents/tech/bridge.py +28 -0
  350. swarm/agents/tech/forge.py +28 -0
  351. swarm/agents/trading/__init__.py +1 -0
  352. swarm/agents/trading/global_prediction_engine.py +611 -0
  353. swarm/agents/trading/polymarket_trader.py +165 -0
  354. swarm/agents/trading/prediction_strategist.py +218 -0
  355. swarm/autogen_bridge.py +68 -0
  356. swarm/bid_protocol.py +196 -0
  357. swarm/dispatcher.py +527 -0
  358. swarm/nexus.py +181 -0
  359. swarm/registry.py +112 -0
  360. telegram_listener.py +403 -0
  361. ui/tui/app.py +214 -0
@@ -0,0 +1,608 @@
1
+ """
2
+ Search Provider Abstraction Layer.
3
+ Unified interface for web search, academic search, and knowledge bases.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import logging
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+
12
+ import httpx
13
+
14
+ from hyperclaw.cost_guard import guarded_async
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class SearchResult:
21
+ """Single search result."""
22
+ title: str
23
+ url: str
24
+ snippet: str
25
+ raw: dict = field(default_factory=dict)
26
+
27
+
28
+ @dataclass
29
+ class SearchResponse:
30
+ """Search response containing multiple results."""
31
+ results: list[SearchResult]
32
+ total: int | None = None
33
+ raw: dict = field(default_factory=dict)
34
+
35
+
36
+ class SearchProvider(ABC):
37
+ """Abstract base for search providers."""
38
+
39
+ name: str = "base"
40
+
41
+ @abstractmethod
42
+ async def search(
43
+ self,
44
+ query: str,
45
+ num_results: int = 10,
46
+ **kwargs,
47
+ ) -> SearchResponse:
48
+ """Execute a search query."""
49
+ pass
50
+
51
+
52
+ # ═══════════════════════════════════════════════════════════════════════════════
53
+ # GOOGLE SEARCH (via Custom Search API)
54
+ # ═══════════════════════════════════════════════════════════════════════════════
55
+
56
+ class GoogleSearchProvider(SearchProvider):
57
+ """Google Custom Search API."""
58
+
59
+ name = "google"
60
+
61
+ def __init__(self, api_key: str | None = None, cx: str | None = None):
62
+ self.api_key = api_key or os.environ.get("GOOGLE_API_KEY")
63
+ self.cx = cx or os.environ.get("GOOGLE_CSE_ID")
64
+ self.base_url = "https://www.googleapis.com/customsearch/v1"
65
+
66
+ async def search(
67
+ self,
68
+ query: str,
69
+ num_results: int = 10,
70
+ **kwargs,
71
+ ) -> SearchResponse:
72
+ async with httpx.AsyncClient() as client:
73
+ response = await client.get(
74
+ self.base_url,
75
+ params={
76
+ "key": self.api_key,
77
+ "cx": self.cx,
78
+ "q": query,
79
+ "num": min(num_results, 10),
80
+ },
81
+ timeout=30.0,
82
+ )
83
+ response.raise_for_status()
84
+ data = response.json()
85
+
86
+ results = [
87
+ SearchResult(
88
+ title=item.get("title", ""),
89
+ url=item.get("link", ""),
90
+ snippet=item.get("snippet", ""),
91
+ raw=item,
92
+ )
93
+ for item in data.get("items", [])
94
+ ]
95
+
96
+ return SearchResponse(
97
+ results=results,
98
+ total=int(data.get("searchInformation", {}).get("totalResults", 0)),
99
+ raw=data,
100
+ )
101
+
102
+
103
+ # ═══════════════════════════════════════════════════════════════════════════════
104
+ # SERPAPI
105
+ # ═══════════════════════════════════════════════════════════════════════════════
106
+
107
+ class SerpAPIProvider(SearchProvider):
108
+ """SerpAPI - Google Search wrapper with structured results."""
109
+
110
+ name = "serpapi"
111
+
112
+ def __init__(self, api_key: str | None = None):
113
+ self.api_key = api_key or os.environ.get("SERPAPI_API_KEY")
114
+ self.base_url = "https://serpapi.com/search"
115
+
116
+ async def search(
117
+ self,
118
+ query: str,
119
+ num_results: int = 10,
120
+ **kwargs,
121
+ ) -> SearchResponse:
122
+ async with httpx.AsyncClient() as client:
123
+ response = await client.get(
124
+ self.base_url,
125
+ params={
126
+ "api_key": self.api_key,
127
+ "q": query,
128
+ "num": num_results,
129
+ "engine": "google",
130
+ },
131
+ timeout=30.0,
132
+ )
133
+ response.raise_for_status()
134
+ data = response.json()
135
+
136
+ results = [
137
+ SearchResult(
138
+ title=item.get("title", ""),
139
+ url=item.get("link", ""),
140
+ snippet=item.get("snippet", ""),
141
+ raw=item,
142
+ )
143
+ for item in data.get("organic_results", [])
144
+ ]
145
+
146
+ return SearchResponse(
147
+ results=results,
148
+ total=data.get("search_information", {}).get("total_results"),
149
+ raw=data,
150
+ )
151
+
152
+
153
+ # ═══════════════════════════════════════════════════════════════════════════════
154
+ # BING SEARCH
155
+ # ═══════════════════════════════════════════════════════════════════════════════
156
+
157
+ class BingSearchProvider(SearchProvider):
158
+ """Microsoft Bing Search API."""
159
+
160
+ name = "bing"
161
+
162
+ def __init__(self, api_key: str | None = None):
163
+ self.api_key = api_key or os.environ.get("BING_API_KEY")
164
+ self.base_url = "https://api.bing.microsoft.com/v7.0/search"
165
+
166
+ async def search(
167
+ self,
168
+ query: str,
169
+ num_results: int = 10,
170
+ **kwargs,
171
+ ) -> SearchResponse:
172
+ async with httpx.AsyncClient() as client:
173
+ response = await client.get(
174
+ self.base_url,
175
+ headers={"Ocp-Apim-Subscription-Key": self.api_key},
176
+ params={"q": query, "count": num_results},
177
+ timeout=30.0,
178
+ )
179
+ response.raise_for_status()
180
+ data = response.json()
181
+
182
+ results = [
183
+ SearchResult(
184
+ title=item.get("name", ""),
185
+ url=item.get("url", ""),
186
+ snippet=item.get("snippet", ""),
187
+ raw=item,
188
+ )
189
+ for item in data.get("webPages", {}).get("value", [])
190
+ ]
191
+
192
+ return SearchResponse(
193
+ results=results,
194
+ total=data.get("webPages", {}).get("totalEstimatedMatches"),
195
+ raw=data,
196
+ )
197
+
198
+
199
+ # ═══════════════════════════════════════════════════════════════════════════════
200
+ # DUCKDUCKGO
201
+ # ═══════════════════════════════════════════════════════════════════════════════
202
+
203
+ class DuckDuckGoProvider(SearchProvider):
204
+ """DuckDuckGo Instant Answer API (free, no API key required)."""
205
+
206
+ name = "duckduckgo"
207
+
208
+ def __init__(self):
209
+ self.base_url = "https://api.duckduckgo.com"
210
+
211
+ async def search(
212
+ self,
213
+ query: str,
214
+ num_results: int = 10,
215
+ **kwargs,
216
+ ) -> SearchResponse:
217
+ async with httpx.AsyncClient() as client:
218
+ response = await client.get(
219
+ self.base_url,
220
+ params={"q": query, "format": "json", "no_html": 1},
221
+ timeout=30.0,
222
+ )
223
+ response.raise_for_status()
224
+ data = response.json()
225
+
226
+ results = []
227
+
228
+ # Abstract result
229
+ if data.get("Abstract"):
230
+ results.append(
231
+ SearchResult(
232
+ title=data.get("Heading", ""),
233
+ url=data.get("AbstractURL", ""),
234
+ snippet=data.get("Abstract", ""),
235
+ raw=data,
236
+ )
237
+ )
238
+
239
+ # Related topics
240
+ for topic in data.get("RelatedTopics", [])[:num_results - len(results)]:
241
+ if isinstance(topic, dict) and topic.get("FirstURL"):
242
+ results.append(
243
+ SearchResult(
244
+ title=topic.get("Text", "")[:100],
245
+ url=topic.get("FirstURL", ""),
246
+ snippet=topic.get("Text", ""),
247
+ raw=topic,
248
+ )
249
+ )
250
+
251
+ return SearchResponse(results=results, raw=data)
252
+
253
+
254
+ # ═══════════════════════════════════════════════════════════════════════════════
255
+ # PERPLEXITY SEARCH
256
+ # ═══════════════════════════════════════════════════════════════════════════════
257
+
258
+ class PerplexitySearchProvider(SearchProvider):
259
+ """Perplexity AI - AI-powered search with citations."""
260
+
261
+ name = "perplexity"
262
+
263
+ def __init__(self, api_key: str | None = None):
264
+ self.api_key = api_key or os.environ.get("PERPLEXITY_API_KEY")
265
+ self.base_url = "https://api.perplexity.ai"
266
+
267
+ @guarded_async("perplexity", path="search") # an LLM answers this search: cost guard (spec §9)
268
+ async def search(
269
+ self,
270
+ query: str,
271
+ num_results: int = 10,
272
+ model: str = "llama-3.1-sonar-small-128k-online",
273
+ **kwargs,
274
+ ) -> SearchResponse:
275
+ async with httpx.AsyncClient() as client:
276
+ response = await client.post(
277
+ f"{self.base_url}/chat/completions",
278
+ headers={"Authorization": f"Bearer {self.api_key}"},
279
+ json={
280
+ "model": model,
281
+ "messages": [{"role": "user", "content": query}],
282
+ "max_tokens": int(kwargs.get("max_tokens") or 1024),
283
+ },
284
+ timeout=60.0,
285
+ )
286
+ response.raise_for_status()
287
+ data = response.json()
288
+
289
+ content = data["choices"][0]["message"]["content"]
290
+ citations = data.get("citations", [])
291
+
292
+ results = [
293
+ SearchResult(
294
+ title=f"Source {i + 1}",
295
+ url=url,
296
+ snippet=content[:200] if i == 0 else "",
297
+ raw={"url": url},
298
+ )
299
+ for i, url in enumerate(citations[:num_results])
300
+ ]
301
+
302
+ # Include the AI summary as the first result
303
+ if content:
304
+ results.insert(
305
+ 0,
306
+ SearchResult(
307
+ title="AI Summary",
308
+ url="",
309
+ snippet=content,
310
+ raw=data,
311
+ ),
312
+ )
313
+
314
+ return SearchResponse(results=results, raw=data)
315
+
316
+
317
+ # ═══════════════════════════════════════════════════════════════════════════════
318
+ # ARXIV
319
+ # ═══════════════════════════════════════════════════════════════════════════════
320
+
321
+ class ArxivProvider(SearchProvider):
322
+ """arXiv academic paper search."""
323
+
324
+ name = "arxiv"
325
+
326
+ def __init__(self):
327
+ self.base_url = "http://export.arxiv.org/api/query"
328
+
329
+ async def search(
330
+ self,
331
+ query: str,
332
+ num_results: int = 10,
333
+ **kwargs,
334
+ ) -> SearchResponse:
335
+ async with httpx.AsyncClient() as client:
336
+ response = await client.get(
337
+ self.base_url,
338
+ params={
339
+ "search_query": f"all:{query}",
340
+ "start": 0,
341
+ "max_results": num_results,
342
+ "sortBy": "relevance",
343
+ },
344
+ timeout=30.0,
345
+ )
346
+ response.raise_for_status()
347
+
348
+ # Parse Atom XML
349
+ import xml.etree.ElementTree as ET
350
+ root = ET.fromstring(response.text)
351
+
352
+ ns = {"atom": "http://www.w3.org/2005/Atom"}
353
+ results = []
354
+
355
+ for entry in root.findall("atom:entry", ns):
356
+ title = entry.find("atom:title", ns)
357
+ summary = entry.find("atom:summary", ns)
358
+ link = entry.find("atom:id", ns)
359
+
360
+ results.append(
361
+ SearchResult(
362
+ title=title.text.strip() if title is not None else "",
363
+ url=link.text.strip() if link is not None else "",
364
+ snippet=summary.text.strip()[:500] if summary is not None else "",
365
+ raw={"entry": entry},
366
+ )
367
+ )
368
+
369
+ total_elem = root.find("{http://a9.com/-/spec/opensearch/1.1/}totalResults")
370
+ total = int(total_elem.text) if total_elem is not None else None
371
+
372
+ return SearchResponse(results=results, total=total)
373
+
374
+
375
+ # ═══════════════════════════════════════════════════════════════════════════════
376
+ # WIKIPEDIA
377
+ # ═══════════════════════════════════════════════════════════════════════════════
378
+
379
+ class WikipediaProvider(SearchProvider):
380
+ """Wikipedia search API."""
381
+
382
+ name = "wikipedia"
383
+
384
+ def __init__(self, language: str = "en"):
385
+ self.language = language
386
+ self.base_url = f"https://{language}.wikipedia.org/w/api.php"
387
+
388
+ async def search(
389
+ self,
390
+ query: str,
391
+ num_results: int = 10,
392
+ **kwargs,
393
+ ) -> SearchResponse:
394
+ async with httpx.AsyncClient() as client:
395
+ # Search for page titles
396
+ search_response = await client.get(
397
+ self.base_url,
398
+ params={
399
+ "action": "query",
400
+ "list": "search",
401
+ "srsearch": query,
402
+ "srlimit": num_results,
403
+ "format": "json",
404
+ },
405
+ timeout=30.0,
406
+ )
407
+ search_response.raise_for_status()
408
+ search_data = search_response.json()
409
+
410
+ results = []
411
+ for item in search_data.get("query", {}).get("search", []):
412
+ title = item.get("title", "")
413
+ snippet = item.get("snippet", "").replace("<span class=\"searchmatch\">", "").replace("</span>", "")
414
+ url = f"https://{self.language}.wikipedia.org/wiki/{title.replace(' ', '_')}"
415
+
416
+ results.append(
417
+ SearchResult(
418
+ title=title,
419
+ url=url,
420
+ snippet=snippet,
421
+ raw=item,
422
+ )
423
+ )
424
+
425
+ return SearchResponse(
426
+ results=results,
427
+ total=search_data.get("query", {}).get("searchinfo", {}).get("totalhits"),
428
+ raw=search_data,
429
+ )
430
+
431
+
432
+ # ═══════════════════════════════════════════════════════════════════════════════
433
+ # TAVILY (AI-optimized search)
434
+ # ═══════════════════════════════════════════════════════════════════════════════
435
+
436
+ class TavilyProvider(SearchProvider):
437
+ """Tavily AI-optimized search API."""
438
+
439
+ name = "tavily"
440
+
441
+ def __init__(self, api_key: str | None = None):
442
+ self.api_key = api_key or os.environ.get("TAVILY_API_KEY")
443
+ self.base_url = "https://api.tavily.com"
444
+
445
+ async def search(
446
+ self,
447
+ query: str,
448
+ num_results: int = 10,
449
+ **kwargs,
450
+ ) -> SearchResponse:
451
+ async with httpx.AsyncClient() as client:
452
+ response = await client.post(
453
+ f"{self.base_url}/search",
454
+ json={
455
+ "api_key": self.api_key,
456
+ "query": query,
457
+ "max_results": num_results,
458
+ "include_answer": True,
459
+ },
460
+ timeout=30.0,
461
+ )
462
+ response.raise_for_status()
463
+ data = response.json()
464
+
465
+ results = []
466
+
467
+ # Include AI answer if present
468
+ if data.get("answer"):
469
+ results.append(
470
+ SearchResult(
471
+ title="AI Answer",
472
+ url="",
473
+ snippet=data["answer"],
474
+ raw={"answer": data["answer"]},
475
+ )
476
+ )
477
+
478
+ # Add search results
479
+ for item in data.get("results", []):
480
+ results.append(
481
+ SearchResult(
482
+ title=item.get("title", ""),
483
+ url=item.get("url", ""),
484
+ snippet=item.get("content", "")[:500],
485
+ raw=item,
486
+ )
487
+ )
488
+
489
+ return SearchResponse(results=results, raw=data)
490
+
491
+
492
+ # ═══════════════════════════════════════════════════════════════════════════════
493
+ # BRAVE SEARCH
494
+ # ═══════════════════════════════════════════════════════════════════════════════
495
+
496
+ class BraveSearchProvider(SearchProvider):
497
+ """Brave Search API."""
498
+
499
+ name = "brave"
500
+
501
+ def __init__(self, api_key: str | None = None):
502
+ self.api_key = api_key or os.environ.get("BRAVE_API_KEY")
503
+ self.base_url = "https://api.search.brave.com/res/v1/web/search"
504
+
505
+ async def search(
506
+ self,
507
+ query: str,
508
+ num_results: int = 10,
509
+ **kwargs,
510
+ ) -> SearchResponse:
511
+ async with httpx.AsyncClient() as client:
512
+ response = await client.get(
513
+ self.base_url,
514
+ headers={
515
+ "X-Subscription-Token": self.api_key,
516
+ "Accept": "application/json",
517
+ },
518
+ params={"q": query, "count": num_results},
519
+ timeout=30.0,
520
+ )
521
+ response.raise_for_status()
522
+ data = response.json()
523
+
524
+ results = [
525
+ SearchResult(
526
+ title=item.get("title", ""),
527
+ url=item.get("url", ""),
528
+ snippet=item.get("description", ""),
529
+ raw=item,
530
+ )
531
+ for item in data.get("web", {}).get("results", [])
532
+ ]
533
+
534
+ return SearchResponse(results=results, raw=data)
535
+
536
+
537
+ # ═══════════════════════════════════════════════════════════════════════════════
538
+ # YOU.COM
539
+ # ═══════════════════════════════════════════════════════════════════════════════
540
+
541
+ class YouSearchProvider(SearchProvider):
542
+ """You.com Search API."""
543
+
544
+ name = "you"
545
+
546
+ def __init__(self, api_key: str | None = None):
547
+ self.api_key = api_key or os.environ.get("YOU_API_KEY")
548
+ self.base_url = "https://api.you.com/search"
549
+
550
+ async def search(
551
+ self,
552
+ query: str,
553
+ num_results: int = 10,
554
+ **kwargs,
555
+ ) -> SearchResponse:
556
+ async with httpx.AsyncClient() as client:
557
+ response = await client.get(
558
+ self.base_url,
559
+ headers={"X-API-Key": self.api_key},
560
+ params={"query": query},
561
+ timeout=30.0,
562
+ )
563
+ response.raise_for_status()
564
+ data = response.json()
565
+
566
+ results = [
567
+ SearchResult(
568
+ title=item.get("title", ""),
569
+ url=item.get("url", ""),
570
+ snippet=item.get("snippet", ""),
571
+ raw=item,
572
+ )
573
+ for item in data.get("hits", [])[:num_results]
574
+ ]
575
+
576
+ return SearchResponse(results=results, raw=data)
577
+
578
+
579
+ # ═══════════════════════════════════════════════════════════════════════════════
580
+ # PROVIDER FACTORY
581
+ # ═══════════════════════════════════════════════════════════════════════════════
582
+
583
+ SEARCH_PROVIDERS: dict[str, type[SearchProvider]] = {
584
+ "google": GoogleSearchProvider,
585
+ "serpapi": SerpAPIProvider,
586
+ "bing": BingSearchProvider,
587
+ "duckduckgo": DuckDuckGoProvider,
588
+ "ddg": DuckDuckGoProvider,
589
+ "perplexity": PerplexitySearchProvider,
590
+ "arxiv": ArxivProvider,
591
+ "wikipedia": WikipediaProvider,
592
+ "wiki": WikipediaProvider,
593
+ "tavily": TavilyProvider,
594
+ "brave": BraveSearchProvider,
595
+ "you": YouSearchProvider,
596
+ }
597
+
598
+
599
+ def get_search_provider(name: str | None = None, **kwargs) -> SearchProvider:
600
+ """Get a search provider by name."""
601
+ name = name or os.environ.get("HYPERCLAW_SEARCH_PROVIDER", "duckduckgo")
602
+ name = name.lower()
603
+
604
+ if name not in SEARCH_PROVIDERS:
605
+ available = ", ".join(sorted(SEARCH_PROVIDERS.keys()))
606
+ raise ValueError(f"Unknown search provider: {name}. Available: {available}")
607
+
608
+ return SEARCH_PROVIDERS[name](**kwargs)