argo-search 1.0.1 → 2.9.8
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 +735 -0
- package/README.es.md +777 -0
- package/README.ja.md +777 -0
- package/README.ko.md +777 -0
- package/README.md +778 -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/why-better.svg +104 -0
- package/assets/readme/workflow.svg +83 -0
- package/backends/domain_profiles.json +2356 -96
- package/backends/engine_registry.yaml +3428 -503
- package/backends/query_synonyms_cn.json +35 -0
- package/backends/quota_profiles.json +2488 -160
- package/backends/source_types_cn.json +109 -0
- package/backends/text_lexicon.json +44 -0
- package/bin/argo +772 -0
- package/bin/argo.js +102 -39
- package/config.yaml +5139 -367
- 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 +575 -0
- package/docs/MCP_SETUP.md +108 -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/RELEASE_NOTES_v2.9.8.md +73 -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/cfets.yaml +41 -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/defillama.yaml +38 -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 +36 -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/gleif.yaml +55 -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/hkex.yaml +40 -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/keenable_free.yaml +36 -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/shfe.yaml +39 -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/tavily_free.yaml +56 -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/yuanbao.yaml +93 -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 +106 -0
- package/package.json +55 -12
- package/packages/dsh-plugin/README.md +152 -0
- package/packages/dsh-plugin/cordis.patch.yml +48 -0
- package/packages/dsh-plugin/dsh/client.js +1533 -0
- package/packages/dsh-plugin/dsh/diagnostics.js +54 -0
- package/packages/dsh-plugin/dsh/index.js +527 -0
- package/packages/dsh-plugin/dsh/native-tools.mjs +749 -0
- package/packages/dsh-plugin/dsh/runner.js +666 -0
- package/packages/dsh-plugin/dsh/wide-research.js +878 -0
- package/packages/dsh-plugin/package.json +38 -0
- package/packages/dsh-plugin/scripts/sync-local.sh +70 -0
- package/packages/dsh-plugin/test/client.test.js +406 -0
- package/packages/dsh-plugin/test/config.test.js +30 -0
- package/packages/dsh-plugin/test/diagnostics.test.js +28 -0
- package/packages/dsh-plugin/test/index.test.js +1382 -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 +294 -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/article_meta.py +131 -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/budget.py +247 -0
- package/scripts/budget_cli.py +93 -0
- package/scripts/build_release_assets.sh +65 -0
- package/scripts/burstiness_detector.py +147 -0
- package/scripts/cache.py +1066 -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/ci_drift_gate.py +90 -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_boundary.py +46 -0
- package/scripts/content_security.py +685 -0
- package/scripts/content_signals.py +751 -0
- package/scripts/crawl.py +74 -35
- package/scripts/cross_lingual_trace.py +293 -0
- package/scripts/daemon.py +866 -0
- package/scripts/digup_bridge.py +214 -0
- package/scripts/dispatch_result.py +98 -0
- package/scripts/engine_admission.py +366 -0
- package/scripts/engine_dispatch.py +982 -0
- package/scripts/engine_env.py +539 -0
- package/scripts/engine_failure.py +411 -0
- package/scripts/engine_families.py +738 -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 +799 -294
- package/scripts/engines_base.py +1521 -0
- package/scripts/engines_builders.py +310 -0
- package/scripts/engines_builders_batch10.py +204 -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_batch14.py +656 -0
- package/scripts/engines_builders_batch15.py +249 -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_keyless.py +111 -0
- package/scripts/engines_builders_search.py +440 -0
- package/scripts/engines_builders_tech.py +1359 -0
- package/scripts/evidence.py +620 -166
- package/scripts/evidence_loop.py +624 -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 +1804 -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 +195 -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.swift +126 -0
- package/scripts/install.ps1 +166 -0
- package/scripts/install.sh +221 -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_image.py +970 -0
- package/scripts/local_seek.py +402 -0
- package/scripts/macro_countries.py +97 -0
- package/scripts/matrix_search_eval.py +1187 -0
- package/scripts/mcp_compact.py +234 -0
- package/scripts/mcp_diag.py +8 -3
- package/scripts/mcp_handlers.py +492 -0
- package/scripts/mcp_handlers_surface.py +157 -0
- package/scripts/mcp_launch.sh +124 -0
- package/scripts/mcp_local.py +241 -0
- package/scripts/mcp_server.py +125 -419
- package/scripts/mcp_setup.py +431 -0
- package/scripts/mcp_tools.py +331 -0
- package/scripts/mcp_transport.py +191 -0
- package/scripts/mcp_warm.py +127 -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/output_transparency.py +200 -0
- package/scripts/paid_policy.py +98 -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 +633 -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/recovery_runner.py +150 -0
- package/scripts/redskill/redskill_engine.py +332 -0
- package/scripts/regression_p0p1.py +318 -0
- package/scripts/replay_eval.py +658 -0
- package/scripts/rerank_tfidf.py +225 -0
- package/scripts/research.py +607 -403
- package/scripts/research_cli.py +361 -0
- package/scripts/research_dossier.py +492 -0
- package/scripts/research_expand.py +169 -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/retry_policy.py +76 -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 +243 -0
- package/scripts/route_combo.py +917 -0
- package/scripts/route_domains.py +341 -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 +886 -406
- package/scripts/search_attribution.py +185 -0
- package/scripts/search_benchmark.py +407 -0
- package/scripts/search_cli.py +640 -0
- package/scripts/search_entry.py +395 -0
- package/scripts/search_output.py +690 -0
- package/scripts/search_pipeline.py +926 -0
- package/scripts/search_rank.py +991 -0
- package/scripts/search_types.py +129 -16
- package/scripts/seek_locator.py +68 -0
- package/scripts/semantic_evidence.py +391 -0
- package/scripts/serp_guard.py +336 -0
- package/scripts/serp_kill_log.py +74 -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 +107 -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 +447 -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/scripts/zhihu_quota.py +139 -0
- package/sub-skills/ego-search/SKILL.md +253 -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/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/local-search/SKILL.md +50 -53
- package/sub-skills/local-search/bm25.py +186 -0
- package/sub-skills/local-search/cli_runner.py +331 -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/parsers.py +328 -0
- package/sub-skills/local-search/search_v3.py +716 -242
- 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/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 +197 -0
package/scripts/evidence.py
CHANGED
|
@@ -1,73 +1,100 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
evidence.py —
|
|
3
|
+
evidence.py — 来源可信度评估(v2.2 两阶段:Selection × Absorption)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
问题重定义(第一性):
|
|
6
|
+
Agent 要的不是「搜到了什么」,而是「哪些材料可被吸收进可核验答案」。
|
|
7
|
+
被检索到(selection)≠ 被采用为证据(absorption)。
|
|
8
|
+
|
|
9
|
+
MECE 分解:
|
|
10
|
+
A. Selection 门槛:URL 是否可引用页面 / 域名权威 / 是否 SERP·榜单污染
|
|
11
|
+
B. Absorption 深度:数字·定义·对比·披露 等证据块密度(snippet 或正文)
|
|
12
|
+
C. Freshness:发布年/URL 年;忽略「2015年以来」类历史对比年
|
|
13
|
+
D. Consensus:多域名佐证(交叉验证)
|
|
14
|
+
|
|
15
|
+
综合:
|
|
16
|
+
final = 0.40 * selection + 0.35 * absorption + 0.15 * freshness + 0.10 * original
|
|
17
|
+
high-stakes 可再 × consensus 调节(见 compute_credibility)
|
|
10
18
|
|
|
11
19
|
用法:
|
|
12
|
-
python3 evidence.py
|
|
13
|
-
python3 evidence.py --search-result '{"results": [...]}' --query "茅台股价"
|
|
14
|
-
echo '{"results": [...]}' | python3 evidence.py --stdin --query "查询词"
|
|
20
|
+
echo '{"results": [...]}' | python3 evidence.py "查询词" --stdin --json
|
|
15
21
|
"""
|
|
16
22
|
|
|
17
23
|
from __future__ import annotations
|
|
18
24
|
|
|
19
25
|
import argparse
|
|
26
|
+
import functools
|
|
20
27
|
import json
|
|
21
28
|
import os
|
|
22
29
|
import re
|
|
23
30
|
import sys
|
|
24
31
|
import time
|
|
32
|
+
from datetime import datetime
|
|
25
33
|
from typing import Any
|
|
26
34
|
from urllib.parse import urlparse
|
|
35
|
+
from cli_io import dumps, ensure_utf8_stdio
|
|
27
36
|
|
|
28
37
|
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
29
38
|
sys.path.insert(0, SCRIPT_DIR)
|
|
30
39
|
|
|
40
|
+
try:
|
|
41
|
+
from content_signals import score_evidence_density
|
|
42
|
+
except ImportError: # pragma: no cover
|
|
43
|
+
def score_evidence_density(text: str, title: str = "") -> dict[str, Any]:
|
|
44
|
+
return {
|
|
45
|
+
"has_numbers": False,
|
|
46
|
+
"has_definition": False,
|
|
47
|
+
"has_comparison": False,
|
|
48
|
+
"has_howto": False,
|
|
49
|
+
"has_disclose": False,
|
|
50
|
+
"is_qa_format": False,
|
|
51
|
+
"absorption_score": 0.2,
|
|
52
|
+
}
|
|
53
|
+
|
|
31
54
|
|
|
32
55
|
# ── 权威性评分 ────────────────────────────────────────────────────────────────
|
|
33
56
|
|
|
34
|
-
# 域名权威性分级(越高越好)
|
|
35
57
|
AUTHORITY_TIERS = {
|
|
36
|
-
# Tier 1:
|
|
58
|
+
# Tier 1: 官方/权威
|
|
37
59
|
"gov.cn": 1.0, "gov": 0.95, "edu.cn": 0.95, "edu": 0.9,
|
|
38
|
-
"ac.cn": 0.95,
|
|
60
|
+
"ac.cn": 0.95,
|
|
39
61
|
"nature.com": 0.95, "science.org": 0.95, "ieee.org": 0.9,
|
|
40
62
|
"acm.org": 0.9, "springer.com": 0.9, "elsevier.com": 0.9,
|
|
41
63
|
"arxiv.org": 0.9, "pubmed.ncbi.nlm.nih.gov": 0.95,
|
|
42
64
|
"scholar.google.com": 0.85, "ncbi.nlm.nih.gov": 0.95,
|
|
43
|
-
"nvd.nist.gov": 0.95,
|
|
44
|
-
"
|
|
65
|
+
"nvd.nist.gov": 0.95, "cve.mitre.org": 0.95,
|
|
66
|
+
"xinhuanet.com": 0.95, "people.com.cn": 0.95,
|
|
45
67
|
|
|
46
|
-
# Tier 2:
|
|
68
|
+
# Tier 2: 专业媒体/平台
|
|
47
69
|
"zhihu.com": 0.85, "github.com": 0.85, "stackoverflow.com": 0.85,
|
|
48
70
|
"medium.com": 0.75, "dev.to": 0.75,
|
|
49
71
|
"reuters.com": 0.9, "bloomberg.com": 0.9, "wsj.com": 0.9,
|
|
50
|
-
"
|
|
72
|
+
"caixin.com": 0.9, "yicai.com": 0.86, "wallstreetcn.com": 0.86,
|
|
73
|
+
"cls.cn": 0.88, "api3.cls.cn": 0.85, "cs.com.cn": 0.92, "stcn.com": 0.88,
|
|
74
|
+
"cnr.cn": 0.9, "thepaper.cn": 0.82, "jiemian.com": 0.8,
|
|
51
75
|
"36kr.com": 0.8, "infoq.cn": 0.8, "juejin.cn": 0.75,
|
|
52
|
-
"eastmoney.com": 0.85, "xueqiu.com": 0.8,
|
|
76
|
+
"eastmoney.com": 0.85, "xueqiu.com": 0.8, "10jqka.com.cn": 0.78,
|
|
53
77
|
"docs.python.org": 0.9, "react.dev": 0.9, "nextjs.org": 0.9,
|
|
54
78
|
|
|
55
|
-
# Tier 3:
|
|
79
|
+
# Tier 3: 通用可信
|
|
56
80
|
"wikipedia.org": 0.75, "baike.baidu.com": 0.7,
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
"
|
|
81
|
+
"linkedin.com": 0.65, "twitter.com": 0.45, "x.com": 0.45,
|
|
82
|
+
"reddit.com": 0.55,
|
|
83
|
+
# 主流门户:与 evidence_tier.py B 级「知名第三方」同口径(163/sohu/sina
|
|
84
|
+
# 都在 B 级名单里)。此前压在下方「内容农场」档 0.4-0.55,两份名单
|
|
85
|
+
# 自相矛盾,中文主流媒体被系统性降权(09-26 审查第 7 项,十项 P0 的
|
|
86
|
+
# 最后一项,2026-10-02 归位)。k.sina.com.cn 是聚合/博客通道,留 Tier 4。
|
|
87
|
+
"163.com": 0.65, "sohu.com": 0.65, "sina.com.cn": 0.65,
|
|
88
|
+
|
|
89
|
+
# Tier 4: 内容农场/低质
|
|
90
|
+
"k.sina.com.cn": 0.55, "baijiahao.baidu.com": 0.35,
|
|
91
|
+
"zhuanlan.zhihu.com": 0.7, "toutiao.com": 0.4, "weixin.qq.com": 0.5,
|
|
92
|
+
"guba.eastmoney.com": 0.35,
|
|
93
|
+
|
|
94
|
+
# 商业榜单(高引用≠高可信)
|
|
95
|
+
"maigoo.com": 0.28, "chinapp.com": 0.28, "cnpp.cn": 0.28,
|
|
68
96
|
}
|
|
69
97
|
|
|
70
|
-
# 来源类型映射
|
|
71
98
|
SOURCE_TYPE_MAP = {
|
|
72
99
|
"eastmoney": ("金融数据", 0.9),
|
|
73
100
|
"zhihu": ("社区观点", 0.8),
|
|
@@ -75,149 +102,294 @@ SOURCE_TYPE_MAP = {
|
|
|
75
102
|
"semantic_scholar": ("学术索引", 0.9),
|
|
76
103
|
"openalex": ("学术索引", 0.85),
|
|
77
104
|
"crossref": ("学术元数据", 0.9),
|
|
105
|
+
"europepmc": ("生物医学文献", 0.88),
|
|
106
|
+
"dblp": ("CS 文献索引", 0.85),
|
|
78
107
|
"github": ("代码仓库", 0.85),
|
|
79
108
|
"byted": ("中文搜索", 0.7),
|
|
80
109
|
"bocha": ("中文搜索", 0.7),
|
|
81
110
|
"tavily": ("AI搜索", 0.75),
|
|
82
111
|
"felo": ("AI搜索", 0.7),
|
|
83
|
-
"duckduckgo": ("通用搜索", 0.65),
|
|
84
112
|
"wikipedia": ("百科", 0.75),
|
|
113
|
+
"baidu_baike": ("中文百科", 0.72),
|
|
114
|
+
"mdn": ("官方文档", 0.9),
|
|
115
|
+
"pypi": ("包注册表", 0.88),
|
|
116
|
+
"npm": ("包注册表", 0.88),
|
|
117
|
+
"huggingface": ("模型仓库", 0.85),
|
|
118
|
+
"docker_hub": ("容器镜像", 0.8),
|
|
119
|
+
"juejin": ("中文技术社区", 0.65),
|
|
120
|
+
"devto": ("技术社区", 0.65),
|
|
121
|
+
"clinicaltrials": ("临床试验注册", 0.9),
|
|
122
|
+
"openfda": ("药品标签", 0.88),
|
|
123
|
+
"archive_org": ("数字档案馆", 0.75),
|
|
124
|
+
"steam": ("游戏商店", 0.7),
|
|
125
|
+
"polymarket": ("预测市场叙事", 0.35),
|
|
85
126
|
"anysearch": ("垂直搜索", 0.75),
|
|
86
|
-
"
|
|
127
|
+
"local_search": ("本地搜索", 0.7),
|
|
87
128
|
"metaso": ("AI搜索", 0.75),
|
|
129
|
+
"local_baidu": ("搜索结果页聚合", 0.35),
|
|
130
|
+
"local_sogou": ("搜索结果页聚合", 0.35),
|
|
131
|
+
"local_bing": ("搜索结果页聚合", 0.4),
|
|
132
|
+
"finviz": ("美股快照", 0.75),
|
|
133
|
+
"seeking_alpha": ("美股分析", 0.7),
|
|
134
|
+
"jin10": ("财经快讯", 0.65),
|
|
135
|
+
"models_dev": ("模型目录", 0.85),
|
|
136
|
+
"octen": ("AI搜索", 0.7),
|
|
137
|
+
"qweather": ("天气数据", 0.9),
|
|
138
|
+
"wenshu": ("裁判文书", 0.75),
|
|
139
|
+
"zhihu_global": ("社区观点", 0.8),
|
|
140
|
+
"zhihu_hot": ("社区热榜", 0.55),
|
|
88
141
|
}
|
|
89
142
|
|
|
143
|
+
# SERP/跳转 URL 判定已迁入 serp_guard(纯 URL 规则,轻依赖;热路径不再
|
|
144
|
+
# 为一次判定付出本模块的证据质量栈导入)。此处转出仅为既有调用方兼容,
|
|
145
|
+
# 新代码直接从 serp_guard import。判定的实现注释(为什么后缀匹配、为什么
|
|
146
|
+
# 单一数据源)见 serp_guard.py。
|
|
147
|
+
from serp_guard import ( # noqa: E402
|
|
148
|
+
_load_cn_source_types,
|
|
149
|
+
_normalize_domain,
|
|
150
|
+
is_serp_or_jump_url,
|
|
151
|
+
)
|
|
90
152
|
|
|
91
|
-
def score_authority(url: str, source: str = "") -> dict[str, Any]:
|
|
92
|
-
"""评估 URL 的权威性。"""
|
|
93
|
-
if not url:
|
|
94
|
-
return {"score": 0.3, "reason": "无 URL", "tier": "unknown"}
|
|
95
153
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
154
|
+
@functools.lru_cache(maxsize=512)
|
|
155
|
+
def _score_authority_cached(url: str, source: str = "") -> tuple:
|
|
156
|
+
"""score_authority 的缓存版本,返回 tuple(lru_cache 要求可哈希返回值)。"""
|
|
157
|
+
if not url:
|
|
158
|
+
return (0.3, "无 URL", "unknown", "", True)
|
|
159
|
+
|
|
160
|
+
domain = _normalize_domain(url)
|
|
161
|
+
is_serp = is_serp_or_jump_url(url)
|
|
162
|
+
|
|
163
|
+
if is_serp:
|
|
164
|
+
return (
|
|
165
|
+
0.12,
|
|
166
|
+
"搜索结果页/跳转链(不可作吸收源)",
|
|
167
|
+
"very_low",
|
|
168
|
+
domain,
|
|
169
|
+
True,
|
|
170
|
+
)
|
|
99
171
|
|
|
100
|
-
# 精确匹配域名
|
|
101
172
|
best_score = 0.5
|
|
102
173
|
best_reason = "通用域名"
|
|
103
|
-
|
|
174
|
+
best_len = -1
|
|
104
175
|
for pattern, score in AUTHORITY_TIERS.items():
|
|
105
176
|
if domain == pattern or domain.endswith("." + pattern):
|
|
106
|
-
if
|
|
177
|
+
if len(pattern) > best_len:
|
|
178
|
+
best_len = len(pattern)
|
|
107
179
|
best_score = score
|
|
108
180
|
best_reason = f"域名匹配:{pattern}"
|
|
109
181
|
|
|
110
|
-
#
|
|
182
|
+
# JSON 覆盖
|
|
183
|
+
for d, score in (_load_cn_source_types().get("authority_overrides") or {}).items():
|
|
184
|
+
if domain == d or domain.endswith("." + d):
|
|
185
|
+
if len(d) >= best_len:
|
|
186
|
+
best_len = len(d)
|
|
187
|
+
best_score = float(score)
|
|
188
|
+
best_reason = f"中文信源表:{d}"
|
|
189
|
+
|
|
190
|
+
demote = _load_cn_source_types().get("demote_domains") or {}
|
|
191
|
+
for d, meta in demote.items():
|
|
192
|
+
if domain == d or domain.endswith("." + d):
|
|
193
|
+
best_score = float(meta.get("score", 0.3))
|
|
194
|
+
best_reason = f"降权:{meta.get('reason', d)}"
|
|
195
|
+
break
|
|
196
|
+
|
|
197
|
+
social = _load_cn_source_types().get("social_narrative_only") or {}
|
|
198
|
+
for d, score in social.items():
|
|
199
|
+
if domain == d or domain.endswith("." + d):
|
|
200
|
+
best_score = min(best_score, float(score))
|
|
201
|
+
best_reason = f"社交/叙事源(非事实真值):{d}"
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
# 路径特征
|
|
205
|
+
try:
|
|
206
|
+
path = urlparse(url).path.lower()
|
|
207
|
+
except Exception:
|
|
208
|
+
path = ""
|
|
111
209
|
if "/docs/" in path or "/documentation/" in path:
|
|
112
210
|
best_score = min(best_score + 0.05, 1.0)
|
|
113
211
|
best_reason += "(文档路径)"
|
|
114
212
|
if "/paper/" in path or "/arxiv/" in path or "/abs/" in path:
|
|
115
213
|
best_score = min(best_score + 0.05, 1.0)
|
|
116
214
|
best_reason += "(论文路径)"
|
|
117
|
-
if "/issues/" in path or "/pull/" in path:
|
|
118
|
-
best_score = min(best_score + 0.03, 1.0)
|
|
119
|
-
best_reason += "(Issue/PR路径)"
|
|
120
215
|
|
|
121
|
-
#
|
|
216
|
+
# 来源类型:作为保底分(max),本地 SERP 聚合引擎不抬升
|
|
122
217
|
if source and source in SOURCE_TYPE_MAP:
|
|
123
218
|
type_name, type_score = SOURCE_TYPE_MAP[source]
|
|
124
|
-
if type_score
|
|
125
|
-
|
|
126
|
-
|
|
219
|
+
if source.startswith("local_") and type_score < 0.5:
|
|
220
|
+
# 搜索结果页聚合:压低
|
|
221
|
+
best_score = min(best_score, type_score)
|
|
222
|
+
best_reason += f"(引擎={source}/{type_name})"
|
|
223
|
+
else:
|
|
224
|
+
if type_score > best_score:
|
|
225
|
+
best_score = type_score
|
|
226
|
+
best_reason = f"来源类型:{type_name}" if best_reason == "通用域名" \
|
|
227
|
+
else f"{best_reason}(引擎={source}↑)"
|
|
228
|
+
else:
|
|
229
|
+
best_reason += f"(引擎={source})"
|
|
230
|
+
|
|
231
|
+
tier = (
|
|
232
|
+
"high" if best_score >= 0.8
|
|
233
|
+
else "medium" if best_score >= 0.6
|
|
234
|
+
else "low" if best_score >= 0.35
|
|
235
|
+
else "very_low"
|
|
236
|
+
)
|
|
127
237
|
|
|
128
|
-
|
|
238
|
+
return (best_score, best_reason, tier, domain, bool(is_serp))
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def score_authority(url: str, source: str = "") -> dict[str, Any]:
|
|
242
|
+
"""评估 URL 的权威性(Selection 门槛)。
|
|
129
243
|
|
|
244
|
+
结果按 (url, source) 做进程内 lru_cache:同一 URL + source 组合在多次
|
|
245
|
+
rerank/selection 中只算一次,避免 search.py 的 local_five_dim_rerank 与
|
|
246
|
+
_attach_selection_signals 对同一 URL 重复评分。
|
|
247
|
+
"""
|
|
248
|
+
score, reason, tier, domain, is_serp = _score_authority_cached(url, source)
|
|
130
249
|
return {
|
|
131
|
-
"score": round(
|
|
132
|
-
"reason":
|
|
250
|
+
"score": round(score, 2),
|
|
251
|
+
"reason": reason,
|
|
133
252
|
"tier": tier,
|
|
134
253
|
"domain": domain,
|
|
254
|
+
"is_serp": is_serp,
|
|
135
255
|
}
|
|
136
256
|
|
|
137
257
|
|
|
138
258
|
# ── 时效性评分 ────────────────────────────────────────────────────────────────
|
|
139
259
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
260
|
+
# 「2015年以来」等历史对比年 — 不当作发布年
|
|
261
|
+
_HIST_SINCE_RE = re.compile(
|
|
262
|
+
r"(?:自|从)?(19|20)\d{2}\s*年?\s*(?:以来|起|至今|至|到现在|之前|以前|以前水平)"
|
|
263
|
+
)
|
|
264
|
+
_FULL_DATE_RE = re.compile(
|
|
265
|
+
r"(20\d{2})[年/-](\d{1,2})[月/-](\d{1,2})"
|
|
266
|
+
)
|
|
267
|
+
_PUBLISH_HINT_RE = re.compile(
|
|
268
|
+
r"(?:发布|披露|刊登|更新|Published|发表)[^\n]{0,12}(20\d{2})"
|
|
269
|
+
r"|(20\d{2})\s*年\s*(?:[0-1]?\d\s*月)?\s*(?:[0-3]?\d\s*日)?\s*(?:电|讯|报道)"
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _years_from_text(text: str) -> list[int]:
|
|
274
|
+
"""提取候选年份,排除历史对比语境中的旧年。"""
|
|
275
|
+
if not text:
|
|
276
|
+
return []
|
|
277
|
+
# 挖掉「2015年以来」整段,避免污染
|
|
278
|
+
cleaned = _HIST_SINCE_RE.sub(" ", text)
|
|
279
|
+
# 中文语境不用 \b(「2026年」左侧无词界)
|
|
280
|
+
years = [int(y) for y in re.findall(r"(?<![0-9])(20\d{2})(?![0-9])", cleaned)]
|
|
281
|
+
# 完整日期优先(用原文,保留结构)
|
|
282
|
+
for m in _FULL_DATE_RE.finditer(text):
|
|
283
|
+
years.append(int(m.group(1)))
|
|
284
|
+
for m in _PUBLISH_HINT_RE.finditer(text):
|
|
285
|
+
for g in m.groups():
|
|
286
|
+
if g and re.fullmatch(r"20\d{2}", g):
|
|
287
|
+
years.append(int(g))
|
|
288
|
+
# 合理范围
|
|
289
|
+
now_y = datetime.now().year
|
|
290
|
+
return [y for y in years if 1990 <= y <= now_y + 1]
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@functools.lru_cache(maxsize=512)
|
|
294
|
+
def _score_freshness_cached(url: str, title: str, snippet: str) -> tuple[float, str]:
|
|
295
|
+
"""score_freshness 的缓存版本:同一 (url, title, snippet) 组合在 rerank、
|
|
296
|
+
selection 及 evidence 打分间只算一次,避免重复的时效性评分
|
|
297
|
+
(实测热路径节省一次 full-text 年份提取)。返回 (score, reason)。
|
|
298
|
+
|
|
299
|
+
注:原 `query_time` 参数在 score_freshness 内从未参与计算,
|
|
300
|
+
此处不入缓存键以保持检索一致性。
|
|
301
|
+
"""
|
|
162
302
|
combined = f"{title} {snippet}"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
#
|
|
166
|
-
|
|
167
|
-
if
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
reason = f"{year}年(未来年份)"
|
|
174
|
-
elif age_years == 0:
|
|
175
|
-
score = 0.8
|
|
176
|
-
reason = f"{year}年(今年)"
|
|
177
|
-
elif age_years == 1:
|
|
178
|
-
score = 0.9
|
|
179
|
-
reason = f"{year}年(去年)"
|
|
180
|
-
elif age_years <= 2:
|
|
181
|
-
score = 0.7
|
|
182
|
-
reason = f"{year}年({age_years}年前)"
|
|
183
|
-
elif age_years <= 5:
|
|
184
|
-
score = 0.5
|
|
185
|
-
reason = f"{year}年({age_years}年前)"
|
|
186
|
-
else:
|
|
187
|
-
score = 0.3
|
|
188
|
-
reason = f"{year}年({age_years}年前,较旧)"
|
|
303
|
+
now_y = datetime.now().year
|
|
304
|
+
|
|
305
|
+
# 1) 完整日期
|
|
306
|
+
full = list(_FULL_DATE_RE.finditer(combined))
|
|
307
|
+
if full:
|
|
308
|
+
# 取最「新」的完整日期
|
|
309
|
+
best = max(full, key=lambda m: (int(m.group(1)), int(m.group(2)), int(m.group(3))))
|
|
310
|
+
year = int(best.group(1))
|
|
311
|
+
age_years = now_y - year
|
|
312
|
+
score, reason = _age_to_score(age_years, year, precise=True)
|
|
189
313
|
else:
|
|
190
|
-
|
|
191
|
-
url_year =
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
else
|
|
202
|
-
|
|
203
|
-
|
|
314
|
+
years = _years_from_text(combined)
|
|
315
|
+
url_year = None
|
|
316
|
+
m = re.search(r"/(20\d{2})(?:/|-)", url)
|
|
317
|
+
if m:
|
|
318
|
+
url_year = int(m.group(1))
|
|
319
|
+
years.append(url_year)
|
|
320
|
+
|
|
321
|
+
if years:
|
|
322
|
+
# 优先靠近今年的年份(发布语境),而非文中最早的历史年
|
|
323
|
+
# 若存在今年或去年,取 max;否则取 max 全体
|
|
324
|
+
recent = [y for y in years if y >= now_y - 1]
|
|
325
|
+
year = max(recent) if recent else max(years)
|
|
326
|
+
age_years = now_y - year
|
|
327
|
+
score, reason = _age_to_score(age_years, year, precise=False)
|
|
328
|
+
if url_year and year == url_year:
|
|
329
|
+
reason += "(URL年)"
|
|
204
330
|
else:
|
|
205
331
|
score = 0.5
|
|
206
332
|
reason = "无明确时间标记"
|
|
207
333
|
|
|
208
|
-
|
|
209
|
-
if re.search(r"(最新|latest|recent|breaking|just|刚刚|今日|today)", combined, re.I):
|
|
334
|
+
if re.search(r"(最新|latest|recent|breaking|just|刚刚|今日|today|本周|本月)", combined, re.I):
|
|
210
335
|
score = min(score + 0.1, 1.0)
|
|
211
336
|
reason += "(含时效关键词)"
|
|
212
337
|
|
|
213
|
-
return
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
338
|
+
return score, reason
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def score_freshness(result: dict[str, Any], query_time: float = None) -> dict[str, Any]:
|
|
342
|
+
"""评估结果的时效性(优先完整日期 / URL 年 / 最新合理年)。
|
|
343
|
+
|
|
344
|
+
结果按 (url, title, snippet) 做进程内 lru_cache:同一 URL 在 rerank、
|
|
345
|
+
selection 与 evidence 打分间只算一次,避免 search.py 的
|
|
346
|
+
local_five_dim_rerank 与 _attach_selection_signals 对同一结果重复评分。
|
|
347
|
+
`query_time` 保留以保持调用契约(当前评分逻辑不依赖它)。
|
|
348
|
+
"""
|
|
349
|
+
score, reason = _score_freshness_cached(
|
|
350
|
+
result.get("url", "") or "",
|
|
351
|
+
result.get("title", "") or "",
|
|
352
|
+
result.get("snippet", "") or "",
|
|
353
|
+
)
|
|
354
|
+
return {"score": round(score, 2), "reason": reason}
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def _age_to_score(age_years: int, year: int, precise: bool) -> tuple[float, str]:
|
|
358
|
+
if age_years < 0:
|
|
359
|
+
return 0.4, f"{year}年(未来年份)"
|
|
360
|
+
if age_years == 0:
|
|
361
|
+
return (0.9 if precise else 0.85), f"{year}年(今年)"
|
|
362
|
+
if age_years == 1:
|
|
363
|
+
return 0.8, f"{year}年(去年)"
|
|
364
|
+
if age_years <= 2:
|
|
365
|
+
return 0.65, f"{year}年({age_years}年前)"
|
|
366
|
+
if age_years <= 5:
|
|
367
|
+
return 0.45, f"{year}年({age_years}年前)"
|
|
368
|
+
return 0.25, f"{year}年({age_years}年前,较旧)"
|
|
217
369
|
|
|
218
370
|
|
|
219
371
|
# ── 交叉验证 ──────────────────────────────────────────────────────────────────
|
|
220
372
|
|
|
373
|
+
# 单条结果至少覆盖 20% 的 query 单元才算「与查询相关」。与 serp_guard 的
|
|
374
|
+
# SERP 覆盖率门同量级,但两处判据独立:这里回答「这批结果里几条在答这题」,
|
|
375
|
+
# serp_guard 回答「这一页 SERP 是不是整体答非所问」。
|
|
376
|
+
_RELEVANCE_MIN_COVERAGE = 0.2
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _relevance_units(text: str) -> set[str]:
|
|
380
|
+
"""查询/结果 → 相关性比对单元(CJK 二元组 + 小写拉丁数字词)。
|
|
381
|
+
|
|
382
|
+
复用 rank_signals.relevance_units,避免再维护第三套分词口径。
|
|
383
|
+
import 放在函数内:evidence 不是搜索热路径,但 rank_signals 较重,
|
|
384
|
+
模块级导入会让所有 evidence 调用方一起付启动成本。
|
|
385
|
+
"""
|
|
386
|
+
try:
|
|
387
|
+
from rank_signals import relevance_units
|
|
388
|
+
return set(relevance_units(text or ""))
|
|
389
|
+
except ImportError:
|
|
390
|
+
return {(text or "").lower()}
|
|
391
|
+
|
|
392
|
+
|
|
221
393
|
def cross_validate(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
|
|
222
394
|
"""多来源交叉验证。"""
|
|
223
395
|
if len(results) < 2:
|
|
@@ -227,106 +399,368 @@ def cross_validate(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
|
|
|
227
399
|
"detail": f"仅 {len(results)} 个结果,无法交叉验证",
|
|
228
400
|
"agreement_count": 0,
|
|
229
401
|
"total_sources": len(results),
|
|
402
|
+
"unique_domains": 0,
|
|
403
|
+
"url_overlap": 0.0,
|
|
404
|
+
"content_domains": 0,
|
|
230
405
|
}
|
|
231
406
|
|
|
232
|
-
# 检查 URL 重叠(不同来源指向同一页面 = 强佐证)
|
|
233
407
|
urls = [r.get("url", "") for r in results if r.get("url")]
|
|
234
408
|
unique_urls = set(urls)
|
|
235
409
|
url_overlap = 1 - (len(unique_urls) / max(len(urls), 1))
|
|
236
410
|
|
|
237
|
-
# 检查域名多样性
|
|
238
411
|
domains = set()
|
|
412
|
+
content_domains = set()
|
|
239
413
|
for r in results:
|
|
240
414
|
url = r.get("url", "")
|
|
241
|
-
if url:
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
415
|
+
if not url:
|
|
416
|
+
continue
|
|
417
|
+
domain = _normalize_domain(url)
|
|
418
|
+
domains.add(domain)
|
|
419
|
+
if not is_serp_or_jump_url(url):
|
|
420
|
+
content_domains.add(domain)
|
|
421
|
+
|
|
422
|
+
# 旧口径 `any(w in text for w in query.lower().split())` 在中文查询上
|
|
423
|
+
# 两个方向都错:无空格中文被当成一个整词,结果里永远不会出现整句,
|
|
424
|
+
# 真实相关也报 0/N;反过来,结果只要带「2026」这类年份就被算作相关,
|
|
425
|
+
# 年份表垃圾页也能过。改用 CJK 二元组覆盖率(issue #18 的「0/N」信号
|
|
426
|
+
# 只有在这个口径下才可信)。
|
|
427
|
+
q_units = _relevance_units(query)
|
|
247
428
|
content_matches = 0
|
|
248
429
|
for r in results:
|
|
249
|
-
text = f"{r.get('title', '')} {r.get('snippet', '')}"
|
|
250
|
-
if
|
|
430
|
+
text = f"{r.get('title', '')} {r.get('snippet', '')}"
|
|
431
|
+
if not q_units:
|
|
432
|
+
continue
|
|
433
|
+
hit = q_units & _relevance_units(text)
|
|
434
|
+
if len(hit) / len(q_units) >= _RELEVANCE_MIN_COVERAGE:
|
|
251
435
|
content_matches += 1
|
|
252
436
|
|
|
253
437
|
match_ratio = content_matches / max(len(results), 1)
|
|
438
|
+
n_content = len(content_domains)
|
|
254
439
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
score = 0.
|
|
259
|
-
elif match_ratio >= 0.6 and len(domains) >= 2:
|
|
260
|
-
level = "moderate"
|
|
261
|
-
score = 0.7
|
|
440
|
+
if match_ratio >= 0.8 and n_content >= 3:
|
|
441
|
+
level, score = "strong", 0.9
|
|
442
|
+
elif match_ratio >= 0.6 and n_content >= 2:
|
|
443
|
+
level, score = "moderate", 0.7
|
|
262
444
|
elif match_ratio >= 0.4:
|
|
263
|
-
level = "weak"
|
|
264
|
-
score = 0.5
|
|
445
|
+
level, score = "weak", 0.5
|
|
265
446
|
else:
|
|
266
|
-
level = "minimal"
|
|
267
|
-
score = 0.3
|
|
447
|
+
level, score = "minimal", 0.3
|
|
268
448
|
|
|
269
449
|
return {
|
|
270
450
|
"corroboration_level": level,
|
|
271
451
|
"score": round(score, 2),
|
|
272
|
-
"detail":
|
|
452
|
+
"detail": (
|
|
453
|
+
f"{content_matches}/{len(results)} 与查询相关,"
|
|
454
|
+
f"{n_content} 个可吸收域名(总域名 {len(domains)})"
|
|
455
|
+
),
|
|
273
456
|
"agreement_count": content_matches,
|
|
274
457
|
"total_sources": len(results),
|
|
275
458
|
"unique_domains": len(domains),
|
|
459
|
+
"content_domains": n_content,
|
|
276
460
|
"url_overlap": round(url_overlap, 2),
|
|
277
461
|
}
|
|
278
462
|
|
|
279
463
|
|
|
464
|
+
# ── 可溯源性评分 ──────────────────────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
# 一手信源平台特征(用户直接发布的原始内容)
|
|
467
|
+
_PRIMARY_PLATFORM_PATTERNS = {
|
|
468
|
+
"weibo.com": 0.95, # 微博原帖
|
|
469
|
+
"zhihu.com": 0.90, # 知乎回答/文章
|
|
470
|
+
"github.com": 0.90, # GitHub issue/PR/commit
|
|
471
|
+
"stackoverflow.com": 0.90,
|
|
472
|
+
"xiaohongshu.com": 0.85, # 小红书笔记
|
|
473
|
+
"bilibili.com": 0.85, # B站视频/动态
|
|
474
|
+
"twitter.com": 0.85, # X/Twitter 原帖
|
|
475
|
+
"x.com": 0.85,
|
|
476
|
+
"youtube.com": 0.85, # YouTube 视频
|
|
477
|
+
"reddit.com": 0.80, # Reddit 原帖
|
|
478
|
+
"medium.com": 0.80, # Medium 原文
|
|
479
|
+
"dev.to": 0.80,
|
|
480
|
+
"juejin.cn": 0.75, # 掘金原文
|
|
481
|
+
"cnblogs.com": 0.75, # 博客园原文
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
# 原始媒体报道(新闻机构原创报道)
|
|
485
|
+
_ORIGINAL_MEDIA_PATTERNS = {
|
|
486
|
+
"xinhuanet.com": 0.95, # 新华社
|
|
487
|
+
"people.com.cn": 0.95, # 人民网
|
|
488
|
+
"reuters.com": 0.95, # 路透社
|
|
489
|
+
"bloomberg.com": 0.95, # 彭博社
|
|
490
|
+
"wsj.com": 0.95, # 华尔街日报
|
|
491
|
+
"caixin.com": 0.90, # 财新
|
|
492
|
+
"yicai.com": 0.88, # 第一财经
|
|
493
|
+
"wallstreetcn.com": 0.88,# 华尔街见闻
|
|
494
|
+
"cls.cn": 0.88, # 财联社
|
|
495
|
+
"cs.com.cn": 0.90, # 中证网
|
|
496
|
+
"stcn.com": 0.85, # 证券时报
|
|
497
|
+
"cnr.cn": 0.88, # 央广网
|
|
498
|
+
"thepaper.cn": 0.82, # 澎湃新闻
|
|
499
|
+
"jiemian.com": 0.80, # 界面新闻
|
|
500
|
+
"36kr.com": 0.75, # 36氪
|
|
501
|
+
"eastmoney.com": 0.80, # 东方财富
|
|
502
|
+
"163.com": 0.70, # 网易新闻
|
|
503
|
+
"sohu.com": 0.70, # 搜狐新闻
|
|
504
|
+
"sina.com.cn": 0.70, # 新浪新闻
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
# 二手/聚合/洗稿平台特征
|
|
508
|
+
_SECONDARY_PLATFORM_PATTERNS = {
|
|
509
|
+
"baijiahao.baidu.com": 0.35, # 百家号(自媒体洗稿重灾区)
|
|
510
|
+
"toutiao.com": 0.35, # 头条号(聚合+洗稿)
|
|
511
|
+
"weixin.qq.com": 0.40, # 微信公众号(质量参差不齐)
|
|
512
|
+
"guba.eastmoney.com": 0.30, # 股吧(UGC 讨论)
|
|
513
|
+
"zhuanlan.zhihu.com": 0.55, # 知乎专栏(部分原创部分转载)
|
|
514
|
+
"k.sina.com.cn": 0.40, # 新浪博客/聚合
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
# 转载/引用标记(出现在标题或 snippet 中)
|
|
518
|
+
_REPOST_MARKERS = [
|
|
519
|
+
"转载", "转自", "摘自", "编译自", "译自", "综合自",
|
|
520
|
+
"来源:", "来源:", "原文:", "原文:", "援引", "据援引",
|
|
521
|
+
"reposted", "via@", "via @", "credit:", "courtesy",
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def score_traceability(result: dict[str, Any]) -> dict[str, Any]:
|
|
526
|
+
"""评估搜索结果的可溯源性(Traceability)。
|
|
527
|
+
|
|
528
|
+
借鉴 doubao-sentiment-tracker 的溯源四场景方法论,将「能否追溯到原始
|
|
529
|
+
来源」量化为 0-1 评分。与 Selection(域名权威)正交:一个结果可以
|
|
530
|
+
权威但不可溯源(如聚合媒体的转载稿),也可以不权威但可溯源(如个人
|
|
531
|
+
微博原帖)。
|
|
532
|
+
|
|
533
|
+
评分规则:
|
|
534
|
+
- 一手信源(用户直接发布的原始帖子):0.80-0.95
|
|
535
|
+
- 原始媒体报道(新闻机构原创报道):0.70-0.95
|
|
536
|
+
- 综合报道/评论(引用了原始来源):0.50-0.65
|
|
537
|
+
- 二手转述/洗稿:0.30-0.45
|
|
538
|
+
- 无法判断:0.50(中性)
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
{"score": float, "reason": str, "is_primary": bool, "platform_type": str}
|
|
542
|
+
"""
|
|
543
|
+
url = result.get("url", "") or ""
|
|
544
|
+
title = result.get("title", "") or ""
|
|
545
|
+
snippet = result.get("snippet", "") or ""
|
|
546
|
+
source = result.get("source", "") or ""
|
|
547
|
+
content_type = result.get("content_type", "") or ""
|
|
548
|
+
social_meta = result.get("social_meta")
|
|
549
|
+
text = f"{title} {snippet}"
|
|
550
|
+
|
|
551
|
+
if not url:
|
|
552
|
+
return {
|
|
553
|
+
"score": 0.5,
|
|
554
|
+
"reason": "无 URL,无法判断可溯源性",
|
|
555
|
+
"is_primary": False,
|
|
556
|
+
"platform_type": "unknown",
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
domain = _normalize_domain(url)
|
|
560
|
+
|
|
561
|
+
# 1) 检查是否为 SERP/跳转 URL(不可溯源)
|
|
562
|
+
if is_serp_or_jump_url(url):
|
|
563
|
+
return {
|
|
564
|
+
"score": 0.10,
|
|
565
|
+
"reason": "搜索结果页/跳转链,非原始内容",
|
|
566
|
+
"is_primary": False,
|
|
567
|
+
"platform_type": "serp",
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
# 2) 检查是否为一手信源平台
|
|
571
|
+
best_primary_score = 0.0
|
|
572
|
+
best_primary_domain = ""
|
|
573
|
+
for pattern, score in _PRIMARY_PLATFORM_PATTERNS.items():
|
|
574
|
+
if domain == pattern or domain.endswith("." + pattern):
|
|
575
|
+
if score > best_primary_score:
|
|
576
|
+
best_primary_score = score
|
|
577
|
+
best_primary_domain = pattern
|
|
578
|
+
|
|
579
|
+
if best_primary_score > 0:
|
|
580
|
+
# 检查是否有转载标记(即使是原帖平台,也可能是转载)
|
|
581
|
+
has_repost = any(marker in text for marker in _REPOST_MARKERS)
|
|
582
|
+
if has_repost:
|
|
583
|
+
return {
|
|
584
|
+
"score": 0.45,
|
|
585
|
+
"reason": f"原帖平台({best_primary_domain})但含转载标记",
|
|
586
|
+
"is_primary": False,
|
|
587
|
+
"platform_type": "repost_on_primary",
|
|
588
|
+
}
|
|
589
|
+
return {
|
|
590
|
+
"score": best_primary_score,
|
|
591
|
+
"reason": f"一手信源({best_primary_domain})",
|
|
592
|
+
"is_primary": True,
|
|
593
|
+
"platform_type": "primary",
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
# 3) 检查是否为原始媒体报道
|
|
597
|
+
best_media_score = 0.0
|
|
598
|
+
best_media_domain = ""
|
|
599
|
+
for pattern, score in _ORIGINAL_MEDIA_PATTERNS.items():
|
|
600
|
+
if domain == pattern or domain.endswith("." + pattern):
|
|
601
|
+
if score > best_media_score:
|
|
602
|
+
best_media_score = score
|
|
603
|
+
best_media_domain = pattern
|
|
604
|
+
|
|
605
|
+
if best_media_score > 0:
|
|
606
|
+
has_repost = any(marker in text for marker in _REPOST_MARKERS)
|
|
607
|
+
if has_repost:
|
|
608
|
+
return {
|
|
609
|
+
"score": 0.55,
|
|
610
|
+
"reason": f"媒体平台({best_media_domain})但含转载标记",
|
|
611
|
+
"is_primary": False,
|
|
612
|
+
"platform_type": "media_repost",
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
"score": best_media_score,
|
|
616
|
+
"reason": f"原始媒体报道({best_media_domain})",
|
|
617
|
+
"is_primary": True,
|
|
618
|
+
"platform_type": "official_media",
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
# 4) 检查是否为二手/聚合平台
|
|
622
|
+
for pattern, score in _SECONDARY_PLATFORM_PATTERNS.items():
|
|
623
|
+
if domain == pattern or domain.endswith("." + pattern):
|
|
624
|
+
return {
|
|
625
|
+
"score": score,
|
|
626
|
+
"reason": f"二手/聚合平台({pattern})",
|
|
627
|
+
"is_primary": False,
|
|
628
|
+
"platform_type": "aggregator",
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
# 5) 检查 content_type 和 social_meta
|
|
632
|
+
if content_type == "social_post" or social_meta:
|
|
633
|
+
return {
|
|
634
|
+
"score": 0.75,
|
|
635
|
+
"reason": "社交帖子(content_type=social_post)",
|
|
636
|
+
"is_primary": True,
|
|
637
|
+
"platform_type": "social_post",
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
# 6) 检查转载标记
|
|
641
|
+
has_repost = any(marker in text for marker in _REPOST_MARKERS)
|
|
642
|
+
if has_repost:
|
|
643
|
+
return {
|
|
644
|
+
"score": 0.40,
|
|
645
|
+
"reason": "含转载/引用标记",
|
|
646
|
+
"is_primary": False,
|
|
647
|
+
"platform_type": "repost",
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
# 7) 无法判断(中性)
|
|
651
|
+
return {
|
|
652
|
+
"score": 0.50,
|
|
653
|
+
"reason": "无法判断可溯源性(默认中性)",
|
|
654
|
+
"is_primary": False,
|
|
655
|
+
"platform_type": "unknown",
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
|
|
280
659
|
# ── 综合可信度 ────────────────────────────────────────────────────────────────
|
|
281
660
|
|
|
282
|
-
def compute_credibility(
|
|
283
|
-
|
|
661
|
+
def compute_credibility(
|
|
662
|
+
results: list[dict[str, Any]],
|
|
663
|
+
query: str,
|
|
664
|
+
*,
|
|
665
|
+
high_stakes: bool = False,
|
|
666
|
+
) -> dict[str, Any]:
|
|
667
|
+
"""两阶段综合可信度:Selection × Absorption + Freshness。"""
|
|
284
668
|
query_time = time.time()
|
|
285
|
-
scored_results = []
|
|
669
|
+
scored_results: list[dict[str, Any]] = []
|
|
286
670
|
|
|
287
671
|
for r in results:
|
|
288
672
|
url = r.get("url", "")
|
|
289
673
|
source = r.get("source", "")
|
|
674
|
+
title = r.get("title", "") or ""
|
|
675
|
+
snippet = r.get("snippet", "") or ""
|
|
290
676
|
|
|
291
677
|
auth = score_authority(url, source)
|
|
292
678
|
fresh = score_freshness(r, query_time)
|
|
679
|
+
density = score_evidence_density(snippet, title)
|
|
293
680
|
|
|
294
|
-
#
|
|
295
|
-
|
|
681
|
+
# Selection:权威为主;SERP 直接压到低分
|
|
682
|
+
selection = auth["score"]
|
|
683
|
+
if auth.get("is_serp"):
|
|
684
|
+
selection = min(selection, 0.15)
|
|
685
|
+
|
|
686
|
+
# Absorption:证据密度
|
|
687
|
+
absorption = density["absorption_score"]
|
|
688
|
+
|
|
689
|
+
original_score = r.get("score")
|
|
690
|
+
if original_score is None:
|
|
691
|
+
original_score = 0.5
|
|
692
|
+
else:
|
|
693
|
+
try:
|
|
694
|
+
original_score = float(original_score)
|
|
695
|
+
except (TypeError, ValueError):
|
|
696
|
+
original_score = 0.5
|
|
697
|
+
# Traceability:可溯源性
|
|
698
|
+
trace = score_traceability(r)
|
|
699
|
+
|
|
700
|
+
# MECE 加权(量化)— 五维度
|
|
296
701
|
credibility = (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
702
|
+
selection * 0.35
|
|
703
|
+
+ absorption * 0.30
|
|
704
|
+
+ fresh["score"] * 0.15
|
|
705
|
+
+ float(original_score) * 0.10
|
|
706
|
+
+ trace["score"] * 0.10
|
|
300
707
|
)
|
|
301
708
|
|
|
302
709
|
scored_results.append({
|
|
303
|
-
"title":
|
|
710
|
+
"title": title,
|
|
304
711
|
"url": url,
|
|
305
712
|
"source": source,
|
|
306
|
-
"snippet":
|
|
713
|
+
"snippet": snippet[:180],
|
|
307
714
|
"credibility": {
|
|
308
715
|
"final": round(credibility, 3),
|
|
716
|
+
"selection": round(selection, 3),
|
|
717
|
+
"absorption": round(absorption, 3),
|
|
309
718
|
"authority": auth,
|
|
310
719
|
"freshness": fresh,
|
|
720
|
+
"evidence_density": density,
|
|
311
721
|
"original_score": original_score,
|
|
722
|
+
"traceability": trace,
|
|
312
723
|
},
|
|
313
724
|
})
|
|
314
725
|
|
|
315
|
-
# 交叉验证
|
|
316
726
|
cross = cross_validate(results, query)
|
|
317
727
|
|
|
318
|
-
|
|
728
|
+
if high_stakes and scored_results:
|
|
729
|
+
# 共识调节:强佐证略抬高 top 可吸收源;弱共识不抬
|
|
730
|
+
boost = 0.0
|
|
731
|
+
if cross["corroboration_level"] == "strong":
|
|
732
|
+
boost = 0.05
|
|
733
|
+
elif cross["corroboration_level"] == "moderate":
|
|
734
|
+
boost = 0.02
|
|
735
|
+
for item in scored_results:
|
|
736
|
+
if not item["credibility"]["authority"].get("is_serp"):
|
|
737
|
+
item["credibility"]["final"] = round(
|
|
738
|
+
min(item["credibility"]["final"] + boost, 1.0), 3
|
|
739
|
+
)
|
|
740
|
+
item["credibility"]["consensus_boost"] = boost
|
|
741
|
+
|
|
319
742
|
scored_results.sort(key=lambda x: x["credibility"]["final"], reverse=True)
|
|
320
743
|
|
|
321
744
|
return {
|
|
322
745
|
"query": query,
|
|
746
|
+
"framework": "selection_x_absorption_v2.3_traceability",
|
|
323
747
|
"results": scored_results,
|
|
324
748
|
"cross_validation": cross,
|
|
325
749
|
"summary": {
|
|
326
750
|
"total": len(scored_results),
|
|
327
|
-
"high_credibility": sum(
|
|
328
|
-
|
|
329
|
-
|
|
751
|
+
"high_credibility": sum(
|
|
752
|
+
1 for r in scored_results if r["credibility"]["final"] >= 0.7
|
|
753
|
+
),
|
|
754
|
+
"medium_credibility": sum(
|
|
755
|
+
1 for r in scored_results if 0.5 <= r["credibility"]["final"] < 0.7
|
|
756
|
+
),
|
|
757
|
+
"low_credibility": sum(
|
|
758
|
+
1 for r in scored_results if r["credibility"]["final"] < 0.5
|
|
759
|
+
),
|
|
760
|
+
"serp_filtered": sum(
|
|
761
|
+
1 for r in scored_results
|
|
762
|
+
if r["credibility"]["authority"].get("is_serp")
|
|
763
|
+
),
|
|
330
764
|
"best_source": scored_results[0] if scored_results else None,
|
|
331
765
|
},
|
|
332
766
|
}
|
|
@@ -335,36 +769,53 @@ def compute_credibility(results: list[dict[str, Any]], query: str) -> dict[str,
|
|
|
335
769
|
# ── CLI ───────────────────────────────────────────────────────────────────────
|
|
336
770
|
|
|
337
771
|
def main():
|
|
338
|
-
|
|
772
|
+
ensure_utf8_stdio() # 入口第一件事:钉住 stdout/stderr 编码(见 cli_io)
|
|
773
|
+
parser = argparse.ArgumentParser(description="来源可信度评估工具(Selection×Absorption)")
|
|
339
774
|
parser.add_argument("query", nargs="?", default="", help="搜索查询词")
|
|
340
775
|
parser.add_argument("--stdin", action="store_true", help="从 stdin 读取 JSON 搜索结果")
|
|
341
776
|
parser.add_argument("--json", action="store_true", help="JSON 输出")
|
|
777
|
+
parser.add_argument(
|
|
778
|
+
"--high-stakes",
|
|
779
|
+
action="store_true",
|
|
780
|
+
help="高后果模式:启用共识微调",
|
|
781
|
+
)
|
|
342
782
|
args = parser.parse_args()
|
|
343
783
|
|
|
344
784
|
if args.stdin:
|
|
345
785
|
data = json.load(sys.stdin)
|
|
346
786
|
results = data.get("results", [])
|
|
347
787
|
else:
|
|
348
|
-
# 从搜索结果文件读取
|
|
349
788
|
results = []
|
|
350
789
|
|
|
351
790
|
if not results and args.query:
|
|
352
|
-
# 无输入结果时,输出提示
|
|
353
791
|
print("需要提供搜索结果进行评估。用法:")
|
|
354
792
|
print(' echo \'{"results": [...]}\' | python3 evidence.py --stdin --query "查询词"')
|
|
355
793
|
sys.exit(1)
|
|
356
794
|
|
|
357
|
-
report = compute_credibility(
|
|
795
|
+
report = compute_credibility(
|
|
796
|
+
results, args.query, high_stakes=args.high_stakes
|
|
797
|
+
)
|
|
358
798
|
|
|
359
799
|
if args.json:
|
|
360
|
-
print(
|
|
800
|
+
print(dumps(report))
|
|
361
801
|
else:
|
|
362
|
-
print(f"\n来源可信度评估:{report['query']}")
|
|
802
|
+
print(f"\n来源可信度评估:{report['query']} [{report['framework']}]")
|
|
363
803
|
print(f"{'='*50}")
|
|
364
804
|
for r in report["results"]:
|
|
365
805
|
c = r["credibility"]
|
|
366
806
|
level = "🟢" if c["final"] >= 0.7 else "🟡" if c["final"] >= 0.5 else "🔴"
|
|
367
|
-
|
|
807
|
+
ed = c.get("evidence_density") or {}
|
|
808
|
+
flags = []
|
|
809
|
+
if ed.get("has_numbers"):
|
|
810
|
+
flags.append("数")
|
|
811
|
+
if ed.get("has_comparison"):
|
|
812
|
+
flags.append("比")
|
|
813
|
+
if ed.get("has_definition"):
|
|
814
|
+
flags.append("义")
|
|
815
|
+
if c["authority"].get("is_serp"):
|
|
816
|
+
flags.append("SERP")
|
|
817
|
+
flag_s = ",".join(flags) if flags else "-"
|
|
818
|
+
print(f"{level} [{c['final']:.2f}] sel={c['selection']:.2f} abs={c['absorption']:.2f} [{flag_s}] {r['title'][:48]}")
|
|
368
819
|
print(f" 权威:{c['authority']['score']:.2f} ({c['authority']['reason']})")
|
|
369
820
|
print(f" 时效:{c['freshness']['score']:.2f} ({c['freshness']['reason']})")
|
|
370
821
|
print()
|
|
@@ -374,7 +825,10 @@ def main():
|
|
|
374
825
|
print(f" {cv['detail']}")
|
|
375
826
|
|
|
376
827
|
s = report["summary"]
|
|
377
|
-
print(
|
|
828
|
+
print(
|
|
829
|
+
f"\n总结:🟢高={s['high_credibility']} 🟡中={s['medium_credibility']} "
|
|
830
|
+
f"🔴低={s['low_credibility']} SERP={s.get('serp_filtered', 0)}"
|
|
831
|
+
)
|
|
378
832
|
|
|
379
833
|
|
|
380
834
|
if __name__ == "__main__":
|