argo-search 1.0.1 → 2.9.7

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 (463) hide show
  1. package/LICENSE +201 -21
  2. package/README.en.md +822 -0
  3. package/README.es.md +864 -0
  4. package/README.ja.md +864 -0
  5. package/README.ko.md +864 -0
  6. package/README.md +867 -320
  7. package/SKILL.md +135 -0
  8. package/assets/readme/hero.svg +121 -0
  9. package/assets/readme/made-with-beautify.svg +10 -0
  10. package/assets/readme/proof-routes.svg +73 -0
  11. package/assets/readme/settings-keys.png +0 -0
  12. package/assets/readme/settings-tools.png +0 -0
  13. package/assets/readme/why-better.svg +104 -0
  14. package/assets/readme/workflow.svg +83 -0
  15. package/backends/domain_profiles.json +2308 -96
  16. package/backends/engine_registry.yaml +3314 -503
  17. package/backends/query_synonyms_cn.json +35 -0
  18. package/backends/quota_profiles.json +2409 -161
  19. package/backends/source_types_cn.json +83 -0
  20. package/backends/text_lexicon.json +44 -0
  21. package/bin/argo +703 -0
  22. package/bin/argo.js +71 -39
  23. package/config.yaml +4976 -363
  24. package/cordis.patch.yml +21 -0
  25. package/data/countries.json +40 -0
  26. package/data/lang_matrix/lang_capability.json +842 -0
  27. package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
  28. package/data/regions_cn.json +1 -0
  29. package/docs/ADDING_NEW_ENGINE.md +184 -0
  30. package/docs/ARGO_INTRO.md +124 -0
  31. package/docs/DATA_SOURCE_GAPS.md +81 -0
  32. package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
  33. package/docs/ENGINE_CATALOG.md +564 -0
  34. package/docs/MCP_SETUP.md +106 -0
  35. package/docs/PLATFORM_PATHS.md +111 -0
  36. package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
  37. package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
  38. package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
  39. package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
  40. package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
  41. package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
  42. package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
  43. package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
  44. package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
  45. package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
  46. package/docs/SEARCH_ARCHIVE.md +115 -0
  47. package/docs/assets/architecture.svg +104 -0
  48. package/docs/assets/hero.svg +98 -0
  49. package/docs/engines/bangumi.md +27 -0
  50. package/docs/engines/biorxiv.md +27 -0
  51. package/docs/engines/clawhub.md +27 -0
  52. package/docs/engines/crt_sh.md +27 -0
  53. package/docs/engines/datacite.md +27 -0
  54. package/docs/engines/deps_dev.md +27 -0
  55. package/docs/engines/douban_movie.md +27 -0
  56. package/docs/engines/endoflife.md +27 -0
  57. package/docs/engines/flk_law.md +27 -0
  58. package/docs/engines/fxtwitter.md +27 -0
  59. package/docs/engines/google_news.md +27 -0
  60. package/docs/engines/gov_regulations.md +27 -0
  61. package/docs/engines/guqian.md +27 -0
  62. package/docs/engines/hackernews.md +27 -0
  63. package/docs/engines/iplant.md +27 -0
  64. package/docs/engines/keenable.md +27 -0
  65. package/docs/engines/met_museum.md +27 -0
  66. package/docs/engines/nasa_images.md +27 -0
  67. package/docs/engines/netease_music.md +27 -0
  68. package/docs/engines/npm.md +27 -0
  69. package/docs/engines/nvd.md +27 -0
  70. package/docs/engines/openreview.md +27 -0
  71. package/docs/engines/openstd.md +27 -0
  72. package/docs/engines/parallel_free.md +27 -0
  73. package/docs/engines/people_daily.md +27 -0
  74. package/docs/engines/realtime_index.md +27 -0
  75. package/docs/engines/sec_edgar.md +27 -0
  76. package/docs/engines/seltz.md +27 -0
  77. package/docs/engines/skillsmp.md +27 -0
  78. package/docs/engines/sspai.md +27 -0
  79. package/docs/engines/std_samr.md +27 -0
  80. package/docs/engines/train.md +27 -0
  81. package/docs/engines/un_comtrade.md +27 -0
  82. package/docs/engines/wikisource.md +27 -0
  83. package/docs/engines/zdic.md +27 -0
  84. package/docs/engines/zenodo.md +27 -0
  85. package/docs/engines/zhihu_hot_app.md +27 -0
  86. package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
  87. package/engines/README.md +10 -0
  88. package/engines/_template_cli.yaml +39 -0
  89. package/engines/_template_http.yaml +37 -0
  90. package/engines/plugins/_template_plugin.py +50 -0
  91. package/engines/specs/_cisa_kev.yaml +54 -0
  92. package/engines/specs/_osv.yaml +63 -0
  93. package/engines/specs/aljazeera.yaml +43 -0
  94. package/engines/specs/antara.yaml +43 -0
  95. package/engines/specs/artic.yaml +20 -0
  96. package/engines/specs/aviation_weather.yaml +39 -0
  97. package/engines/specs/bangumi.yaml +21 -0
  98. package/engines/specs/biorxiv.yaml +24 -0
  99. package/engines/specs/carbon_intensity.yaml +32 -0
  100. package/engines/specs/cinii.yaml +44 -0
  101. package/engines/specs/clawhub.yaml +49 -0
  102. package/engines/specs/cleveland.yaml +19 -0
  103. package/engines/specs/cn_ai_news.yaml +43 -0
  104. package/engines/specs/core.yaml +35 -0
  105. package/engines/specs/crt_sh.yaml +38 -0
  106. package/engines/specs/ct24.yaml +43 -0
  107. package/engines/specs/datacite.yaml +27 -0
  108. package/engines/specs/datagov_uk.yaml +41 -0
  109. package/engines/specs/ddgs_images.yaml +45 -0
  110. package/engines/specs/deepseek_search.yaml +74 -0
  111. package/engines/specs/deezer.yaml +20 -0
  112. package/engines/specs/deps_dev.yaml +21 -0
  113. package/engines/specs/digup.yaml +36 -0
  114. package/engines/specs/doi.yaml +39 -0
  115. package/engines/specs/douban_movie.yaml +22 -0
  116. package/engines/specs/dw_news.yaml +47 -0
  117. package/engines/specs/egov_law.yaml +21 -0
  118. package/engines/specs/elpais.yaml +43 -0
  119. package/engines/specs/endoflife.yaml +21 -0
  120. package/engines/specs/energy_charts.yaml +21 -0
  121. package/engines/specs/factcheck_org.yaml +52 -0
  122. package/engines/specs/faz.yaml +43 -0
  123. package/engines/specs/federal_register.yaml +57 -0
  124. package/engines/specs/firecrawl.yaml +53 -0
  125. package/engines/specs/flk_law.yaml +24 -0
  126. package/engines/specs/folha.yaml +43 -0
  127. package/engines/specs/france24.yaml +44 -0
  128. package/engines/specs/full_fact.yaml +47 -0
  129. package/engines/specs/fxtwitter.yaml +31 -0
  130. package/engines/specs/gbfs_nyc.yaml +32 -0
  131. package/engines/specs/gdacs.yaml +21 -0
  132. package/engines/specs/gdelt.yaml +64 -0
  133. package/engines/specs/google_news.yaml +22 -0
  134. package/engines/specs/gov_regulations.yaml +38 -0
  135. package/engines/specs/guardian_rss.yaml +59 -0
  136. package/engines/specs/hurriyet.yaml +43 -0
  137. package/engines/specs/iplant.yaml +25 -0
  138. package/engines/specs/jikan.yaml +24 -0
  139. package/engines/specs/jolpica.yaml +20 -0
  140. package/engines/specs/k10plus.yaml +20 -0
  141. package/engines/specs/keenable.yaml +44 -0
  142. package/engines/specs/lefigaro.yaml +43 -0
  143. package/engines/specs/listenbrainz.yaml +19 -0
  144. package/engines/specs/matichon.yaml +43 -0
  145. package/engines/specs/met_museum.yaml +21 -0
  146. package/engines/specs/nasa_images.yaml +45 -0
  147. package/engines/specs/netease_music.yaml +38 -0
  148. package/engines/specs/nhk.yaml +43 -0
  149. package/engines/specs/nhtsa_vpic.yaml +20 -0
  150. package/engines/specs/noaa_swpc.yaml +20 -0
  151. package/engines/specs/nvd.yaml +38 -0
  152. package/engines/specs/obis.yaml +20 -0
  153. package/engines/specs/opencitations.yaml +52 -0
  154. package/engines/specs/openf1.yaml +21 -0
  155. package/engines/specs/openfoodfacts.yaml +45 -0
  156. package/engines/specs/openligadb.yaml +20 -0
  157. package/engines/specs/openreview.yaml +37 -0
  158. package/engines/specs/openstd.yaml +21 -0
  159. package/engines/specs/people_daily.yaml +22 -0
  160. package/engines/specs/realtime_index.yaml +34 -0
  161. package/engines/specs/ror.yaml +20 -0
  162. package/engines/specs/satnogs.yaml +20 -0
  163. package/engines/specs/sec_edgar.yaml +46 -0
  164. package/engines/specs/skillsmp.yaml +50 -0
  165. package/engines/specs/sogou_weixin_browser.yaml +30 -0
  166. package/engines/specs/soilgrids.yaml +25 -0
  167. package/engines/specs/sspai.yaml +36 -0
  168. package/engines/specs/stackexchange.yaml +46 -0
  169. package/engines/specs/std_samr.yaml +21 -0
  170. package/engines/specs/tass.yaml +43 -0
  171. package/engines/specs/tle_mirror.yaml +21 -0
  172. package/engines/specs/train.yaml +37 -0
  173. package/engines/specs/tvmaze.yaml +20 -0
  174. package/engines/specs/twitter_syndication.yaml +31 -0
  175. package/engines/specs/un_comtrade.yaml +23 -0
  176. package/engines/specs/unpaywall.yaml +54 -0
  177. package/engines/specs/vnpress.yaml +43 -0
  178. package/engines/specs/weather.yaml +32 -0
  179. package/engines/specs/who_don.yaml +21 -0
  180. package/engines/specs/who_gho.yaml +20 -0
  181. package/engines/specs/wikimedia_commons.yaml +34 -0
  182. package/engines/specs/wikisource.yaml +22 -0
  183. package/engines/specs/worms.yaml +19 -0
  184. package/engines/specs/x_trending.yaml +34 -0
  185. package/engines/specs/yna.yaml +43 -0
  186. package/engines/specs/ynet.yaml +43 -0
  187. package/engines/specs/zbmath.yaml +40 -0
  188. package/engines/specs/zdic.yaml +22 -0
  189. package/engines/specs/zenodo.yaml +27 -0
  190. package/engines/specs/zhihu_hot_app.yaml +35 -0
  191. package/mcp/clients.yaml +74 -0
  192. package/package.json +51 -12
  193. package/packages/dsh-plugin/README.md +134 -0
  194. package/packages/dsh-plugin/cordis.patch.yml +48 -0
  195. package/packages/dsh-plugin/dsh/client.js +1244 -0
  196. package/packages/dsh-plugin/dsh/index.js +2290 -0
  197. package/packages/dsh-plugin/dsh/native-tools.mjs +743 -0
  198. package/packages/dsh-plugin/package.json +38 -0
  199. package/packages/dsh-plugin/test/client.test.js +232 -0
  200. package/packages/dsh-plugin/test/config.test.js +30 -0
  201. package/packages/dsh-plugin/test/index.test.js +1338 -0
  202. package/references/query-templates.md +96 -0
  203. package/references/query-templates.yaml +118 -0
  204. package/scripts/ab_eval.py +219 -0
  205. package/scripts/ab_eval_p0p1.py +266 -0
  206. package/scripts/adaptive.py +271 -70
  207. package/scripts/answer.py +154 -0
  208. package/scripts/archive_run.py +591 -0
  209. package/scripts/argo_engine_registry.py +172 -0
  210. package/scripts/argo_paths.py +728 -0
  211. package/scripts/article.py +136 -0
  212. package/scripts/arxiv_source.py +201 -0
  213. package/scripts/badcase_ledger.py +252 -0
  214. package/scripts/batch_probe.py +241 -0
  215. package/scripts/benchmark.py +19 -0
  216. package/scripts/bounded_run.py +144 -0
  217. package/scripts/browser_auth.py +323 -0
  218. package/scripts/burstiness_detector.py +147 -0
  219. package/scripts/cache.py +1078 -67
  220. package/scripts/cache_guard.py +271 -0
  221. package/scripts/cache_key_vdom.py +56 -0
  222. package/scripts/cache_similarity.py +170 -0
  223. package/scripts/caliber_isolation.py +195 -0
  224. package/scripts/calibrate_lowquality.py +210 -0
  225. package/scripts/candidate_envelope.py +366 -0
  226. package/scripts/chrome_cdp.py +871 -0
  227. package/scripts/circuit_breaker.py +389 -0
  228. package/scripts/citations.py +334 -0
  229. package/scripts/cite_cli.py +59 -0
  230. package/scripts/cjk_tokens.py +95 -0
  231. package/scripts/clarify.py +47 -24
  232. package/scripts/cli_io.py +240 -0
  233. package/scripts/config.py +683 -45
  234. package/scripts/config_cache.py +402 -0
  235. package/scripts/conn_pool.py +220 -0
  236. package/scripts/content_security.py +685 -0
  237. package/scripts/content_signals.py +730 -0
  238. package/scripts/crawl.py +74 -35
  239. package/scripts/daemon.py +866 -0
  240. package/scripts/digup_bridge.py +214 -0
  241. package/scripts/engine_admission.py +366 -0
  242. package/scripts/engine_dispatch.py +1019 -0
  243. package/scripts/engine_env.py +534 -0
  244. package/scripts/engine_failure.py +411 -0
  245. package/scripts/engine_families.py +732 -0
  246. package/scripts/engine_policy.py +218 -0
  247. package/scripts/engine_requires.py +190 -0
  248. package/scripts/engine_status.py +322 -0
  249. package/scripts/engine_validate.py +638 -0
  250. package/scripts/engines.py +778 -294
  251. package/scripts/engines_base.py +1521 -0
  252. package/scripts/engines_builders.py +298 -0
  253. package/scripts/engines_builders_batch10.py +159 -0
  254. package/scripts/engines_builders_batch11.py +174 -0
  255. package/scripts/engines_builders_batch12.py +103 -0
  256. package/scripts/engines_builders_batch13.py +340 -0
  257. package/scripts/engines_builders_batch9.py +1468 -0
  258. package/scripts/engines_builders_cn.py +2283 -0
  259. package/scripts/engines_builders_data.py +2923 -0
  260. package/scripts/engines_builders_data_macro.py +775 -0
  261. package/scripts/engines_builders_feeds.py +255 -0
  262. package/scripts/engines_builders_intl.py +1239 -0
  263. package/scripts/engines_builders_search.py +440 -0
  264. package/scripts/engines_builders_tech.py +1360 -0
  265. package/scripts/evidence.py +588 -162
  266. package/scripts/evidence_loop.py +592 -0
  267. package/scripts/evidence_tier.py +298 -0
  268. package/scripts/except_sets.py +25 -0
  269. package/scripts/extract.py +55 -16
  270. package/scripts/fact_align.py +207 -0
  271. package/scripts/fact_crosscheck.py +230 -0
  272. package/scripts/fetch.py +25 -12
  273. package/scripts/fetch_browser.py +87 -0
  274. package/scripts/fetch_jsrun.py +122 -0
  275. package/scripts/fetch_quality.py +225 -0
  276. package/scripts/fetch_render_tinyfish.py +117 -0
  277. package/scripts/fetch_v3.py +1779 -0
  278. package/scripts/focus_extract.py +350 -0
  279. package/scripts/fulltext_store.py +169 -0
  280. package/scripts/fxtwitter_api.py +325 -0
  281. package/scripts/gen_engine_catalog.py +364 -0
  282. package/scripts/gen_native_tools.py +91 -0
  283. package/scripts/gen_routing_snapshot.py +184 -0
  284. package/scripts/health_check.py +140 -402
  285. package/scripts/health_probe.py +23 -8
  286. package/scripts/hot_state.py +134 -0
  287. package/scripts/http_client.py +1053 -0
  288. package/scripts/image_ops.py +430 -0
  289. package/scripts/image_vision/vision_probe +0 -0
  290. package/scripts/image_vision/vision_probe.swift +126 -0
  291. package/scripts/install.ps1 +148 -0
  292. package/scripts/install.sh +206 -0
  293. package/scripts/job.py +1186 -0
  294. package/scripts/jump_resolver.py +235 -0
  295. package/scripts/lang_capability.py +150 -0
  296. package/scripts/lang_detect.py +289 -0
  297. package/scripts/lang_matrix_probe.py +329 -0
  298. package/scripts/lang_pref.py +311 -0
  299. package/scripts/link_source.py +340 -0
  300. package/scripts/local_engines.py +321 -0
  301. package/scripts/local_image.py +970 -0
  302. package/scripts/local_seek.py +366 -0
  303. package/scripts/macro_countries.py +82 -0
  304. package/scripts/matrix_search_eval.py +1105 -0
  305. package/scripts/mcp_diag.py +8 -3
  306. package/scripts/mcp_handlers.py +1074 -0
  307. package/scripts/mcp_handlers_surface.py +157 -0
  308. package/scripts/mcp_launch.sh +122 -0
  309. package/scripts/mcp_server.py +118 -417
  310. package/scripts/mcp_setup.py +407 -0
  311. package/scripts/mcp_tools.py +330 -0
  312. package/scripts/mcp_transport.py +191 -0
  313. package/scripts/min_version_scan.py +335 -0
  314. package/scripts/net_proxy.py +258 -0
  315. package/scripts/network_aware.py +152 -0
  316. package/scripts/paper.py +274 -0
  317. package/scripts/pdf_extract.py +518 -0
  318. package/scripts/phrase_filter.py +66 -0
  319. package/scripts/plan.py +422 -0
  320. package/scripts/postprocess_signals.py +150 -0
  321. package/scripts/pre-commit +48 -0
  322. package/scripts/query_enhance.py +157 -0
  323. package/scripts/query_operators.py +196 -0
  324. package/scripts/query_rewriter.py +116 -7
  325. package/scripts/query_signals.py +300 -0
  326. package/scripts/query_templates.py +167 -0
  327. package/scripts/query_understanding.py +411 -0
  328. package/scripts/query_variants.py +298 -0
  329. package/scripts/quota.py +488 -76
  330. package/scripts/rank_signals.py +775 -0
  331. package/scripts/ranking_eval.py +319 -0
  332. package/scripts/readability_extract.py +433 -0
  333. package/scripts/recompute.py +415 -0
  334. package/scripts/recovery.py +616 -0
  335. package/scripts/recovery_l0.py +160 -0
  336. package/scripts/redskill/redskill_engine.py +332 -0
  337. package/scripts/regression_p0p1.py +318 -0
  338. package/scripts/replay_eval.py +640 -0
  339. package/scripts/rerank_tfidf.py +225 -0
  340. package/scripts/research.py +591 -403
  341. package/scripts/research_cli.py +361 -0
  342. package/scripts/research_dossier.py +492 -0
  343. package/scripts/research_expand.py +146 -0
  344. package/scripts/research_gates.py +272 -0
  345. package/scripts/research_report.py +209 -0
  346. package/scripts/research_strategy.py +37 -0
  347. package/scripts/research_work_packages.py +195 -0
  348. package/scripts/result_lang.py +311 -0
  349. package/scripts/robots_guard.py +259 -0
  350. package/scripts/route.py +921 -254
  351. package/scripts/route_bangs.py +40 -0
  352. package/scripts/route_cache.py +225 -0
  353. package/scripts/route_combo.py +910 -0
  354. package/scripts/route_domains.py +328 -0
  355. package/scripts/route_lang.py +605 -0
  356. package/scripts/route_log.py +63 -0
  357. package/scripts/route_policy.py +250 -0
  358. package/scripts/rrf_classic.py +66 -0
  359. package/scripts/screenshot.py +73 -0
  360. package/scripts/search.py +897 -405
  361. package/scripts/search_attribution.py +88 -0
  362. package/scripts/search_benchmark.py +407 -0
  363. package/scripts/search_cli.py +627 -0
  364. package/scripts/search_entry.py +389 -0
  365. package/scripts/search_output.py +606 -0
  366. package/scripts/search_pipeline.py +1077 -0
  367. package/scripts/search_rank.py +991 -0
  368. package/scripts/search_types.py +27 -12
  369. package/scripts/seek_locator.py +68 -0
  370. package/scripts/semantic_evidence.py +215 -0
  371. package/scripts/serp_guard.py +228 -0
  372. package/scripts/serp_kill_log.py +51 -0
  373. package/scripts/single_flight.py +120 -0
  374. package/scripts/social_engines/__init__.py +10 -0
  375. package/scripts/social_engines/bilibili_engine.py +131 -0
  376. package/scripts/social_engines/hackernews_engine.py +113 -0
  377. package/scripts/social_engines/reddit_engine.py +172 -0
  378. package/scripts/social_engines/twitter_engine.py +397 -0
  379. package/scripts/social_engines/v2ex_engine.py +101 -0
  380. package/scripts/social_engines/weibo_engine.py +128 -0
  381. package/scripts/social_engines/xiaohongshu_engine.py +137 -0
  382. package/scripts/social_engines/zhihu_engine.py +139 -0
  383. package/scripts/social_research.py +201 -0
  384. package/scripts/sogou_weixin_browser.py +318 -0
  385. package/scripts/source_ledger.py +249 -0
  386. package/scripts/stage_timing.py +58 -0
  387. package/scripts/stats_cli.py +95 -0
  388. package/scripts/straggler_policy.py +60 -0
  389. package/scripts/stylometry_detector.py +231 -0
  390. package/scripts/sync_backends.py +353 -0
  391. package/scripts/sync_installs.py +29 -0
  392. package/scripts/template_cli.py +127 -0
  393. package/scripts/text_lexicon.py +72 -0
  394. package/scripts/tfidf_router.py +109 -37
  395. package/scripts/time_utils.py +233 -0
  396. package/scripts/topic_research_profiles.py +643 -0
  397. package/scripts/train.py +737 -0
  398. package/scripts/tweet.py +596 -0
  399. package/scripts/url_canon.py +217 -0
  400. package/scripts/url_optimize.py +56 -0
  401. package/scripts/url_safety.py +236 -0
  402. package/scripts/usage_log.py +116 -0
  403. package/scripts/usage_log_clean.py +113 -0
  404. package/scripts/v2ex_nodes.py +311 -0
  405. package/scripts/variant_recall.py +188 -0
  406. package/scripts/watch.py +205 -0
  407. package/scripts/wechat_article.py +258 -0
  408. package/scripts/wide_research.py +402 -0
  409. package/scripts/wx.py +349 -0
  410. package/scripts/x_article_fetch.py +141 -0
  411. package/scripts/x_search.py +234 -0
  412. package/scripts/x_trending.py +189 -0
  413. package/scripts/yaml_load.py +67 -0
  414. package/sub-skills/ego-search/SKILL.md +253 -0
  415. package/sub-skills/ego-search/references/browser-runtime.md +43 -0
  416. package/sub-skills/ego-search/references/install.md +67 -0
  417. package/sub-skills/ego-search/references/original-ego-upgrade.md +51 -0
  418. package/sub-skills/ego-search/scripts/ego_search.py +800 -0
  419. package/sub-skills/ego-search/scripts/merge.py +226 -0
  420. package/sub-skills/ego-search/scripts/quality.py +64 -0
  421. package/sub-skills/ego-search/scripts/runtime.py +172 -0
  422. package/sub-skills/ego-search/scripts/safety.py +109 -0
  423. package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
  424. package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
  425. package/sub-skills/ego-search/tests/test_query_normalize.py +68 -0
  426. package/sub-skills/js-run/SKILL.md +67 -0
  427. package/sub-skills/js-run/scripts/jsrun.py +112 -0
  428. package/sub-skills/js-run/scripts/shim.js +188 -0
  429. package/sub-skills/js-run/tests/fixtures/challenge_like.js +27 -0
  430. package/sub-skills/js-run/tests/test_jsrun.py +165 -0
  431. package/sub-skills/local-search/SKILL.md +50 -53
  432. package/sub-skills/local-search/bm25.py +162 -0
  433. package/sub-skills/local-search/config.yaml +152 -55
  434. package/sub-skills/local-search/engine_registry.py +91 -26
  435. package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
  436. package/sub-skills/local-search/local_search_adapter.py +13 -2
  437. package/sub-skills/local-search/parse_maps.yaml +40 -42
  438. package/sub-skills/local-search/search_v3.py +1182 -65
  439. package/sub-skills/local-search/smart_router.py +47 -7
  440. package/sub-skills/local-seek/SKILL.md +110 -0
  441. package/sub-skills/local-seek/config/domains.yaml +20 -0
  442. package/sub-skills/local-seek/references/auto-behaviors.md +24 -0
  443. package/sub-skills/local-seek/references/fd-mdfind-recipes.md +62 -0
  444. package/sub-skills/local-seek/references/git-integration.md +54 -0
  445. package/sub-skills/local-seek/references/remote-handoff.md +46 -0
  446. package/sub-skills/local-seek/references/rg-recipes.md +96 -0
  447. package/sub-skills/local-seek/references/strategies.md +52 -0
  448. package/sub-skills/local-seek/references/structural-search.md +59 -0
  449. package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
  450. package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
  451. package/sub-skills/local-seek/scripts/seek.py +1465 -0
  452. package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
  453. package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
  454. package/sub-skills/local-seek/scripts/seek_rank.py +193 -0
  455. package/sub-skills/local-seek/tests/test_content_rank.py +222 -0
  456. package/sub-skills/local-seek/tests/test_determinism_20261006.py +145 -0
  457. package/sub-skills/local-seek/tests/test_noise_tier.py +101 -0
  458. package/sub-skills/local-seek/tests/test_pcre2_probe.py +136 -0
  459. package/sub-skills/local-seek/tests/test_rank_pinyin.py +77 -0
  460. package/sub-skills/local-seek/tests/test_regression_20261003.py +169 -0
  461. package/sub-skills/local-seek/tests/test_regression_20261006.py +146 -0
  462. package/sub-skills/local-seek/tests/test_seek_degradation.py +111 -0
  463. package/sub-skills/local-seek/tests/test_seek_fts.py +313 -0
