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
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
sync_backends.py — 注册表派生与一致性校验(唯一来源:运行时合并后的引擎声明)
|
|
4
|
+
|
|
5
|
+
设计目标:新增引擎只改一处声明,其余注册表自动派生,消灭手工同步。
|
|
6
|
+
|
|
7
|
+
来源计算方式(与运行时一致):`config.load_config()` 合并后的 engines 段,即
|
|
8
|
+
config.yaml engines + engines/*.yaml + engines/specs/*.yaml
|
|
9
|
+
只读 config.yaml 是不够的——外置 spec 声明的引擎运行时可见、可路由,
|
|
10
|
+
若派生时看不见,派生件就会与运行时事实脱钩(batch7 收录的 7 个引擎
|
|
11
|
+
曾因此只出现在人工维护的 registry 里,而 quota/domain 两份漏侧)。
|
|
12
|
+
|
|
13
|
+
派生关系:
|
|
14
|
+
运行时合并后的 engines 段(唯一来源)
|
|
15
|
+
├── backends/quota_profiles.json 配额/成本/限频(由引擎声明的元数据派生)
|
|
16
|
+
├── backends/engine_registry.yaml 引擎注册表文档(由引擎声明派生)
|
|
17
|
+
└── backends/domain_profiles.json TF-IDF 领域文档(校验引擎名集合,缺失补空模板)
|
|
18
|
+
|
|
19
|
+
用法:
|
|
20
|
+
python3 scripts/sync_backends.py # 派生三份 backends 文件
|
|
21
|
+
python3 scripts/sync_backends.py --check # 只校验不写,不一致退出码非 0
|
|
22
|
+
python3 scripts/sync_backends.py --list # 输出引擎清单与统计
|
|
23
|
+
|
|
24
|
+
退出码:0=一致,1=校验发现不一致(--check 模式)。
|
|
25
|
+
|
|
26
|
+
--check 读的是磁盘上的三份派生件,与重新派生的结果逐项比对。比对对象必须是
|
|
27
|
+
「磁盘现状」而不是「本次派生结果」——后者等于拿结果和自己比,永远一致,
|
|
28
|
+
校验形同虚设(本文件 2026-09-12 前的实际状态)。
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import sys
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
from cli_io import dumps, dumps_pretty
|
|
38
|
+
|
|
39
|
+
# ── 路径 ──────────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
SKILL_DIR = Path(__file__).resolve().parent.parent
|
|
42
|
+
CONFIG_PATH = SKILL_DIR / "config.yaml"
|
|
43
|
+
BACKENDS_DIR = SKILL_DIR / "backends"
|
|
44
|
+
QUOTA_PROFILES_PATH = BACKENDS_DIR / "quota_profiles.json"
|
|
45
|
+
DOMAIN_PROFILES_PATH = BACKENDS_DIR / "domain_profiles.json"
|
|
46
|
+
REGISTRY_PATH = BACKENDS_DIR / "engine_registry.yaml"
|
|
47
|
+
|
|
48
|
+
# ── 默认值 ────────────────────────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
DEFAULT_QUOTA: dict[str, Any] = {
|
|
51
|
+
"qps": 2,
|
|
52
|
+
"limit": None,
|
|
53
|
+
"period": "second",
|
|
54
|
+
"cost_per_call": 0.0,
|
|
55
|
+
"cost_unit": "free",
|
|
56
|
+
"cost_tier": "free",
|
|
57
|
+
"priority": 50,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
DEFAULT_COST_FACTOR = {"free": 1.0, "low": 0.7, "api": 0.5, "paid": 0.3}
|
|
61
|
+
|
|
62
|
+
# registry 里 cost 字段的计算方式(free/token/api),与 cost_tier 的映射
|
|
63
|
+
_COST_TIER_TO_REGISTRY_COST = {"free": "free", "low": "api", "api": "api", "paid": "api"}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _load_yaml(path: Path) -> dict:
|
|
67
|
+
from yaml_load import load as _yaml_parse
|
|
68
|
+
data = _yaml_parse(path)
|
|
69
|
+
return data if isinstance(data, dict) else {}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def load_engines() -> dict[str, dict[str, Any]]:
|
|
73
|
+
"""读取**公共**配置里的全部引擎声明(含禁用)。
|
|
74
|
+
|
|
75
|
+
与运行时同一入口(config.load_config 合并 config.yaml + engines/*.yaml +
|
|
76
|
+
engines/specs/*.yaml),但 **include_local=False**:派生件(engine_registry.yaml /
|
|
77
|
+
quota_profiles.json)随公开仓发版,本机覆盖层 config.local.yaml 是本机状态,
|
|
78
|
+
不该进来——否则本机跑一次 sync 就把「parallel 已停用」这类本机状态写进了
|
|
79
|
+
公开仓。config 模块不可用时(缺依赖/语法错)退回只读 config.yaml,并在
|
|
80
|
+
stderr 明示——静默退回会让派生件少一批引擎却不报错。
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
from config import load_config
|
|
84
|
+
merged = load_config(include_local=False).get("engines", {})
|
|
85
|
+
if merged:
|
|
86
|
+
return {n: s for n, s in merged.items() if isinstance(s, dict)}
|
|
87
|
+
except Exception as e: # noqa: BLE001 — 退回单源并留痕,不吞错误
|
|
88
|
+
print(f"⚠️ config 模块不可用({type(e).__name__}: {e}),"
|
|
89
|
+
f"退回只读 config.yaml——派生件将不含外置 spec 引擎",
|
|
90
|
+
file=sys.stderr)
|
|
91
|
+
cfg = _load_yaml(CONFIG_PATH)
|
|
92
|
+
engines = cfg.get("engines", {})
|
|
93
|
+
return {name: spec for name, spec in engines.items() if isinstance(spec, dict)}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def engine_meta(spec: dict[str, Any], name: str) -> dict[str, Any]:
|
|
97
|
+
"""提取引擎声明的运营元数据,缺失用默认值保底。"""
|
|
98
|
+
meta: dict[str, Any] = {}
|
|
99
|
+
for key, default in DEFAULT_QUOTA.items():
|
|
100
|
+
meta[key] = spec.get(key, default)
|
|
101
|
+
meta["label"] = spec.get("label", name)
|
|
102
|
+
return meta
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── 派生:quota_profiles.json ────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def derive_quota_profiles(engines: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
108
|
+
profiles: dict[str, Any] = {
|
|
109
|
+
"_description": "各引擎的配额/成本/限频配置。由 scripts/sync_backends.py 从运行时引擎声明派生,勿手工修改。",
|
|
110
|
+
}
|
|
111
|
+
for name, spec in sorted(engines.items()):
|
|
112
|
+
meta = engine_meta(spec, name)
|
|
113
|
+
profiles[name] = {
|
|
114
|
+
"label": meta["label"],
|
|
115
|
+
"qps": meta["qps"],
|
|
116
|
+
"limit": meta["limit"],
|
|
117
|
+
"period": meta["period"],
|
|
118
|
+
"cost_per_call": meta["cost_per_call"],
|
|
119
|
+
"cost_unit": meta["cost_unit"],
|
|
120
|
+
"cost_tier": meta["cost_tier"],
|
|
121
|
+
"priority": meta["priority"],
|
|
122
|
+
}
|
|
123
|
+
return profiles
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ── 派生:engine_registry.yaml ───────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
def derive_registry(engines: dict[str, dict[str, Any]]) -> dict[str, Any]:
|
|
129
|
+
"""从引擎声明派生注册表文档。coverage/desc 从引擎声明读,缺省用通用标签。"""
|
|
130
|
+
entries = []
|
|
131
|
+
for name, spec in sorted(engines.items()):
|
|
132
|
+
enabled = spec.get("enabled", True)
|
|
133
|
+
etype = spec.get("type", "cli")
|
|
134
|
+
cost_tier = spec.get("cost_tier", "free")
|
|
135
|
+
# tier 计算方式:T1 直连 API / T2 local-search 本地引擎
|
|
136
|
+
tier = "T2" if name.startswith("local_") else "T1"
|
|
137
|
+
# desc 优先级:声明自述 → label 保底。spec 侧 desc 是权威自述(外置 spec
|
|
138
|
+
# 只声明 engine_id/desc,没有 label),此前只认 label 导致 14 个引擎
|
|
139
|
+
# 的 desc 派生为空——外置声明的引擎在注册表里集体失语。
|
|
140
|
+
label = spec.get("label")
|
|
141
|
+
desc = spec.get("desc") or (f"{label}({etype})".replace("(cli)", "") if label else "")
|
|
142
|
+
entries.append({
|
|
143
|
+
"name": name,
|
|
144
|
+
"tier": tier,
|
|
145
|
+
"type": etype,
|
|
146
|
+
"coverage": spec.get("coverage", ["general"]),
|
|
147
|
+
"latency_ms": spec.get("latency_ms", 2000),
|
|
148
|
+
"cost": spec.get("cost", _COST_TIER_TO_REGISTRY_COST.get(cost_tier, "free")),
|
|
149
|
+
"status": "ok" if enabled else "disabled",
|
|
150
|
+
"recommended": spec.get("recommended", True),
|
|
151
|
+
# explicit_only:设计上不进自动路由(需密钥的源 / 输入形态特殊),
|
|
152
|
+
# 按 --engine 显式调用;可达性检查据此区分「有意显式」与「忘了接线」
|
|
153
|
+
"explicit_only": bool(spec.get("explicit_only")),
|
|
154
|
+
"desc": desc,
|
|
155
|
+
})
|
|
156
|
+
return {
|
|
157
|
+
"_generated": "由 scripts/sync_backends.py 从运行时引擎声明派生,勿手工修改。",
|
|
158
|
+
"version": 2,
|
|
159
|
+
"last_updated": __import__("datetime").date.today().isoformat(),
|
|
160
|
+
"engines": entries,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ── 校验/补全:domain_profiles.json ─────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
def check_domain_profiles(engines: dict[str, dict[str, Any]], profiles: dict[str, Any]) -> list[str]:
|
|
167
|
+
"""校验 domain_profiles 引擎名集合与 config 一致。返回问题列表。"""
|
|
168
|
+
issues = []
|
|
169
|
+
config_names = set(engines.keys())
|
|
170
|
+
domain_names = set(k for k in profiles if not k.startswith("_"))
|
|
171
|
+
missing = sorted(config_names - domain_names)
|
|
172
|
+
stale = sorted(domain_names - config_names)
|
|
173
|
+
if missing:
|
|
174
|
+
issues.append(f"domain_profiles 缺失引擎: {missing}")
|
|
175
|
+
if stale:
|
|
176
|
+
issues.append(f"domain_profiles 含 config 已不存在的引擎: {stale}")
|
|
177
|
+
return issues
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def patch_domain_profiles(engines: dict[str, dict[str, Any]], profiles: dict[str, Any]) -> dict[str, Any]:
|
|
181
|
+
"""为缺失引擎补空模板(documents 留空,TF-IDF 对无文档引擎返回零向量,不影响路由)。"""
|
|
182
|
+
patched = dict(profiles)
|
|
183
|
+
for name, spec in engines.items():
|
|
184
|
+
if name in patched:
|
|
185
|
+
continue
|
|
186
|
+
patched[name] = {
|
|
187
|
+
"label": spec.get("label", name),
|
|
188
|
+
"documents": [],
|
|
189
|
+
"boost_keywords": {},
|
|
190
|
+
"boost_combos": {},
|
|
191
|
+
}
|
|
192
|
+
# 移除 config 已不存在的引擎(孤儿条目)
|
|
193
|
+
for name in [k for k in patched if not k.startswith("_") and k not in engines]:
|
|
194
|
+
del patched[name]
|
|
195
|
+
return patched
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
# ── 主流程 ───────────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
def collect_issues(engines: dict[str, dict[str, Any]],
|
|
201
|
+
quota: dict[str, Any], registry: dict[str, Any],
|
|
202
|
+
domain: dict[str, Any]) -> list[str]:
|
|
203
|
+
"""汇总所有一致性检查问题(引擎名 + 字段值双层)。
|
|
204
|
+
|
|
205
|
+
只比引擎名是不够的:名字都在、值被手改(firecrawl.qps 1→99)同样会让
|
|
206
|
+
派生件与来源脱钩,而限流/配额恰恰是靠这些值生效的。第一版校验只比名字,
|
|
207
|
+
把 qps 手改成 99 试一次,它直接漏报。
|
|
208
|
+
"""
|
|
209
|
+
issues = []
|
|
210
|
+
config_names = set(engines.keys())
|
|
211
|
+
quota_names = set(k for k in quota if not k.startswith("_"))
|
|
212
|
+
reg_entries = {e["name"]: e for e in registry.get("engines", [])}
|
|
213
|
+
reg_names = set(reg_entries)
|
|
214
|
+
|
|
215
|
+
if missing := sorted(config_names - quota_names):
|
|
216
|
+
issues.append(f"quota_profiles 缺失引擎: {missing}")
|
|
217
|
+
if stale := sorted(quota_names - config_names):
|
|
218
|
+
issues.append(f"quota_profiles 含 config 已不存在的引擎: {stale}")
|
|
219
|
+
if missing := sorted(config_names - reg_names):
|
|
220
|
+
issues.append(f"engine_registry 缺失引擎: {missing}")
|
|
221
|
+
if stale := sorted(reg_names - config_names):
|
|
222
|
+
issues.append(f"engine_registry 含 config 已不存在的引擎: {stale}")
|
|
223
|
+
|
|
224
|
+
# quota 值级一致性:声明的运营元数据必须逐字段落到派生件上
|
|
225
|
+
expected_quota = derive_quota_profiles(engines)
|
|
226
|
+
for name in sorted(config_names & quota_names):
|
|
227
|
+
exp = expected_quota.get(name, {})
|
|
228
|
+
cur = quota.get(name, {})
|
|
229
|
+
if not isinstance(cur, dict):
|
|
230
|
+
issues.append(f"quota_profiles {name} 不是对象: {type(cur).__name__}")
|
|
231
|
+
continue
|
|
232
|
+
for key, want in exp.items():
|
|
233
|
+
if cur.get(key) != want:
|
|
234
|
+
issues.append(
|
|
235
|
+
f"quota_profiles {name}.{key} 不一致: 真源={want!r} 派生件={cur.get(key)!r}")
|
|
236
|
+
|
|
237
|
+
# registry 值级一致性(跳过生成时间戳:它不是声明派生的)
|
|
238
|
+
expected_reg = {e["name"]: e for e in derive_registry(engines)["engines"]}
|
|
239
|
+
for name in sorted(config_names & reg_names):
|
|
240
|
+
exp = expected_reg.get(name, {})
|
|
241
|
+
cur = reg_entries.get(name, {})
|
|
242
|
+
for key in ("tier", "type", "coverage", "latency_ms", "cost",
|
|
243
|
+
"status", "recommended", "explicit_only", "desc"):
|
|
244
|
+
if exp.get(key) != cur.get(key):
|
|
245
|
+
issues.append(
|
|
246
|
+
f"engine_registry {name}.{key} 不一致: 真源={exp.get(key)!r} "
|
|
247
|
+
f"派生件={cur.get(key)!r}")
|
|
248
|
+
|
|
249
|
+
issues.extend(check_domain_profiles(engines, domain))
|
|
250
|
+
return issues
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def write_quota(quota: dict[str, Any]) -> None:
|
|
254
|
+
QUOTA_PROFILES_PATH.write_text(dumps_pretty(quota) + "\n", encoding="utf-8")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def write_registry(registry: dict[str, Any]) -> None:
|
|
258
|
+
import yaml
|
|
259
|
+
REGISTRY_PATH.write_text(
|
|
260
|
+
yaml.safe_dump(registry, allow_unicode=True, sort_keys=False, default_flow_style=False),
|
|
261
|
+
encoding="utf-8")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def write_domain(domain: dict[str, Any]) -> None:
|
|
265
|
+
DOMAIN_PROFILES_PATH.write_text(dumps_pretty(domain) + "\n", encoding="utf-8")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def main() -> int:
|
|
269
|
+
import argparse
|
|
270
|
+
parser = argparse.ArgumentParser(
|
|
271
|
+
description="注册表派生与一致性校验(真源:运行时合并后的引擎声明)")
|
|
272
|
+
parser.add_argument("--check", action="store_true", help="只校验不写,不一致退出码 1")
|
|
273
|
+
parser.add_argument("--list", action="store_true", help="输出引擎清单与统计")
|
|
274
|
+
args = parser.parse_args()
|
|
275
|
+
|
|
276
|
+
engines = load_engines()
|
|
277
|
+
if args.list:
|
|
278
|
+
tiers: dict[str, list[str]] = {}
|
|
279
|
+
for n, s in engines.items():
|
|
280
|
+
tiers.setdefault(s.get("cost_tier", "free"), []).append(n)
|
|
281
|
+
# 计算方式收紧(D7):默认数字只有一个——「声明合并后的引擎数」。
|
|
282
|
+
# enabled 与 disabled 由同一入口的 env 就绪判定派生,不再各自成数。
|
|
283
|
+
declared = len(engines)
|
|
284
|
+
try:
|
|
285
|
+
from config import get_engines
|
|
286
|
+
ready_names = set(get_engines() or {})
|
|
287
|
+
except Exception:
|
|
288
|
+
ready_names = {n for n, s in engines.items() if s.get("enabled", True)}
|
|
289
|
+
enabled_declared = {n for n, s in engines.items() if s.get("enabled", True)}
|
|
290
|
+
print(dumps({
|
|
291
|
+
# 默认计算方式:运行时可见的引擎声明总数
|
|
292
|
+
"total": declared,
|
|
293
|
+
"enabled": len(ready_names),
|
|
294
|
+
"disabled": declared - len(ready_names),
|
|
295
|
+
"enabled_declared": len(enabled_declared),
|
|
296
|
+
"by_cost_tier": {k: len(v) for k, v in tiers.items()},
|
|
297
|
+
"engines": sorted(engines),
|
|
298
|
+
}))
|
|
299
|
+
return 0
|
|
300
|
+
|
|
301
|
+
quota_cur = {}
|
|
302
|
+
domain_cur = {}
|
|
303
|
+
if QUOTA_PROFILES_PATH.exists():
|
|
304
|
+
try:
|
|
305
|
+
quota_cur = json.loads(QUOTA_PROFILES_PATH.read_text(encoding="utf-8"))
|
|
306
|
+
except json.JSONDecodeError:
|
|
307
|
+
pass
|
|
308
|
+
if DOMAIN_PROFILES_PATH.exists():
|
|
309
|
+
try:
|
|
310
|
+
domain_cur = json.loads(DOMAIN_PROFILES_PATH.read_text(encoding="utf-8"))
|
|
311
|
+
except json.JSONDecodeError:
|
|
312
|
+
pass
|
|
313
|
+
# registry 现状也要读:校验必须拿「磁盘上的派生件」比对,而不是拿本次
|
|
314
|
+
# 派生结果比对——后者是拿结果跟自己比,恒等,校验永远绿。
|
|
315
|
+
registry_cur: dict[str, Any] = {}
|
|
316
|
+
if REGISTRY_PATH.exists():
|
|
317
|
+
registry_cur = _load_yaml(REGISTRY_PATH)
|
|
318
|
+
|
|
319
|
+
quota_new = derive_quota_profiles(engines)
|
|
320
|
+
registry_new = derive_registry(engines)
|
|
321
|
+
domain_new = patch_domain_profiles(engines, domain_cur)
|
|
322
|
+
|
|
323
|
+
# 校验对象是「磁盘现状 vs 来源」,与本次派生了什么无关——派生结果在这里
|
|
324
|
+
# 只用于写入,不用于自行验证。
|
|
325
|
+
issues = collect_issues(engines, quota_cur, registry_cur, domain_cur)
|
|
326
|
+
|
|
327
|
+
if args.check:
|
|
328
|
+
if issues:
|
|
329
|
+
print("❌ 一致性校验失败:", file=sys.stderr)
|
|
330
|
+
for issue in issues:
|
|
331
|
+
print(f" - {issue}", file=sys.stderr)
|
|
332
|
+
print(" 修复:python3 scripts/sync_backends.py", file=sys.stderr)
|
|
333
|
+
return 1
|
|
334
|
+
print(f"✅ 一致性校验通过:{len(engines)} 个引擎,"
|
|
335
|
+
f"三份派生件与运行时声明一致。")
|
|
336
|
+
return 0
|
|
337
|
+
|
|
338
|
+
write_quota(quota_new)
|
|
339
|
+
write_registry(registry_new)
|
|
340
|
+
write_domain(domain_new)
|
|
341
|
+
print(f"已派生 {len(engines)} 个引擎的注册表:")
|
|
342
|
+
print(f" quota_profiles.json {len(quota_new) - 1} 条")
|
|
343
|
+
print(f" engine_registry.yaml {len(registry_new['engines'])} 条")
|
|
344
|
+
print(f" domain_profiles.json {len([k for k in domain_new if not k.startswith('_')])} 条(缺失已补空模板)")
|
|
345
|
+
if issues:
|
|
346
|
+
print(f" 已修复 {len(issues)} 处此前与真源不一致的项:", file=sys.stderr)
|
|
347
|
+
for issue in issues:
|
|
348
|
+
print(f" ⚠️ {issue}", file=sys.stderr)
|
|
349
|
+
return 0
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
if __name__ == "__main__":
|
|
353
|
+
sys.exit(main())
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""已废弃:多副本 rsync 同步违反「唯一来源」。
|
|
3
|
+
|
|
4
|
+
请改用:
|
|
5
|
+
python3 scripts/link_source.py --to <消费者入口>
|
|
6
|
+
# 或 installs.local.yaml / ARGO_LINK_TARGETS(见 link_source.py 文档)
|
|
7
|
+
|
|
8
|
+
注册表派生仍使用:
|
|
9
|
+
python3 scripts/sync_backends.py
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main() -> int:
|
|
18
|
+
print(
|
|
19
|
+
"sync_installs.py 已废弃(禁止硬编码路径 + rsync 多副本)。\n"
|
|
20
|
+
"请使用:\n"
|
|
21
|
+
" python3 scripts/link_source.py --to <path>\n"
|
|
22
|
+
" python3 scripts/sync_backends.py # 仅派生 backends\n",
|
|
23
|
+
file=sys.stderr,
|
|
24
|
+
)
|
|
25
|
+
return 2
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""template_cli.py — argo template 子命令:查询模板的发现、展开与执行。
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
argo template --list [--json] 列出全部模板(按分组)
|
|
6
|
+
argo template <id> --dry-run [--json] 只看展开后的命令与查询串,不执行
|
|
7
|
+
argo template <id> [--add 词 ...] [转发参数] 展开并执行
|
|
8
|
+
|
|
9
|
+
设计要点:
|
|
10
|
+
- 模板是「命名查询模式」,执行面只有一步:展开 → 转发给该模板的 command。
|
|
11
|
+
- 转发参数原样透传(--engine/--mode/-n/--since/--json …),搜索的语义与开关
|
|
12
|
+
仍由 search.py / x_search.py 各自持有——本模块**不复制**任何搜索开关定义,
|
|
13
|
+
否则就是「同一个开关两处声明、改一处漏一处」的老病。
|
|
14
|
+
- 加载/合并/展开是纯逻辑,全在 query_templates.py;本文件只做参数面与输出面
|
|
15
|
+
(分工同 citations.py / cite_cli.py)。
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import argparse
|
|
21
|
+
import os
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
|
|
25
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
26
|
+
|
|
27
|
+
from cli_io import dumps # noqa: E402
|
|
28
|
+
from query_templates import ( # noqa: E402
|
|
29
|
+
TemplateError, expand, get_template, list_templates, load_templates)
|
|
30
|
+
|
|
31
|
+
SCRIPTS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
32
|
+
|
|
33
|
+
# command → (脚本, 默认参数)。默认参数与 bin/argo 的 dispatcher 保持一致:
|
|
34
|
+
# `argo search` 恒带 --json(其输出契约就是 JSON),x-search 默认人读、--json 选开。
|
|
35
|
+
_COMMAND_SCRIPTS = {
|
|
36
|
+
"search": ("search.py", ["--json"]),
|
|
37
|
+
"x-search": ("x_search.py", []),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _run_list(registry: dict, as_json: bool) -> int:
|
|
42
|
+
rows = list_templates(registry)
|
|
43
|
+
if as_json:
|
|
44
|
+
print(dumps(rows))
|
|
45
|
+
return 0
|
|
46
|
+
if not rows:
|
|
47
|
+
print("(注册表为空)")
|
|
48
|
+
return 0
|
|
49
|
+
cur = None
|
|
50
|
+
for t in rows:
|
|
51
|
+
if t["category"] != cur:
|
|
52
|
+
cur = t["category"]
|
|
53
|
+
print(f"\n{cur}")
|
|
54
|
+
print(f" {t['id']:<20} {t['description']}")
|
|
55
|
+
print(f"\n共 {len(rows)} 个模板。展开看命令:argo template <id> --dry-run")
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _run_template(tid: str, adds: list[str], dry_run: bool,
|
|
60
|
+
as_json: bool, forwarded: list[str], registry: dict) -> int:
|
|
61
|
+
t = get_template(registry, tid)
|
|
62
|
+
query = expand(t, adds)
|
|
63
|
+
command = t["command"]
|
|
64
|
+
script, defaults = _COMMAND_SCRIPTS[command]
|
|
65
|
+
|
|
66
|
+
if dry_run:
|
|
67
|
+
if as_json:
|
|
68
|
+
print(dumps({"id": tid, "command": command,
|
|
69
|
+
"query": query, "script": script}))
|
|
70
|
+
else:
|
|
71
|
+
print(f"# 模板: {tid} → {command}")
|
|
72
|
+
print(query)
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
argv = [sys.executable, os.path.join(SCRIPTS_DIR, script)]
|
|
76
|
+
argv += defaults + [query] + forwarded
|
|
77
|
+
proc = subprocess.run(argv, encoding="utf-8", errors="replace")
|
|
78
|
+
return proc.returncode
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def main(argv: list | None = None) -> int:
|
|
82
|
+
ap = argparse.ArgumentParser(
|
|
83
|
+
prog="argo template",
|
|
84
|
+
description="查询模板:命名好的检索模式,一步展开并执行",
|
|
85
|
+
epilog="示例:\n"
|
|
86
|
+
" argo template --list\n"
|
|
87
|
+
" argo template ai_model_release --add \"GPT-5\" -n 20\n"
|
|
88
|
+
" argo template ai_zh --sort top --json",
|
|
89
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
90
|
+
ap.add_argument("template", nargs="?", help="模板 id(见 --list)")
|
|
91
|
+
ap.add_argument("--list", action="store_true", help="列出全部模板")
|
|
92
|
+
ap.add_argument("--dry-run", action="store_true",
|
|
93
|
+
help="只打印展开后的命令与查询串,不执行")
|
|
94
|
+
ap.add_argument("--add", action="append", default=[], metavar="词",
|
|
95
|
+
help="追加检索词(可多次;空格即 AND),与模板 query 组合")
|
|
96
|
+
# --json 在此声明以便 --list 消费;非列表场景下会被重新塞回转发参数,
|
|
97
|
+
# 让底层命令拿到它(`argo template <id> --json` = 要 JSON 结果)。
|
|
98
|
+
ap.add_argument("--json", action="store_true", help="JSON 输出")
|
|
99
|
+
args, forwarded = ap.parse_known_args(argv)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
registry = load_templates()
|
|
103
|
+
except TemplateError as e:
|
|
104
|
+
print(f"错误: {e}", file=sys.stderr)
|
|
105
|
+
return 1
|
|
106
|
+
|
|
107
|
+
if args.list:
|
|
108
|
+
return _run_list(registry, args.json)
|
|
109
|
+
|
|
110
|
+
if not args.template:
|
|
111
|
+
ap.print_help()
|
|
112
|
+
return 1
|
|
113
|
+
|
|
114
|
+
fwd = list(forwarded)
|
|
115
|
+
if args.json:
|
|
116
|
+
fwd.append("--json")
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
return _run_template(args.template, args.add, args.dry_run,
|
|
120
|
+
args.json, fwd, registry)
|
|
121
|
+
except TemplateError as e:
|
|
122
|
+
print(f"错误: {e}", file=sys.stderr)
|
|
123
|
+
return 1
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
sys.exit(main())
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""text_lexicon.py — 文本词表唯一来源的延迟加载器。
|
|
3
|
+
|
|
4
|
+
backends/text_lexicon.json 是词表的唯一真源(地域词 / 公司名 / 核心指标),
|
|
5
|
+
fact_crosscheck 只经本模块读取,不再各硬编码一份(2026-10-16:另一消费者 query_signals_v2 已按方案一回收,其「不是X」否定补进 query_understanding,模块本体移入 .trash/)。
|
|
6
|
+
|
|
7
|
+
性能纪律:模块级不读文件(import 零成本),首次调用时读一次并缓存;
|
|
8
|
+
JSON 缺失/损坏时退化到内嵌最小保底,保证纯函数模块永不因词表崩溃。
|
|
9
|
+
测试隔离:设 ARGO_TEXT_LEXICON 指向另一份 JSON 即可换表,调 reload_lexicon() 生效。
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import functools
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
ENV_LEXICON_PATH = "ARGO_TEXT_LEXICON"
|
|
20
|
+
|
|
21
|
+
# 内嵌最小保底:仅保证核心断言(茅台≠地域、非同期≠否定、营收可比)成立。
|
|
22
|
+
# 正常安装永远走 JSON,这里只是「文件被删也不崩」的保险。
|
|
23
|
+
_FALLBACK: dict[str, frozenset[str]] = {
|
|
24
|
+
"geo_keywords": frozenset({"北京", "上海", "广州", "深圳", "美国", "日本"}),
|
|
25
|
+
"company_names": frozenset({"贵州茅台", "云南白药", "中国石化", "中国石油"}),
|
|
26
|
+
"core_indicators": frozenset({
|
|
27
|
+
"营收", "收入", "净利润", "利润", "GDP", "CPI", "PMI",
|
|
28
|
+
"进球", "助攻", "价格", "市值", "ROE", "同比", "环比",
|
|
29
|
+
}),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _lexicon_path() -> Path:
|
|
34
|
+
override = os.environ.get(ENV_LEXICON_PATH, "").strip()
|
|
35
|
+
if override:
|
|
36
|
+
return Path(override).expanduser()
|
|
37
|
+
return Path(__file__).resolve().parent.parent / "backends" / "text_lexicon.json"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@functools.lru_cache(maxsize=1)
|
|
41
|
+
def _load() -> dict[str, frozenset[str]]:
|
|
42
|
+
try:
|
|
43
|
+
raw: dict[str, Any] = json.loads(_lexicon_path().read_text(encoding="utf-8"))
|
|
44
|
+
out: dict[str, frozenset[str]] = {}
|
|
45
|
+
for key in ("geo_keywords", "company_names", "core_indicators"):
|
|
46
|
+
vals = raw.get(key) or []
|
|
47
|
+
out[key] = frozenset(str(v) for v in vals if str(v).strip())
|
|
48
|
+
if not any(out.values()):
|
|
49
|
+
raise ValueError("empty lexicon")
|
|
50
|
+
return out
|
|
51
|
+
except Exception:
|
|
52
|
+
return dict(_FALLBACK)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def reload_lexicon() -> None:
|
|
56
|
+
"""清缓存(换 ARGO_TEXT_LEXICON 后或测试中调用)。"""
|
|
57
|
+
_load.cache_clear()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def geo_keywords() -> frozenset[str]:
|
|
61
|
+
"""地域词(城市/省份/国家)。"""
|
|
62
|
+
return _load()["geo_keywords"]
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def company_names() -> frozenset[str]:
|
|
66
|
+
"""公司名词典(地域判定时优先排除这些词内部的地名)。"""
|
|
67
|
+
return _load()["company_names"]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def core_indicators() -> frozenset[str]:
|
|
71
|
+
"""核心指标词(数字交叉验证用)。"""
|
|
72
|
+
return _load()["core_indicators"]
|