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.
- package/LICENSE +201 -21
- package/README.en.md +822 -0
- package/README.es.md +864 -0
- package/README.ja.md +864 -0
- package/README.ko.md +864 -0
- package/README.md +867 -320
- package/SKILL.md +135 -0
- package/assets/readme/hero.svg +121 -0
- package/assets/readme/made-with-beautify.svg +10 -0
- package/assets/readme/proof-routes.svg +73 -0
- package/assets/readme/settings-keys.png +0 -0
- package/assets/readme/settings-tools.png +0 -0
- package/assets/readme/why-better.svg +104 -0
- package/assets/readme/workflow.svg +83 -0
- package/backends/domain_profiles.json +2308 -96
- package/backends/engine_registry.yaml +3314 -503
- package/backends/query_synonyms_cn.json +35 -0
- package/backends/quota_profiles.json +2409 -161
- package/backends/source_types_cn.json +83 -0
- package/backends/text_lexicon.json +44 -0
- package/bin/argo +703 -0
- package/bin/argo.js +71 -39
- package/config.yaml +4976 -363
- package/cordis.patch.yml +21 -0
- package/data/countries.json +40 -0
- package/data/lang_matrix/lang_capability.json +842 -0
- package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
- package/data/regions_cn.json +1 -0
- package/docs/ADDING_NEW_ENGINE.md +184 -0
- package/docs/ARGO_INTRO.md +124 -0
- package/docs/DATA_SOURCE_GAPS.md +81 -0
- package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
- package/docs/ENGINE_CATALOG.md +564 -0
- package/docs/MCP_SETUP.md +106 -0
- package/docs/PLATFORM_PATHS.md +111 -0
- package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
- package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
- package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
- package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
- package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
- package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
- package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
- package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
- package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
- package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
- package/docs/SEARCH_ARCHIVE.md +115 -0
- package/docs/assets/architecture.svg +104 -0
- package/docs/assets/hero.svg +98 -0
- package/docs/engines/bangumi.md +27 -0
- package/docs/engines/biorxiv.md +27 -0
- package/docs/engines/clawhub.md +27 -0
- package/docs/engines/crt_sh.md +27 -0
- package/docs/engines/datacite.md +27 -0
- package/docs/engines/deps_dev.md +27 -0
- package/docs/engines/douban_movie.md +27 -0
- package/docs/engines/endoflife.md +27 -0
- package/docs/engines/flk_law.md +27 -0
- package/docs/engines/fxtwitter.md +27 -0
- package/docs/engines/google_news.md +27 -0
- package/docs/engines/gov_regulations.md +27 -0
- package/docs/engines/guqian.md +27 -0
- package/docs/engines/hackernews.md +27 -0
- package/docs/engines/iplant.md +27 -0
- package/docs/engines/keenable.md +27 -0
- package/docs/engines/met_museum.md +27 -0
- package/docs/engines/nasa_images.md +27 -0
- package/docs/engines/netease_music.md +27 -0
- package/docs/engines/npm.md +27 -0
- package/docs/engines/nvd.md +27 -0
- package/docs/engines/openreview.md +27 -0
- package/docs/engines/openstd.md +27 -0
- package/docs/engines/parallel_free.md +27 -0
- package/docs/engines/people_daily.md +27 -0
- package/docs/engines/realtime_index.md +27 -0
- package/docs/engines/sec_edgar.md +27 -0
- package/docs/engines/seltz.md +27 -0
- package/docs/engines/skillsmp.md +27 -0
- package/docs/engines/sspai.md +27 -0
- package/docs/engines/std_samr.md +27 -0
- package/docs/engines/train.md +27 -0
- package/docs/engines/un_comtrade.md +27 -0
- package/docs/engines/wikisource.md +27 -0
- package/docs/engines/zdic.md +27 -0
- package/docs/engines/zenodo.md +27 -0
- package/docs/engines/zhihu_hot_app.md +27 -0
- package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
- package/engines/README.md +10 -0
- package/engines/_template_cli.yaml +39 -0
- package/engines/_template_http.yaml +37 -0
- package/engines/plugins/_template_plugin.py +50 -0
- package/engines/specs/_cisa_kev.yaml +54 -0
- package/engines/specs/_osv.yaml +63 -0
- package/engines/specs/aljazeera.yaml +43 -0
- package/engines/specs/antara.yaml +43 -0
- package/engines/specs/artic.yaml +20 -0
- package/engines/specs/aviation_weather.yaml +39 -0
- package/engines/specs/bangumi.yaml +21 -0
- package/engines/specs/biorxiv.yaml +24 -0
- package/engines/specs/carbon_intensity.yaml +32 -0
- package/engines/specs/cinii.yaml +44 -0
- package/engines/specs/clawhub.yaml +49 -0
- package/engines/specs/cleveland.yaml +19 -0
- package/engines/specs/cn_ai_news.yaml +43 -0
- package/engines/specs/core.yaml +35 -0
- package/engines/specs/crt_sh.yaml +38 -0
- package/engines/specs/ct24.yaml +43 -0
- package/engines/specs/datacite.yaml +27 -0
- package/engines/specs/datagov_uk.yaml +41 -0
- package/engines/specs/ddgs_images.yaml +45 -0
- package/engines/specs/deepseek_search.yaml +74 -0
- package/engines/specs/deezer.yaml +20 -0
- package/engines/specs/deps_dev.yaml +21 -0
- package/engines/specs/digup.yaml +36 -0
- package/engines/specs/doi.yaml +39 -0
- package/engines/specs/douban_movie.yaml +22 -0
- package/engines/specs/dw_news.yaml +47 -0
- package/engines/specs/egov_law.yaml +21 -0
- package/engines/specs/elpais.yaml +43 -0
- package/engines/specs/endoflife.yaml +21 -0
- package/engines/specs/energy_charts.yaml +21 -0
- package/engines/specs/factcheck_org.yaml +52 -0
- package/engines/specs/faz.yaml +43 -0
- package/engines/specs/federal_register.yaml +57 -0
- package/engines/specs/firecrawl.yaml +53 -0
- package/engines/specs/flk_law.yaml +24 -0
- package/engines/specs/folha.yaml +43 -0
- package/engines/specs/france24.yaml +44 -0
- package/engines/specs/full_fact.yaml +47 -0
- package/engines/specs/fxtwitter.yaml +31 -0
- package/engines/specs/gbfs_nyc.yaml +32 -0
- package/engines/specs/gdacs.yaml +21 -0
- package/engines/specs/gdelt.yaml +64 -0
- package/engines/specs/google_news.yaml +22 -0
- package/engines/specs/gov_regulations.yaml +38 -0
- package/engines/specs/guardian_rss.yaml +59 -0
- package/engines/specs/hurriyet.yaml +43 -0
- package/engines/specs/iplant.yaml +25 -0
- package/engines/specs/jikan.yaml +24 -0
- package/engines/specs/jolpica.yaml +20 -0
- package/engines/specs/k10plus.yaml +20 -0
- package/engines/specs/keenable.yaml +44 -0
- package/engines/specs/lefigaro.yaml +43 -0
- package/engines/specs/listenbrainz.yaml +19 -0
- package/engines/specs/matichon.yaml +43 -0
- package/engines/specs/met_museum.yaml +21 -0
- package/engines/specs/nasa_images.yaml +45 -0
- package/engines/specs/netease_music.yaml +38 -0
- package/engines/specs/nhk.yaml +43 -0
- package/engines/specs/nhtsa_vpic.yaml +20 -0
- package/engines/specs/noaa_swpc.yaml +20 -0
- package/engines/specs/nvd.yaml +38 -0
- package/engines/specs/obis.yaml +20 -0
- package/engines/specs/opencitations.yaml +52 -0
- package/engines/specs/openf1.yaml +21 -0
- package/engines/specs/openfoodfacts.yaml +45 -0
- package/engines/specs/openligadb.yaml +20 -0
- package/engines/specs/openreview.yaml +37 -0
- package/engines/specs/openstd.yaml +21 -0
- package/engines/specs/people_daily.yaml +22 -0
- package/engines/specs/realtime_index.yaml +34 -0
- package/engines/specs/ror.yaml +20 -0
- package/engines/specs/satnogs.yaml +20 -0
- package/engines/specs/sec_edgar.yaml +46 -0
- package/engines/specs/skillsmp.yaml +50 -0
- package/engines/specs/sogou_weixin_browser.yaml +30 -0
- package/engines/specs/soilgrids.yaml +25 -0
- package/engines/specs/sspai.yaml +36 -0
- package/engines/specs/stackexchange.yaml +46 -0
- package/engines/specs/std_samr.yaml +21 -0
- package/engines/specs/tass.yaml +43 -0
- package/engines/specs/tle_mirror.yaml +21 -0
- package/engines/specs/train.yaml +37 -0
- package/engines/specs/tvmaze.yaml +20 -0
- package/engines/specs/twitter_syndication.yaml +31 -0
- package/engines/specs/un_comtrade.yaml +23 -0
- package/engines/specs/unpaywall.yaml +54 -0
- package/engines/specs/vnpress.yaml +43 -0
- package/engines/specs/weather.yaml +32 -0
- package/engines/specs/who_don.yaml +21 -0
- package/engines/specs/who_gho.yaml +20 -0
- package/engines/specs/wikimedia_commons.yaml +34 -0
- package/engines/specs/wikisource.yaml +22 -0
- package/engines/specs/worms.yaml +19 -0
- package/engines/specs/x_trending.yaml +34 -0
- package/engines/specs/yna.yaml +43 -0
- package/engines/specs/ynet.yaml +43 -0
- package/engines/specs/zbmath.yaml +40 -0
- package/engines/specs/zdic.yaml +22 -0
- package/engines/specs/zenodo.yaml +27 -0
- package/engines/specs/zhihu_hot_app.yaml +35 -0
- package/mcp/clients.yaml +74 -0
- package/package.json +51 -12
- package/packages/dsh-plugin/README.md +134 -0
- package/packages/dsh-plugin/cordis.patch.yml +48 -0
- package/packages/dsh-plugin/dsh/client.js +1244 -0
- package/packages/dsh-plugin/dsh/index.js +2290 -0
- package/packages/dsh-plugin/dsh/native-tools.mjs +743 -0
- package/packages/dsh-plugin/package.json +38 -0
- package/packages/dsh-plugin/test/client.test.js +232 -0
- package/packages/dsh-plugin/test/config.test.js +30 -0
- package/packages/dsh-plugin/test/index.test.js +1338 -0
- package/references/query-templates.md +96 -0
- package/references/query-templates.yaml +118 -0
- package/scripts/ab_eval.py +219 -0
- package/scripts/ab_eval_p0p1.py +266 -0
- package/scripts/adaptive.py +271 -70
- package/scripts/answer.py +154 -0
- package/scripts/archive_run.py +591 -0
- package/scripts/argo_engine_registry.py +172 -0
- package/scripts/argo_paths.py +728 -0
- package/scripts/article.py +136 -0
- package/scripts/arxiv_source.py +201 -0
- package/scripts/badcase_ledger.py +252 -0
- package/scripts/batch_probe.py +241 -0
- package/scripts/benchmark.py +19 -0
- package/scripts/bounded_run.py +144 -0
- package/scripts/browser_auth.py +323 -0
- package/scripts/burstiness_detector.py +147 -0
- package/scripts/cache.py +1078 -67
- package/scripts/cache_guard.py +271 -0
- package/scripts/cache_key_vdom.py +56 -0
- package/scripts/cache_similarity.py +170 -0
- package/scripts/caliber_isolation.py +195 -0
- package/scripts/calibrate_lowquality.py +210 -0
- package/scripts/candidate_envelope.py +366 -0
- package/scripts/chrome_cdp.py +871 -0
- package/scripts/circuit_breaker.py +389 -0
- package/scripts/citations.py +334 -0
- package/scripts/cite_cli.py +59 -0
- package/scripts/cjk_tokens.py +95 -0
- package/scripts/clarify.py +47 -24
- package/scripts/cli_io.py +240 -0
- package/scripts/config.py +683 -45
- package/scripts/config_cache.py +402 -0
- package/scripts/conn_pool.py +220 -0
- package/scripts/content_security.py +685 -0
- package/scripts/content_signals.py +730 -0
- package/scripts/crawl.py +74 -35
- package/scripts/daemon.py +866 -0
- package/scripts/digup_bridge.py +214 -0
- package/scripts/engine_admission.py +366 -0
- package/scripts/engine_dispatch.py +1019 -0
- package/scripts/engine_env.py +534 -0
- package/scripts/engine_failure.py +411 -0
- package/scripts/engine_families.py +732 -0
- package/scripts/engine_policy.py +218 -0
- package/scripts/engine_requires.py +190 -0
- package/scripts/engine_status.py +322 -0
- package/scripts/engine_validate.py +638 -0
- package/scripts/engines.py +778 -294
- package/scripts/engines_base.py +1521 -0
- package/scripts/engines_builders.py +298 -0
- package/scripts/engines_builders_batch10.py +159 -0
- package/scripts/engines_builders_batch11.py +174 -0
- package/scripts/engines_builders_batch12.py +103 -0
- package/scripts/engines_builders_batch13.py +340 -0
- package/scripts/engines_builders_batch9.py +1468 -0
- package/scripts/engines_builders_cn.py +2283 -0
- package/scripts/engines_builders_data.py +2923 -0
- package/scripts/engines_builders_data_macro.py +775 -0
- package/scripts/engines_builders_feeds.py +255 -0
- package/scripts/engines_builders_intl.py +1239 -0
- package/scripts/engines_builders_search.py +440 -0
- package/scripts/engines_builders_tech.py +1360 -0
- package/scripts/evidence.py +588 -162
- package/scripts/evidence_loop.py +592 -0
- package/scripts/evidence_tier.py +298 -0
- package/scripts/except_sets.py +25 -0
- package/scripts/extract.py +55 -16
- package/scripts/fact_align.py +207 -0
- package/scripts/fact_crosscheck.py +230 -0
- package/scripts/fetch.py +25 -12
- package/scripts/fetch_browser.py +87 -0
- package/scripts/fetch_jsrun.py +122 -0
- package/scripts/fetch_quality.py +225 -0
- package/scripts/fetch_render_tinyfish.py +117 -0
- package/scripts/fetch_v3.py +1779 -0
- package/scripts/focus_extract.py +350 -0
- package/scripts/fulltext_store.py +169 -0
- package/scripts/fxtwitter_api.py +325 -0
- package/scripts/gen_engine_catalog.py +364 -0
- package/scripts/gen_native_tools.py +91 -0
- package/scripts/gen_routing_snapshot.py +184 -0
- package/scripts/health_check.py +140 -402
- package/scripts/health_probe.py +23 -8
- package/scripts/hot_state.py +134 -0
- package/scripts/http_client.py +1053 -0
- package/scripts/image_ops.py +430 -0
- package/scripts/image_vision/vision_probe +0 -0
- package/scripts/image_vision/vision_probe.swift +126 -0
- package/scripts/install.ps1 +148 -0
- package/scripts/install.sh +206 -0
- package/scripts/job.py +1186 -0
- package/scripts/jump_resolver.py +235 -0
- package/scripts/lang_capability.py +150 -0
- package/scripts/lang_detect.py +289 -0
- package/scripts/lang_matrix_probe.py +329 -0
- package/scripts/lang_pref.py +311 -0
- package/scripts/link_source.py +340 -0
- package/scripts/local_engines.py +321 -0
- package/scripts/local_image.py +970 -0
- package/scripts/local_seek.py +366 -0
- package/scripts/macro_countries.py +82 -0
- package/scripts/matrix_search_eval.py +1105 -0
- package/scripts/mcp_diag.py +8 -3
- package/scripts/mcp_handlers.py +1074 -0
- package/scripts/mcp_handlers_surface.py +157 -0
- package/scripts/mcp_launch.sh +122 -0
- package/scripts/mcp_server.py +118 -417
- package/scripts/mcp_setup.py +407 -0
- package/scripts/mcp_tools.py +330 -0
- package/scripts/mcp_transport.py +191 -0
- package/scripts/min_version_scan.py +335 -0
- package/scripts/net_proxy.py +258 -0
- package/scripts/network_aware.py +152 -0
- package/scripts/paper.py +274 -0
- package/scripts/pdf_extract.py +518 -0
- package/scripts/phrase_filter.py +66 -0
- package/scripts/plan.py +422 -0
- package/scripts/postprocess_signals.py +150 -0
- package/scripts/pre-commit +48 -0
- package/scripts/query_enhance.py +157 -0
- package/scripts/query_operators.py +196 -0
- package/scripts/query_rewriter.py +116 -7
- package/scripts/query_signals.py +300 -0
- package/scripts/query_templates.py +167 -0
- package/scripts/query_understanding.py +411 -0
- package/scripts/query_variants.py +298 -0
- package/scripts/quota.py +488 -76
- package/scripts/rank_signals.py +775 -0
- package/scripts/ranking_eval.py +319 -0
- package/scripts/readability_extract.py +433 -0
- package/scripts/recompute.py +415 -0
- package/scripts/recovery.py +616 -0
- package/scripts/recovery_l0.py +160 -0
- package/scripts/redskill/redskill_engine.py +332 -0
- package/scripts/regression_p0p1.py +318 -0
- package/scripts/replay_eval.py +640 -0
- package/scripts/rerank_tfidf.py +225 -0
- package/scripts/research.py +591 -403
- package/scripts/research_cli.py +361 -0
- package/scripts/research_dossier.py +492 -0
- package/scripts/research_expand.py +146 -0
- package/scripts/research_gates.py +272 -0
- package/scripts/research_report.py +209 -0
- package/scripts/research_strategy.py +37 -0
- package/scripts/research_work_packages.py +195 -0
- package/scripts/result_lang.py +311 -0
- package/scripts/robots_guard.py +259 -0
- package/scripts/route.py +921 -254
- package/scripts/route_bangs.py +40 -0
- package/scripts/route_cache.py +225 -0
- package/scripts/route_combo.py +910 -0
- package/scripts/route_domains.py +328 -0
- package/scripts/route_lang.py +605 -0
- package/scripts/route_log.py +63 -0
- package/scripts/route_policy.py +250 -0
- package/scripts/rrf_classic.py +66 -0
- package/scripts/screenshot.py +73 -0
- package/scripts/search.py +897 -405
- package/scripts/search_attribution.py +88 -0
- package/scripts/search_benchmark.py +407 -0
- package/scripts/search_cli.py +627 -0
- package/scripts/search_entry.py +389 -0
- package/scripts/search_output.py +606 -0
- package/scripts/search_pipeline.py +1077 -0
- package/scripts/search_rank.py +991 -0
- package/scripts/search_types.py +27 -12
- package/scripts/seek_locator.py +68 -0
- package/scripts/semantic_evidence.py +215 -0
- package/scripts/serp_guard.py +228 -0
- package/scripts/serp_kill_log.py +51 -0
- package/scripts/single_flight.py +120 -0
- package/scripts/social_engines/__init__.py +10 -0
- package/scripts/social_engines/bilibili_engine.py +131 -0
- package/scripts/social_engines/hackernews_engine.py +113 -0
- package/scripts/social_engines/reddit_engine.py +172 -0
- package/scripts/social_engines/twitter_engine.py +397 -0
- package/scripts/social_engines/v2ex_engine.py +101 -0
- package/scripts/social_engines/weibo_engine.py +128 -0
- package/scripts/social_engines/xiaohongshu_engine.py +137 -0
- package/scripts/social_engines/zhihu_engine.py +139 -0
- package/scripts/social_research.py +201 -0
- package/scripts/sogou_weixin_browser.py +318 -0
- package/scripts/source_ledger.py +249 -0
- package/scripts/stage_timing.py +58 -0
- package/scripts/stats_cli.py +95 -0
- package/scripts/straggler_policy.py +60 -0
- package/scripts/stylometry_detector.py +231 -0
- package/scripts/sync_backends.py +353 -0
- package/scripts/sync_installs.py +29 -0
- package/scripts/template_cli.py +127 -0
- package/scripts/text_lexicon.py +72 -0
- package/scripts/tfidf_router.py +109 -37
- package/scripts/time_utils.py +233 -0
- package/scripts/topic_research_profiles.py +643 -0
- package/scripts/train.py +737 -0
- package/scripts/tweet.py +596 -0
- package/scripts/url_canon.py +217 -0
- package/scripts/url_optimize.py +56 -0
- package/scripts/url_safety.py +236 -0
- package/scripts/usage_log.py +116 -0
- package/scripts/usage_log_clean.py +113 -0
- package/scripts/v2ex_nodes.py +311 -0
- package/scripts/variant_recall.py +188 -0
- package/scripts/watch.py +205 -0
- package/scripts/wechat_article.py +258 -0
- package/scripts/wide_research.py +402 -0
- package/scripts/wx.py +349 -0
- package/scripts/x_article_fetch.py +141 -0
- package/scripts/x_search.py +234 -0
- package/scripts/x_trending.py +189 -0
- package/scripts/yaml_load.py +67 -0
- package/sub-skills/ego-search/SKILL.md +253 -0
- package/sub-skills/ego-search/references/browser-runtime.md +43 -0
- package/sub-skills/ego-search/references/install.md +67 -0
- package/sub-skills/ego-search/references/original-ego-upgrade.md +51 -0
- package/sub-skills/ego-search/scripts/ego_search.py +800 -0
- package/sub-skills/ego-search/scripts/merge.py +226 -0
- package/sub-skills/ego-search/scripts/quality.py +64 -0
- package/sub-skills/ego-search/scripts/runtime.py +172 -0
- package/sub-skills/ego-search/scripts/safety.py +109 -0
- package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
- package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
- package/sub-skills/ego-search/tests/test_query_normalize.py +68 -0
- package/sub-skills/js-run/SKILL.md +67 -0
- package/sub-skills/js-run/scripts/jsrun.py +112 -0
- package/sub-skills/js-run/scripts/shim.js +188 -0
- package/sub-skills/js-run/tests/fixtures/challenge_like.js +27 -0
- package/sub-skills/js-run/tests/test_jsrun.py +165 -0
- package/sub-skills/local-search/SKILL.md +50 -53
- package/sub-skills/local-search/bm25.py +162 -0
- package/sub-skills/local-search/config.yaml +152 -55
- package/sub-skills/local-search/engine_registry.py +91 -26
- package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
- package/sub-skills/local-search/local_search_adapter.py +13 -2
- package/sub-skills/local-search/parse_maps.yaml +40 -42
- package/sub-skills/local-search/search_v3.py +1182 -65
- package/sub-skills/local-search/smart_router.py +47 -7
- package/sub-skills/local-seek/SKILL.md +110 -0
- package/sub-skills/local-seek/config/domains.yaml +20 -0
- package/sub-skills/local-seek/references/auto-behaviors.md +24 -0
- package/sub-skills/local-seek/references/fd-mdfind-recipes.md +62 -0
- package/sub-skills/local-seek/references/git-integration.md +54 -0
- package/sub-skills/local-seek/references/remote-handoff.md +46 -0
- package/sub-skills/local-seek/references/rg-recipes.md +96 -0
- package/sub-skills/local-seek/references/strategies.md +52 -0
- package/sub-skills/local-seek/references/structural-search.md +59 -0
- package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
- package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
- package/sub-skills/local-seek/scripts/seek.py +1465 -0
- package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
- package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
- package/sub-skills/local-seek/scripts/seek_rank.py +193 -0
- package/sub-skills/local-seek/tests/test_content_rank.py +222 -0
- package/sub-skills/local-seek/tests/test_determinism_20261006.py +145 -0
- package/sub-skills/local-seek/tests/test_noise_tier.py +101 -0
- package/sub-skills/local-seek/tests/test_pcre2_probe.py +136 -0
- package/sub-skills/local-seek/tests/test_rank_pinyin.py +77 -0
- package/sub-skills/local-seek/tests/test_regression_20261003.py +169 -0
- package/sub-skills/local-seek/tests/test_regression_20261006.py +146 -0
- package/sub-skills/local-seek/tests/test_seek_degradation.py +111 -0
- package/sub-skills/local-seek/tests/test_seek_fts.py +313 -0
package/scripts/adaptive.py
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
conn.
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
#
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
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,
|
|
117
|
-
if not
|
|
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 =
|
|
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
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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(
|
|
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()
|