@@ -9,117 +9,262 @@ adaptive.py — Unified Search v2 自适应学习引擎
9
9
  - 预算模式感知(高 cost 引擎在 budget 模式下降权)
10
10
 
11
11
  评分公式:
12
- score = success_rate × latency_factor × cost_factor
12
+ score = success_rate × latency_factor × cost_factor × quality_factor
13
13
  latency_factor = min(1.0, 2000 / avg_latency_ms) # 2s 内满分
14
14
  cost_factor = free=1.0, low=0.85, paid=0.6
15
+ quality_factor = clamp(0.75 + 0.5 × avg_quality, 0.75, 1.25) # 无质量数据时 = 1.0
16
+
17
+ quality(2026-09-27 接通):结果相关度代理 ∈ [0,1],由 search 层用查询词元
18
+ 覆盖率零成本算好后回写。修复「只学快慢、不学好坏」盲区——`engine_perf`
19
+ 的 `quality` 列自建库起就存在、却全仓无人写入也无人读取,1978 行实测数据
20
+ 全为 0.0:反馈信号只有 success/latency/cost,于是「成功返回一堆无关结果」
21
+ 与「成功返回精准结果」在评分上完全等价,而这是多数免密钥源的常态。
15
22
  """
16
23
 
17
24
  from __future__ import annotations
18
25
 
19
- import json
26
+ import contextlib
20
27
  import os
21
28
  import sqlite3
22
29
  import threading
23
30
  import time
24
31
  from pathlib import Path
25
32
  from typing import Optional
33
+ from cli_io import dumps
26
34
 
27
35
  # ── 路径 ──────────────────────────────────────────────────────────────────────
28
36
 
29
- DB_DIR = Path.home() / ".cache" / "unified-search"
37
+ def _state_dir() -> Path:
38
+ """状态目录(惰性派生,支持 ARGO_STATE_DIR 覆盖)。"""
39
+ import argo_paths
40
+ return argo_paths.ensure_state_dir()
41
+
42
+
43
+ DB_DIR = _state_dir() # 兼容旧引用
30
44
  DB_PATH = DB_DIR / "adaptive.db"
31
45
  WINDOW_DAYS = 7
32
46
 
47
+ # 数据过时阈值:窗口内有历史数据但最近一次调用距今超过该值,
48
+ # 视为历史快照失效(引擎可能已恢复健康),返回中性分让其重新进入组合。
49
+ # 被降权引擎不会出现在组合里 → 没有新记录 → 分数永远卡低(死锁),
50
+ # 此机制类似熔断器的 half-open 探测,是自适应学习的标准恢复通道。
51
+ STALE_AFTER_SECONDS = 24 * 3600
52
+
33
53
  # ── 成本分级因子 ─────────────────────────────────────────────────────────────
34
54
 
35
55
  COST_FACTORS = {"free": 1.0, "low": 0.85, "paid": 0.6}
36
56
 
57
+ # 质量因子的取值域:±25%。刻意不放大——质量是四个维度里最噪的一个
58
+ # (词元覆盖率只是相关度代理),让它单独颠覆 success×latency×cost 的
59
+ # 既有排序会把「一次词面巧合」变成引擎生死。先降权,不封杀。
60
+ QUALITY_FACTOR_MIN = 0.75
61
+ QUALITY_FACTOR_MAX = 1.25
62
+
63
+
64
+ def quality_factor(avg_quality: float | None, sum_quality: float | int | None) -> float:
65
+ """质量因子:把窗口内平均相关度映射到 ±25% 的乘数。
66
+
67
+ `sum_quality` 是**判定「有没有数据」的口径**,不是冗余参数:它取
68
+ `SUM(quality)`,而空结果/未回写的行 quality=0(列的 DEFAULT 0.0),
69
+ 于是「平均值的分母」与「有数据的行数」并不一致——只有 sum>0 才能
70
+ 说明确有质量回写。若改用 `avg > 0` 判定,窗口内全是未回写的老数据时
71
+ avg 恰为 0.0,会被误读成「质量极差」而把全仓引擎一次性降权
72
+ (本次升级前正是这个状态:1978 行 quality 全 0.0)。故一律看 sum。
73
+
74
+ - 无任何质量记录(sum=0)→ 1.0,中性,不惩罚存量数据
75
+ - 差结果(avg≈0.1)→ 0.80
76
+ - 好结果(avg≥0.8)→ 1.15
77
+ """
78
+ if not sum_quality:
79
+ return 1.0
80
+ avg = float(avg_quality or 0.0)
81
+ return max(QUALITY_FACTOR_MIN,
82
+ min(QUALITY_FACTOR_MAX, 0.75 + 0.5 * avg))
83
+
37
84
 
38
85
  class AdaptiveLearner:
39
86
  """自适应学习引擎:追踪引擎表现并输出推荐分数。"""
40
87
 
88
+ # get_score 在 route 热路径上可能每请求多次调用;内存缓存 + 复用连接
89
+ SCORE_CACHE_TTL = 30.0
90
+
41
91
  def __init__(self):
42
92
  self._lock = threading.Lock()
93
+ self._local = threading.local()
94
+ self._score_cache: dict[str, tuple[float, float]] = {} # engine -> (score, expires_at)
95
+ self._batch_depth = 0
96
+ self._batch_dirty = False
43
97
  DB_DIR.mkdir(parents=True, exist_ok=True)
44
98
  self._init_db()
45
99
 
100
+ @contextlib.contextmanager
101
+ def batch(self):
102
+ """合并多次 record 为单次 SQLite commit,保持 record 调用面不变。"""
103
+ with self._lock:
104
+ self._batch_depth = getattr(self, "_batch_depth", 0) + 1
105
+ try:
106
+ yield
107
+ finally:
108
+ with self._lock:
109
+ self._batch_depth = max(0, getattr(self, "_batch_depth", 1) - 1)
110
+ if self._batch_depth == 0 and getattr(self, "_batch_dirty", False):
111
+ self._connect().commit()
112
+ self._batch_dirty = False
113
+
46
114
  def _connect(self) -> sqlite3.Connection:
47
- conn = sqlite3.connect(str(DB_PATH), timeout=10)
48
- conn.execute("PRAGMA journal_mode=WAL")
115
+ """线程本地复用连接,避免 route 每次 open/close SQLite。"""
116
+ conn = getattr(self._local, "conn", None)
117
+ if conn is not None:
118
+ return conn
119
+ conn = sqlite3.connect(str(DB_PATH), timeout=10, check_same_thread=False)
120
+ import argo_paths
121
+ argo_paths.apply_state_pragmas(conn)
122
+ self._local.conn = conn
49
123
  return conn
50
124
 
51
125
  def _init_db(self):
52
- with self._connect() as conn:
53
- conn.execute("""
54
- CREATE TABLE IF NOT EXISTS engine_perf (
55
- engine TEXT NOT NULL,
56
- success INTEGER NOT NULL,
57
- latency_ms REAL NOT NULL,
58
- cost REAL NOT NULL DEFAULT 0.0,
59
- created_at REAL NOT NULL
60
- )
61
- """)
62
- # 检查列是否存在(迁移)
63
- cols = [r[1] for r in conn.execute("PRAGMA table_info(engine_perf)")]
64
- if "created_at" not in cols:
65
- conn.execute("ALTER TABLE engine_perf ADD COLUMN created_at REAL DEFAULT 0")
66
- if "cost" not in cols:
67
- conn.execute("ALTER TABLE engine_perf ADD COLUMN cost REAL DEFAULT 0.0")
68
- # 检查索引
69
- conn.execute("CREATE INDEX IF NOT EXISTS idx_perf_engine_time ON engine_perf(engine, created_at)")
70
- # 清理过期数据
71
- cutoff = time.time() - WINDOW_DAYS * 86400
72
- conn.execute("DELETE FROM engine_perf WHERE created_at < ?", (cutoff,))
126
+ conn = self._connect()
127
+ conn.execute("""
128
+ CREATE TABLE IF NOT EXISTS engine_perf (
129
+ engine TEXT NOT NULL,
130
+ success INTEGER NOT NULL,
131
+ latency_ms REAL NOT NULL,
132
+ cost REAL NOT NULL DEFAULT 0.0,
133
+ created_at REAL NOT NULL,
134
+ empty INTEGER NOT NULL DEFAULT 0
135
+ )
136
+ """)
137
+ cols = [r[1] for r in conn.execute("PRAGMA table_info(engine_perf)")]
138
+ if "created_at" not in cols:
139
+ conn.execute("ALTER TABLE engine_perf ADD COLUMN created_at REAL DEFAULT 0")
140
+ if "cost" not in cols:
141
+ conn.execute("ALTER TABLE engine_perf ADD COLUMN cost REAL DEFAULT 0.0")
142
+ if "empty" not in cols:
143
+ # 老库补列:历史行 empty=0,等价于旧口径(空结果也算失败),
144
+ # 不做回溯改写——我们无法知道那些行当时到底是「空」还是「错」。
145
+ conn.execute("ALTER TABLE engine_perf ADD COLUMN empty INTEGER DEFAULT 0")
146
+ if "quality" not in cols:
147
+ # 同理补列。历史行 quality=0 表示「当时没回写质量」,不是「质量为零」——
148
+ # 评分侧以 SUM(quality) > 0 判定有无数据,故这批行不会拉低任何引擎。
149
+ conn.execute("ALTER TABLE engine_perf ADD COLUMN quality REAL DEFAULT 0.0")
150
+ conn.execute(
151
+ "CREATE INDEX IF NOT EXISTS idx_perf_engine_time ON engine_perf(engine, created_at)"
152
+ )
153
+ cutoff = time.time() - WINDOW_DAYS * 86400
154
+ conn.execute("DELETE FROM engine_perf WHERE created_at < ?", (cutoff,))
155
+ conn.commit()
156
+
157
+ def record(self, engine: str, success: bool, latency_ms: float, cost: float = 0.0,
158
+ empty: bool = False, quality: float | None = None):
159
+ """记录一次引擎调用结果。
73
160
 
74
- def record(self, engine: str, success: bool, latency_ms: float, cost: float = 0.0):
75
- """记录一次引擎调用结果。"""
161
+ ``empty=True`` 表示「引擎正常,但这次查询它没有结果」——与「失败」是
162
+ 两回事,见 `get_score` 的说明。默认 False 保持旧调用方行为不变。
163
+
164
+ ``quality`` 是本次结果的相关度代理 ∈ [0,1](调用方用词元覆盖率算好
165
+ 回写,见 search_pipeline 的回填点)。None = 拿不到质量信号,此时
166
+ 落 0.0 而**不是**当作「质量为零」参与评分——`quality_factor` 以
167
+ `SUM(quality) > 0` 判定有无数据,故未回写的行不会拖累任何引擎。
168
+ """
169
+ q = 0.0 if quality is None else max(0.0, min(1.0, float(quality)))
76
170
  with self._lock:
77
- with self._connect() as conn:
78
- conn.execute(
79
- "INSERT INTO engine_perf (engine, success, latency_ms, cost, created_at) VALUES (?, ?, ?, ?, ?)",
80
- (engine, 1 if success else 0, latency_ms, cost, time.time()),
81
- )
171
+ conn = self._connect()
172
+ conn.execute(
173
+ "INSERT INTO engine_perf "
174
+ "(engine, success, latency_ms, cost, created_at, empty, quality) "
175
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
176
+ (engine, 1 if success else 0, latency_ms, cost, time.time(),
177
+ 1 if empty else 0, q),
178
+ )
179
+ if getattr(self, "_batch_depth", 0) > 0:
180
+ self._batch_dirty = True
181
+ else:
82
182
  conn.commit()
183
+ self._score_cache.pop(engine, None)
83
184
 
84
185
  def get_score(self, engine: str) -> float:
85
- """获取引擎的综合推荐分数(0.0 ~ 1.0)。"""
186
+ """获取引擎的综合推荐分数(0.0 ~ 1.0)。
187
+
188
+ **只按「有明确结果」的调用算成功率**(`empty=0`)。
189
+
190
+ 为什么要把空结果排除:分数被用来决定「这个源该不该继续用」,
191
+ 而「这次查询它没东西」说明的是**查询与源不匹配**,不是源坏了。
192
+ 两者混在一起会误杀:实测 github 在窗口内 63 次调用、成功率 6%,
193
+ 失败归因全是 empty——它被用来回答各种查询,只有少数落进它的覆盖范围;
194
+ 而它却在 `local_code` / `package_search` 这些**声明要用它**的域里
195
+ 因分数低于 0.3 被整个剔除。同类还有 wikipedia(0.17)、
196
+ openalex(0.29)、hackernews(0.10)、twitter(0.17)、open_library(0.17)。
197
+
198
+ 延迟与成本仍按全部调用统计——空结果也是真实开销。
199
+ """
200
+ now = time.time()
201
+ cached = self._score_cache.get(engine)
202
+ if cached and cached[1] > now:
203
+ return cached[0]
204
+
86
205
  with self._lock:
87
- cutoff = time.time() - WINDOW_DAYS * 86400
88
- with self._connect() as conn:
89
- row = conn.execute(
90
- "SELECT COUNT(*), AVG(success), AVG(latency_ms), AVG(cost) FROM engine_perf WHERE engine = ? AND created_at > ?",
91
- (engine, cutoff),
92
- ).fetchone()
93
- total, avg_success, avg_latency, avg_cost = row
94
- if not total or total == 0:
95
- return 0.5 # 无数据时中性分
96
-
97
- success_rate = avg_success or 0.5
98
- latency_factor = min(1.0, 2000.0 / max(avg_latency or 2000, 1))
99
- # 成本因子:平均成本越低越好
100
- cost_factor = max(0.3, 1.0 - (avg_cost or 0.0) * 10)
101
-
102
- return round(success_rate * latency_factor * cost_factor, 4)
206
+ cached = self._score_cache.get(engine)
207
+ if cached and cached[1] > now:
208
+ return cached[0]
209
+ cutoff = now - WINDOW_DAYS * 86400
210
+ conn = self._connect()
211
+ row = conn.execute(
212
+ "SELECT COUNT(*), "
213
+ " SUM(CASE WHEN empty = 0 THEN 1 ELSE 0 END), "
214
+ " SUM(CASE WHEN empty = 0 THEN success ELSE 0 END), "
215
+ " AVG(latency_ms), AVG(cost), "
216
+ " AVG(quality), SUM(quality), MAX(created_at) "
217
+ "FROM engine_perf WHERE engine = ? AND created_at > ?",
218
+ (engine, cutoff),
219
+ ).fetchone()
220
+ total, judged, judged_ok, avg_latency, avg_cost, avg_q, sum_q, last_ts = row
221
+ total = int(total or 0)
222
+ judged = int(judged or 0)
223
+ if not total:
224
+ score = 0.5 # 无数据时中性分
225
+ else:
226
+ # 数据过时恢复:最后一次调用距今过久 → 中性分,给恢复探测机会
227
+ last_ts = float(last_ts or 0)
228
+ if last_ts and (now - last_ts) > STALE_AFTER_SECONDS:
229
+ score = 0.5
230
+ elif judged == 0:
231
+ # 窗口内全是空结果:没有任何可判定健康与否的调用 → 中性分。
232
+ # 给中性分而不是 0:否则「每次都只是没搜到」会把源打死。
233
+ score = 0.5
234
+ else:
235
+ success_rate = (judged_ok or 0) / judged
236
+ latency_factor = min(1.0, 2000.0 / max(avg_latency or 2000, 1))
237
+ cost_factor = max(0.3, 1.0 - (avg_cost or 0.0) * 10)
238
+ q_factor = quality_factor(avg_q, sum_q)
239
+ score = round(success_rate * latency_factor * cost_factor
240
+ * q_factor, 4)
241
+
242
+ self._score_cache[engine] = (score, now + self.SCORE_CACHE_TTL)
243
+ return score
103
244
 
104
245
  def get_ranking(self) -> list[tuple[str, float]]:
105
- """获取所有引擎的推荐排名(降序)。"""
246
+ """获取所有引擎的推荐排名(降序)。空结果的排除口径同 `get_score`。"""
106
247
  with self._lock:
107
248
  cutoff = time.time() - WINDOW_DAYS * 86400
108
- with self._connect() as conn:
109
- rows = conn.execute(
110
- "SELECT engine, COUNT(*), AVG(success), AVG(latency_ms), AVG(cost) "
111
- "FROM engine_perf WHERE created_at > ? GROUP BY engine",
112
- (cutoff,),
113
- ).fetchall()
249
+ conn = self._connect()
250
+ rows = conn.execute(
251
+ "SELECT engine, "
252
+ " SUM(CASE WHEN empty = 0 THEN 1 ELSE 0 END), "
253
+ " SUM(CASE WHEN empty = 0 THEN success ELSE 0 END), "
254
+ " AVG(latency_ms), AVG(cost), AVG(quality), SUM(quality) "
255
+ "FROM engine_perf WHERE created_at > ? GROUP BY engine",
256
+ (cutoff,),
257
+ ).fetchall()
114
258
 
115
259
  results = []
116
- for engine, total, avg_success, avg_latency, avg_cost in rows:
117
- if not total:
260
+ for engine, judged, judged_ok, avg_latency, avg_cost, avg_q, sum_q in rows:
261
+ if not judged:
118
262
  continue
119
- success_rate = avg_success or 0.5
263
+ success_rate = (judged_ok or 0) / judged
120
264
  latency_factor = min(1.0, 2000.0 / max(avg_latency or 2000, 1))
121
265
  cost_factor = max(0.3, 1.0 - (avg_cost or 0.0) * 10)
122
- score = round(success_rate * latency_factor * cost_factor, 4)
266
+ score = round(success_rate * latency_factor * cost_factor
267
+ * quality_factor(avg_q, sum_q), 4)
123
268
  results.append((engine, score))
124
269
 
125
270
  results.sort(key=lambda x: -x[1])
@@ -127,22 +272,28 @@ class AdaptiveLearner:
127
272
 
128
273
  def get_stats(self) -> dict:
129
274
  """获取所有引擎的统计信息。"""
130
- ranking = self.get_ranking()
131
275
  cutoff = time.time() - WINDOW_DAYS * 86400
132
- with self._connect() as conn:
133
- rows = conn.execute(
134
- "SELECT engine, COUNT(*), AVG(success), AVG(latency_ms), AVG(cost) "
135
- "FROM engine_perf WHERE created_at > ? GROUP BY engine",
136
- (cutoff,),
137
- ).fetchall()
276
+ conn = self._connect()
277
+ rows = conn.execute(
278
+ "SELECT engine, COUNT(*), AVG(success), AVG(latency_ms), AVG(cost), "
279
+ " AVG(quality), SUM(quality) "
280
+ "FROM engine_perf WHERE created_at > ? GROUP BY engine",
281
+ (cutoff,),
282
+ ).fetchall()
138
283
 
139
284
  stats = {}
140
- for engine, total, avg_success, avg_latency, avg_cost in rows:
285
+ for engine, total, avg_success, avg_latency, avg_cost, avg_q, sum_q in rows:
286
+ # 质量相关度:avg 是窗口内均值;`quality_tracked` 标明这批数据
287
+ # 里是否真的有质量回写——没有时 avg_q 恒为 0,看数会误读成
288
+ # 「这个源质量极差」,而实际是「一直没测过」。
141
289
  stats[engine] = {
142
290
  "calls": total,
143
291
  "success_rate": round(avg_success or 0, 3),
144
292
  "avg_latency_ms": round(avg_latency or 0, 1),
145
293
  "avg_cost": round(avg_cost or 0, 6),
294
+ "avg_quality": round(avg_q or 0, 3),
295
+ "quality_tracked": bool(sum_q),
296
+ "quality_factor": round(quality_factor(avg_q, sum_q), 3),
146
297
  "score": self.get_score(engine),
147
298
  }
148
299
  return stats
@@ -151,6 +302,56 @@ class AdaptiveLearner:
151
302
  """判断引擎是否值得使用(分数高于阈值)。"""
152
303
  return self.get_score(engine) >= threshold
153
304
 
305
+ def latency_percentile(self, engine: str, p: float = 0.75,
306
+ min_samples: int = 5) -> Optional[float]:
307
+ """窗口内「正常交付」延迟的分位数(毫秒);样本不足/过时/无数据 → None。
308
+
309
+ 口径比 `get_score` 的 judged 更严(judged 仅 empty=0,这里是
310
+ 其子集):只统计 **success=1 且 empty=0** 的
311
+ 行——「这个源在它能回答的查询上多快」。空结果的低延迟会把分位数拉低,
312
+ 但空结果并不解决对冲竞速(sufficiency 判据不放行),把它算进来会让
313
+ 宽限窗对「只会秒回空结果」的源收得过紧,反而多发对冲调用。
314
+
315
+ 用途(2026-10-11,调度微手术):给对冲宽限窗一个「快引擎收窄、慢引擎
316
+ 不动」的自适应窗,见 engine_dispatch._adaptive_grace。只做读侧,不缓存
317
+ ——每次编排至多一次查询(窗口内样本量小,全量取回后本地排序,免去依赖
318
+ SQLite 版本的 percentile 扩展)。
319
+
320
+ 调用次数说明(2026-10-16 修订):此处原文写「每次编排至多一次」,
321
+ 随 straggler_policy.straggler_tail 接入后不再成立——分层尾宽会在
322
+ 每次交付时刻对「还没回来的引擎」逐个取值(2 引擎编排约 1 次,多引
323
+ 擎编排最坏 ≈ 各次交付的 waiting 数之和)。默认关(ARGO_ADAPTIVE_TAIL)
324
+ 时不发生;开启后每引擎每次约 0.07ms(实测 200 次调用 warm),代价
325
+ 相对引擎 RTT 可忽略,但判「多不多」时按这个口径,别按旧句子。
326
+ """
327
+ if not engine:
328
+ return None
329
+ try:
330
+ q = float(p)
331
+ except (TypeError, ValueError):
332
+ return None
333
+ if not 0.0 < q < 1.0:
334
+ return None
335
+ now = time.time()
336
+ cutoff = now - WINDOW_DAYS * 86400
337
+ with self._lock:
338
+ conn = self._connect()
339
+ rows = conn.execute(
340
+ "SELECT latency_ms, created_at FROM engine_perf "
341
+ "WHERE engine = ? AND created_at > ? "
342
+ " AND success = 1 AND empty = 0",
343
+ (engine, cutoff),
344
+ ).fetchall()
345
+ if not rows or len(rows) < max(1, int(min_samples)):
346
+ return None
347
+ # 数据过时恢复:与 get_score 同判据——窗口内有行但最近一次调用距今
348
+ # 过久,说明这张快照已不能代表引擎现状(可能已恢复/已换网络环境)。
349
+ if (now - max(float(ts) for _, ts in rows)) > STALE_AFTER_SECONDS:
350
+ return None
351
+ lats = sorted(float(lat) for lat, _ in rows)
352
+ idx = min(len(lats) - 1, max(0, int(round(q * (len(lats) - 1)))))
353
+ return lats[idx]
354
+
154
355
 
155
356
  # ── 模块级单例 ─────────────────────────────────────────────────────────────────
156
357
 
@@ -170,7 +371,7 @@ if __name__ == "__main__":
170
371
  import sys
171
372
  learner = get_learner()
172
373
  if len(sys.argv) > 1 and sys.argv[1] == "stats":
173
- print(json.dumps(learner.get_stats(), ensure_ascii=False, indent=2))
374
+ print(dumps(learner.get_stats()))
174
375
  elif len(sys.argv) > 1 and sys.argv[1] == "rank":
175
376
  ranking = learner.get_ranking()
176
377
  for engine, score in ranking:
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env python3
2
+ """answer.py — 直答端点(Answer 形态):带引用的合成答案。
3
+
4
+ 吸纳自 2026-09 对标研究:Brave Answers / Perplexity Sonar / Seltz Answer
5
+ 三家同形态,行业已收紧为「搜索的 Answer 端点」标配。当前通道:
6
+ Seltz POST /v1/answer(x-api-key 鉴权,注册赠 20000 次)。
7
+
8
+ **scope 决定语料,不传默认 news**:上游只支持 news / wikipedia / people /
9
+ companies 四个语料。2026-09-16 实测:不传 scope 时技术查询(「RRF 融合算法」)
10
+ 拿到的是 news 语料里的时政与播客条目,看上去像「引用跑偏」,实为语料选错
11
+ 而非上游坏了。实测四个语料——companies 查公司概况最好(Apple / 台积电 答得
12
+ 准且完整);news 在真新闻查询上可用但覆盖窄;wikipedia 干净;people 不可用
13
+ (10 条全 linkedin.com,人名查不到),故不对外推荐。
14
+
15
+ 置信计算方式:上游响应无置信度标量(2026-09-14 实测),故输出诚实计数——
16
+ 引用条数 + 唯一引用域名数;上游若日后提供 confidence 字段则原样透传。
17
+ 不造模拟精确度的单一分数。
18
+
19
+ 用法:
20
+ argo answer "query" # --json 由 bin/argo 默认注入
21
+ argo answer "query" --scope companies # 查公司概况
22
+ argo answer "query" --model seltz-pro # 模型自己发起检索,更慢更贵
23
+ python3 answer.py "query" --json
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import json
30
+ import os
31
+ import sys
32
+ import time
33
+ from urllib.parse import urlparse
34
+ from cli_io import dumps
35
+ from engine_env import get_env
36
+
37
+ _SELTZ_ANSWER_URL = "https://api.seltz.ai/v1/answer"
38
+ _SELTZ_SCOPES = ("news", "wikipedia", "people", "companies")
39
+ _SELTZ_MODELS = ("seltz-base", "seltz-pro")
40
+
41
+
42
+ def _unique_domains(citations: list) -> int:
43
+ hosts = set()
44
+ for c in citations or []:
45
+ if isinstance(c, dict):
46
+ u = str(c.get("url") or "")
47
+ else:
48
+ u = str(c or "")
49
+ if u:
50
+ hosts.add(urlparse(u).hostname or "")
51
+ return len(hosts - {""})
52
+
53
+
54
+ def seltz_answer(query: str, timeout: float = 40.0, *,
55
+ scope: str | None = None, model: str | None = None) -> tuple[dict | None, str]:
56
+ """调 Seltz Answer;返回 (响应 dict, 错误串)。响应含 answer/citations。
57
+
58
+ scope / model 不传就由上游取默认(news / seltz-base)。scope 传错值时上游
59
+ 返回 404,故此处先按白名单挡掉,错误信息比 HTTP 404 好读。
60
+ """
61
+ scope = (scope or "").strip().lower()
62
+ model = (model or "").strip().lower()
63
+ if scope and scope not in _SELTZ_SCOPES:
64
+ return None, f"未知 scope {scope!r}(可选:{'/'.join(_SELTZ_SCOPES)})"
65
+ if model and model not in _SELTZ_MODELS:
66
+ return None, f"未知 model {model!r}(可选:{'/'.join(_SELTZ_MODELS)})"
67
+ key = get_env(["ARGO_SELTZ_API_KEY", "SELTZ_API_KEY"])
68
+ if not key:
69
+ return None, "SELTZ_API_KEY 未设置(写入 ~/.config/argo/env 后重试)"
70
+ body: dict = {"query": query}
71
+ if scope:
72
+ body["scope"] = scope
73
+ if model:
74
+ body["model"] = model
75
+ try:
76
+ from http_client import HttpClient
77
+ # max_retries=0:合成答案一次计费一次调用,引擎内重试会双倍烧赠额
78
+ client = HttpClient(timeout=timeout, max_retries=0, jitter=False)
79
+ resp = client.post(_SELTZ_ANSWER_URL, body=body, extra_headers={
80
+ "x-api-key": key, "Content-Type": "application/json",
81
+ })
82
+ except Exception as e:
83
+ return None, f"seltz answer 请求失败: {e}"
84
+ if resp.get("status", 0) != 200 or not resp.get("text"):
85
+ return None, f"seltz answer HTTP {resp.get('status')}"
86
+ try:
87
+ data = json.loads(resp["text"])
88
+ except (ValueError, TypeError):
89
+ return None, "seltz answer 响应非 JSON"
90
+ if not isinstance(data, dict) or "answer" not in data:
91
+ return None, f"seltz answer 响应缺 answer 字段: {str(data)[:200]}"
92
+ return data, ""
93
+
94
+
95
+ def main() -> None:
96
+ ap = argparse.ArgumentParser(description="直答:带引用的合成答案(Seltz)")
97
+ ap.add_argument("query", help="要回答的问题")
98
+ ap.add_argument("--json", action="store_true", help="JSON 输出(bin/argo 默认注入)")
99
+ ap.add_argument("--scope", choices=list(_SELTZ_SCOPES), default=None,
100
+ help="语料:companies 查公司概况最好;news 覆盖窄;"
101
+ "people 实测不可用。不传由上游取默认 news")
102
+ ap.add_argument("--model", choices=list(_SELTZ_MODELS), default=None,
103
+ help="seltz-base(默认,一次检索)或 seltz-pro(模型自己发起检索)")
104
+ ap.add_argument("--timeout", type=float, default=40.0)
105
+ args = ap.parse_args()
106
+
107
+ t0 = time.monotonic()
108
+ data, err = seltz_answer(args.query, timeout=args.timeout,
109
+ scope=args.scope, model=args.model)
110
+ elapsed_ms = int((time.monotonic() - t0) * 1000)
111
+
112
+ if err:
113
+ envelope = {"status": "error", "query": args.query, "error": err,
114
+ "elapsed_ms": elapsed_ms}
115
+ print(dumps(envelope))
116
+ sys.exit(1)
117
+
118
+ citations = data.get("citations") or []
119
+ # 上游无置信度标量:诚实计数计算方式;有则透传(前向兼容)
120
+ confidence = {"citations": len(citations),
121
+ "unique_domains": _unique_domains(citations)}
122
+ if isinstance(data.get("confidence"), (int, float)):
123
+ confidence["upstream"] = data["confidence"]
124
+
125
+ envelope = {
126
+ "status": "completed",
127
+ "query": args.query,
128
+ # scope 如实回填实际生效值(未传即上游默认 news),便于排查语料选错
129
+ "scope": args.scope or "news",
130
+ "model": args.model or "seltz-base",
131
+ "answer": str(data.get("answer") or ""),
132
+ "citations": citations,
133
+ "confidence": confidence,
134
+ "engine": "seltz",
135
+ "elapsed_ms": elapsed_ms,
136
+ }
137
+ if args.json:
138
+ print(dumps(envelope))
139
+ return
140
+ # 人类可读:答案正文 + 引用清单
141
+ print(envelope["answer"])
142
+ if citations:
143
+ print("\n引用:")
144
+ for i, c in enumerate(citations, 1):
145
+ if isinstance(c, dict):
146
+ print(f" [{i}] {str(c.get('url') or '')}")
147
+ else:
148
+ print(f" [{i}] {c}")
149
+ print(f"\n语料: {envelope['scope']} · 模型: {envelope['model']}")
150
+ print(f"置信口径: {dumps(envelope['confidence'])}")
151
+
152
+
153
+ if __name__ == "__main__":
154
+ main()