argo-search 1.0.1 → 2.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -21
- package/README.en.md +822 -0
- package/README.es.md +864 -0
- package/README.ja.md +864 -0
- package/README.ko.md +864 -0
- package/README.md +867 -320
- package/SKILL.md +135 -0
- package/assets/readme/hero.svg +121 -0
- package/assets/readme/made-with-beautify.svg +10 -0
- package/assets/readme/proof-routes.svg +73 -0
- package/assets/readme/settings-keys.png +0 -0
- package/assets/readme/settings-tools.png +0 -0
- package/assets/readme/why-better.svg +104 -0
- package/assets/readme/workflow.svg +83 -0
- package/backends/domain_profiles.json +2308 -96
- package/backends/engine_registry.yaml +3314 -503
- package/backends/query_synonyms_cn.json +35 -0
- package/backends/quota_profiles.json +2409 -161
- package/backends/source_types_cn.json +83 -0
- package/backends/text_lexicon.json +44 -0
- package/bin/argo +703 -0
- package/bin/argo.js +71 -39
- package/config.yaml +4976 -363
- package/cordis.patch.yml +21 -0
- package/data/countries.json +40 -0
- package/data/lang_matrix/lang_capability.json +842 -0
- package/data/lang_matrix/matrix_2026-09-10.json +11921 -0
- package/data/regions_cn.json +1 -0
- package/docs/ADDING_NEW_ENGINE.md +184 -0
- package/docs/ARGO_INTRO.md +124 -0
- package/docs/DATA_SOURCE_GAPS.md +81 -0
- package/docs/DESIGN_LOCAL_DATA_FUSION.md +90 -0
- package/docs/ENGINE_CATALOG.md +564 -0
- package/docs/MCP_SETUP.md +106 -0
- package/docs/PLATFORM_PATHS.md +111 -0
- package/docs/RELEASE_NOTES_v2.8.0.md +71 -0
- package/docs/RELEASE_NOTES_v2.8.2.md +63 -0
- package/docs/RELEASE_NOTES_v2.8.3.md +55 -0
- package/docs/RELEASE_NOTES_v2.8.4.md +79 -0
- package/docs/RELEASE_NOTES_v2.8.5.md +102 -0
- package/docs/RELEASE_NOTES_v2.8.6.md +66 -0
- package/docs/RELEASE_NOTES_v2.8.7.md +65 -0
- package/docs/RELEASE_NOTES_v2.8.8.md +67 -0
- package/docs/RELEASE_NOTES_v2.8.9.md +67 -0
- package/docs/RELEASE_NOTES_v2.9.0.md +64 -0
- package/docs/SEARCH_ARCHIVE.md +115 -0
- package/docs/assets/architecture.svg +104 -0
- package/docs/assets/hero.svg +98 -0
- package/docs/engines/bangumi.md +27 -0
- package/docs/engines/biorxiv.md +27 -0
- package/docs/engines/clawhub.md +27 -0
- package/docs/engines/crt_sh.md +27 -0
- package/docs/engines/datacite.md +27 -0
- package/docs/engines/deps_dev.md +27 -0
- package/docs/engines/douban_movie.md +27 -0
- package/docs/engines/endoflife.md +27 -0
- package/docs/engines/flk_law.md +27 -0
- package/docs/engines/fxtwitter.md +27 -0
- package/docs/engines/google_news.md +27 -0
- package/docs/engines/gov_regulations.md +27 -0
- package/docs/engines/guqian.md +27 -0
- package/docs/engines/hackernews.md +27 -0
- package/docs/engines/iplant.md +27 -0
- package/docs/engines/keenable.md +27 -0
- package/docs/engines/met_museum.md +27 -0
- package/docs/engines/nasa_images.md +27 -0
- package/docs/engines/netease_music.md +27 -0
- package/docs/engines/npm.md +27 -0
- package/docs/engines/nvd.md +27 -0
- package/docs/engines/openreview.md +27 -0
- package/docs/engines/openstd.md +27 -0
- package/docs/engines/parallel_free.md +27 -0
- package/docs/engines/people_daily.md +27 -0
- package/docs/engines/realtime_index.md +27 -0
- package/docs/engines/sec_edgar.md +27 -0
- package/docs/engines/seltz.md +27 -0
- package/docs/engines/skillsmp.md +27 -0
- package/docs/engines/sspai.md +27 -0
- package/docs/engines/std_samr.md +27 -0
- package/docs/engines/train.md +27 -0
- package/docs/engines/un_comtrade.md +27 -0
- package/docs/engines/wikisource.md +27 -0
- package/docs/engines/zdic.md +27 -0
- package/docs/engines/zenodo.md +27 -0
- package/docs/engines/zhihu_hot_app.md +27 -0
- package/docs//344/270/272/344/273/200/344/271/210/351/200/211/346/213/251argo.md +176 -0
- package/engines/README.md +10 -0
- package/engines/_template_cli.yaml +39 -0
- package/engines/_template_http.yaml +37 -0
- package/engines/plugins/_template_plugin.py +50 -0
- package/engines/specs/_cisa_kev.yaml +54 -0
- package/engines/specs/_osv.yaml +63 -0
- package/engines/specs/aljazeera.yaml +43 -0
- package/engines/specs/antara.yaml +43 -0
- package/engines/specs/artic.yaml +20 -0
- package/engines/specs/aviation_weather.yaml +39 -0
- package/engines/specs/bangumi.yaml +21 -0
- package/engines/specs/biorxiv.yaml +24 -0
- package/engines/specs/carbon_intensity.yaml +32 -0
- package/engines/specs/cinii.yaml +44 -0
- package/engines/specs/clawhub.yaml +49 -0
- package/engines/specs/cleveland.yaml +19 -0
- package/engines/specs/cn_ai_news.yaml +43 -0
- package/engines/specs/core.yaml +35 -0
- package/engines/specs/crt_sh.yaml +38 -0
- package/engines/specs/ct24.yaml +43 -0
- package/engines/specs/datacite.yaml +27 -0
- package/engines/specs/datagov_uk.yaml +41 -0
- package/engines/specs/ddgs_images.yaml +45 -0
- package/engines/specs/deepseek_search.yaml +74 -0
- package/engines/specs/deezer.yaml +20 -0
- package/engines/specs/deps_dev.yaml +21 -0
- package/engines/specs/digup.yaml +36 -0
- package/engines/specs/doi.yaml +39 -0
- package/engines/specs/douban_movie.yaml +22 -0
- package/engines/specs/dw_news.yaml +47 -0
- package/engines/specs/egov_law.yaml +21 -0
- package/engines/specs/elpais.yaml +43 -0
- package/engines/specs/endoflife.yaml +21 -0
- package/engines/specs/energy_charts.yaml +21 -0
- package/engines/specs/factcheck_org.yaml +52 -0
- package/engines/specs/faz.yaml +43 -0
- package/engines/specs/federal_register.yaml +57 -0
- package/engines/specs/firecrawl.yaml +53 -0
- package/engines/specs/flk_law.yaml +24 -0
- package/engines/specs/folha.yaml +43 -0
- package/engines/specs/france24.yaml +44 -0
- package/engines/specs/full_fact.yaml +47 -0
- package/engines/specs/fxtwitter.yaml +31 -0
- package/engines/specs/gbfs_nyc.yaml +32 -0
- package/engines/specs/gdacs.yaml +21 -0
- package/engines/specs/gdelt.yaml +64 -0
- package/engines/specs/google_news.yaml +22 -0
- package/engines/specs/gov_regulations.yaml +38 -0
- package/engines/specs/guardian_rss.yaml +59 -0
- package/engines/specs/hurriyet.yaml +43 -0
- package/engines/specs/iplant.yaml +25 -0
- package/engines/specs/jikan.yaml +24 -0
- package/engines/specs/jolpica.yaml +20 -0
- package/engines/specs/k10plus.yaml +20 -0
- package/engines/specs/keenable.yaml +44 -0
- package/engines/specs/lefigaro.yaml +43 -0
- package/engines/specs/listenbrainz.yaml +19 -0
- package/engines/specs/matichon.yaml +43 -0
- package/engines/specs/met_museum.yaml +21 -0
- package/engines/specs/nasa_images.yaml +45 -0
- package/engines/specs/netease_music.yaml +38 -0
- package/engines/specs/nhk.yaml +43 -0
- package/engines/specs/nhtsa_vpic.yaml +20 -0
- package/engines/specs/noaa_swpc.yaml +20 -0
- package/engines/specs/nvd.yaml +38 -0
- package/engines/specs/obis.yaml +20 -0
- package/engines/specs/opencitations.yaml +52 -0
- package/engines/specs/openf1.yaml +21 -0
- package/engines/specs/openfoodfacts.yaml +45 -0
- package/engines/specs/openligadb.yaml +20 -0
- package/engines/specs/openreview.yaml +37 -0
- package/engines/specs/openstd.yaml +21 -0
- package/engines/specs/people_daily.yaml +22 -0
- package/engines/specs/realtime_index.yaml +34 -0
- package/engines/specs/ror.yaml +20 -0
- package/engines/specs/satnogs.yaml +20 -0
- package/engines/specs/sec_edgar.yaml +46 -0
- package/engines/specs/skillsmp.yaml +50 -0
- package/engines/specs/sogou_weixin_browser.yaml +30 -0
- package/engines/specs/soilgrids.yaml +25 -0
- package/engines/specs/sspai.yaml +36 -0
- package/engines/specs/stackexchange.yaml +46 -0
- package/engines/specs/std_samr.yaml +21 -0
- package/engines/specs/tass.yaml +43 -0
- package/engines/specs/tle_mirror.yaml +21 -0
- package/engines/specs/train.yaml +37 -0
- package/engines/specs/tvmaze.yaml +20 -0
- package/engines/specs/twitter_syndication.yaml +31 -0
- package/engines/specs/un_comtrade.yaml +23 -0
- package/engines/specs/unpaywall.yaml +54 -0
- package/engines/specs/vnpress.yaml +43 -0
- package/engines/specs/weather.yaml +32 -0
- package/engines/specs/who_don.yaml +21 -0
- package/engines/specs/who_gho.yaml +20 -0
- package/engines/specs/wikimedia_commons.yaml +34 -0
- package/engines/specs/wikisource.yaml +22 -0
- package/engines/specs/worms.yaml +19 -0
- package/engines/specs/x_trending.yaml +34 -0
- package/engines/specs/yna.yaml +43 -0
- package/engines/specs/ynet.yaml +43 -0
- package/engines/specs/zbmath.yaml +40 -0
- package/engines/specs/zdic.yaml +22 -0
- package/engines/specs/zenodo.yaml +27 -0
- package/engines/specs/zhihu_hot_app.yaml +35 -0
- package/mcp/clients.yaml +74 -0
- package/package.json +51 -12
- package/packages/dsh-plugin/README.md +134 -0
- package/packages/dsh-plugin/cordis.patch.yml +48 -0
- package/packages/dsh-plugin/dsh/client.js +1244 -0
- package/packages/dsh-plugin/dsh/index.js +2290 -0
- package/packages/dsh-plugin/dsh/native-tools.mjs +743 -0
- package/packages/dsh-plugin/package.json +38 -0
- package/packages/dsh-plugin/test/client.test.js +232 -0
- package/packages/dsh-plugin/test/config.test.js +30 -0
- package/packages/dsh-plugin/test/index.test.js +1338 -0
- package/references/query-templates.md +96 -0
- package/references/query-templates.yaml +118 -0
- package/scripts/ab_eval.py +219 -0
- package/scripts/ab_eval_p0p1.py +266 -0
- package/scripts/adaptive.py +271 -70
- package/scripts/answer.py +154 -0
- package/scripts/archive_run.py +591 -0
- package/scripts/argo_engine_registry.py +172 -0
- package/scripts/argo_paths.py +728 -0
- package/scripts/article.py +136 -0
- package/scripts/arxiv_source.py +201 -0
- package/scripts/badcase_ledger.py +252 -0
- package/scripts/batch_probe.py +241 -0
- package/scripts/benchmark.py +19 -0
- package/scripts/bounded_run.py +144 -0
- package/scripts/browser_auth.py +323 -0
- package/scripts/burstiness_detector.py +147 -0
- package/scripts/cache.py +1078 -67
- package/scripts/cache_guard.py +271 -0
- package/scripts/cache_key_vdom.py +56 -0
- package/scripts/cache_similarity.py +170 -0
- package/scripts/caliber_isolation.py +195 -0
- package/scripts/calibrate_lowquality.py +210 -0
- package/scripts/candidate_envelope.py +366 -0
- package/scripts/chrome_cdp.py +871 -0
- package/scripts/circuit_breaker.py +389 -0
- package/scripts/citations.py +334 -0
- package/scripts/cite_cli.py +59 -0
- package/scripts/cjk_tokens.py +95 -0
- package/scripts/clarify.py +47 -24
- package/scripts/cli_io.py +240 -0
- package/scripts/config.py +683 -45
- package/scripts/config_cache.py +402 -0
- package/scripts/conn_pool.py +220 -0
- package/scripts/content_security.py +685 -0
- package/scripts/content_signals.py +730 -0
- package/scripts/crawl.py +74 -35
- package/scripts/daemon.py +866 -0
- package/scripts/digup_bridge.py +214 -0
- package/scripts/engine_admission.py +366 -0
- package/scripts/engine_dispatch.py +1019 -0
- package/scripts/engine_env.py +534 -0
- package/scripts/engine_failure.py +411 -0
- package/scripts/engine_families.py +732 -0
- package/scripts/engine_policy.py +218 -0
- package/scripts/engine_requires.py +190 -0
- package/scripts/engine_status.py +322 -0
- package/scripts/engine_validate.py +638 -0
- package/scripts/engines.py +778 -294
- package/scripts/engines_base.py +1521 -0
- package/scripts/engines_builders.py +298 -0
- package/scripts/engines_builders_batch10.py +159 -0
- package/scripts/engines_builders_batch11.py +174 -0
- package/scripts/engines_builders_batch12.py +103 -0
- package/scripts/engines_builders_batch13.py +340 -0
- package/scripts/engines_builders_batch9.py +1468 -0
- package/scripts/engines_builders_cn.py +2283 -0
- package/scripts/engines_builders_data.py +2923 -0
- package/scripts/engines_builders_data_macro.py +775 -0
- package/scripts/engines_builders_feeds.py +255 -0
- package/scripts/engines_builders_intl.py +1239 -0
- package/scripts/engines_builders_search.py +440 -0
- package/scripts/engines_builders_tech.py +1360 -0
- package/scripts/evidence.py +588 -162
- package/scripts/evidence_loop.py +592 -0
- package/scripts/evidence_tier.py +298 -0
- package/scripts/except_sets.py +25 -0
- package/scripts/extract.py +55 -16
- package/scripts/fact_align.py +207 -0
- package/scripts/fact_crosscheck.py +230 -0
- package/scripts/fetch.py +25 -12
- package/scripts/fetch_browser.py +87 -0
- package/scripts/fetch_jsrun.py +122 -0
- package/scripts/fetch_quality.py +225 -0
- package/scripts/fetch_render_tinyfish.py +117 -0
- package/scripts/fetch_v3.py +1779 -0
- package/scripts/focus_extract.py +350 -0
- package/scripts/fulltext_store.py +169 -0
- package/scripts/fxtwitter_api.py +325 -0
- package/scripts/gen_engine_catalog.py +364 -0
- package/scripts/gen_native_tools.py +91 -0
- package/scripts/gen_routing_snapshot.py +184 -0
- package/scripts/health_check.py +140 -402
- package/scripts/health_probe.py +23 -8
- package/scripts/hot_state.py +134 -0
- package/scripts/http_client.py +1053 -0
- package/scripts/image_ops.py +430 -0
- package/scripts/image_vision/vision_probe +0 -0
- package/scripts/image_vision/vision_probe.swift +126 -0
- package/scripts/install.ps1 +148 -0
- package/scripts/install.sh +206 -0
- package/scripts/job.py +1186 -0
- package/scripts/jump_resolver.py +235 -0
- package/scripts/lang_capability.py +150 -0
- package/scripts/lang_detect.py +289 -0
- package/scripts/lang_matrix_probe.py +329 -0
- package/scripts/lang_pref.py +311 -0
- package/scripts/link_source.py +340 -0
- package/scripts/local_engines.py +321 -0
- package/scripts/local_image.py +970 -0
- package/scripts/local_seek.py +366 -0
- package/scripts/macro_countries.py +82 -0
- package/scripts/matrix_search_eval.py +1105 -0
- package/scripts/mcp_diag.py +8 -3
- package/scripts/mcp_handlers.py +1074 -0
- package/scripts/mcp_handlers_surface.py +157 -0
- package/scripts/mcp_launch.sh +122 -0
- package/scripts/mcp_server.py +118 -417
- package/scripts/mcp_setup.py +407 -0
- package/scripts/mcp_tools.py +330 -0
- package/scripts/mcp_transport.py +191 -0
- package/scripts/min_version_scan.py +335 -0
- package/scripts/net_proxy.py +258 -0
- package/scripts/network_aware.py +152 -0
- package/scripts/paper.py +274 -0
- package/scripts/pdf_extract.py +518 -0
- package/scripts/phrase_filter.py +66 -0
- package/scripts/plan.py +422 -0
- package/scripts/postprocess_signals.py +150 -0
- package/scripts/pre-commit +48 -0
- package/scripts/query_enhance.py +157 -0
- package/scripts/query_operators.py +196 -0
- package/scripts/query_rewriter.py +116 -7
- package/scripts/query_signals.py +300 -0
- package/scripts/query_templates.py +167 -0
- package/scripts/query_understanding.py +411 -0
- package/scripts/query_variants.py +298 -0
- package/scripts/quota.py +488 -76
- package/scripts/rank_signals.py +775 -0
- package/scripts/ranking_eval.py +319 -0
- package/scripts/readability_extract.py +433 -0
- package/scripts/recompute.py +415 -0
- package/scripts/recovery.py +616 -0
- package/scripts/recovery_l0.py +160 -0
- package/scripts/redskill/redskill_engine.py +332 -0
- package/scripts/regression_p0p1.py +318 -0
- package/scripts/replay_eval.py +640 -0
- package/scripts/rerank_tfidf.py +225 -0
- package/scripts/research.py +591 -403
- package/scripts/research_cli.py +361 -0
- package/scripts/research_dossier.py +492 -0
- package/scripts/research_expand.py +146 -0
- package/scripts/research_gates.py +272 -0
- package/scripts/research_report.py +209 -0
- package/scripts/research_strategy.py +37 -0
- package/scripts/research_work_packages.py +195 -0
- package/scripts/result_lang.py +311 -0
- package/scripts/robots_guard.py +259 -0
- package/scripts/route.py +921 -254
- package/scripts/route_bangs.py +40 -0
- package/scripts/route_cache.py +225 -0
- package/scripts/route_combo.py +910 -0
- package/scripts/route_domains.py +328 -0
- package/scripts/route_lang.py +605 -0
- package/scripts/route_log.py +63 -0
- package/scripts/route_policy.py +250 -0
- package/scripts/rrf_classic.py +66 -0
- package/scripts/screenshot.py +73 -0
- package/scripts/search.py +897 -405
- package/scripts/search_attribution.py +88 -0
- package/scripts/search_benchmark.py +407 -0
- package/scripts/search_cli.py +627 -0
- package/scripts/search_entry.py +389 -0
- package/scripts/search_output.py +606 -0
- package/scripts/search_pipeline.py +1077 -0
- package/scripts/search_rank.py +991 -0
- package/scripts/search_types.py +27 -12
- package/scripts/seek_locator.py +68 -0
- package/scripts/semantic_evidence.py +215 -0
- package/scripts/serp_guard.py +228 -0
- package/scripts/serp_kill_log.py +51 -0
- package/scripts/single_flight.py +120 -0
- package/scripts/social_engines/__init__.py +10 -0
- package/scripts/social_engines/bilibili_engine.py +131 -0
- package/scripts/social_engines/hackernews_engine.py +113 -0
- package/scripts/social_engines/reddit_engine.py +172 -0
- package/scripts/social_engines/twitter_engine.py +397 -0
- package/scripts/social_engines/v2ex_engine.py +101 -0
- package/scripts/social_engines/weibo_engine.py +128 -0
- package/scripts/social_engines/xiaohongshu_engine.py +137 -0
- package/scripts/social_engines/zhihu_engine.py +139 -0
- package/scripts/social_research.py +201 -0
- package/scripts/sogou_weixin_browser.py +318 -0
- package/scripts/source_ledger.py +249 -0
- package/scripts/stage_timing.py +58 -0
- package/scripts/stats_cli.py +95 -0
- package/scripts/straggler_policy.py +60 -0
- package/scripts/stylometry_detector.py +231 -0
- package/scripts/sync_backends.py +353 -0
- package/scripts/sync_installs.py +29 -0
- package/scripts/template_cli.py +127 -0
- package/scripts/text_lexicon.py +72 -0
- package/scripts/tfidf_router.py +109 -37
- package/scripts/time_utils.py +233 -0
- package/scripts/topic_research_profiles.py +643 -0
- package/scripts/train.py +737 -0
- package/scripts/tweet.py +596 -0
- package/scripts/url_canon.py +217 -0
- package/scripts/url_optimize.py +56 -0
- package/scripts/url_safety.py +236 -0
- package/scripts/usage_log.py +116 -0
- package/scripts/usage_log_clean.py +113 -0
- package/scripts/v2ex_nodes.py +311 -0
- package/scripts/variant_recall.py +188 -0
- package/scripts/watch.py +205 -0
- package/scripts/wechat_article.py +258 -0
- package/scripts/wide_research.py +402 -0
- package/scripts/wx.py +349 -0
- package/scripts/x_article_fetch.py +141 -0
- package/scripts/x_search.py +234 -0
- package/scripts/x_trending.py +189 -0
- package/scripts/yaml_load.py +67 -0
- package/sub-skills/ego-search/SKILL.md +253 -0
- package/sub-skills/ego-search/references/browser-runtime.md +43 -0
- package/sub-skills/ego-search/references/install.md +67 -0
- package/sub-skills/ego-search/references/original-ego-upgrade.md +51 -0
- package/sub-skills/ego-search/scripts/ego_search.py +800 -0
- package/sub-skills/ego-search/scripts/merge.py +226 -0
- package/sub-skills/ego-search/scripts/quality.py +64 -0
- package/sub-skills/ego-search/scripts/runtime.py +172 -0
- package/sub-skills/ego-search/scripts/safety.py +109 -0
- package/sub-skills/ego-search/scripts/serp_spec.py +109 -0
- package/sub-skills/ego-search/scripts/webbridge_adapter.py +374 -0
- package/sub-skills/ego-search/tests/test_query_normalize.py +68 -0
- package/sub-skills/js-run/SKILL.md +67 -0
- package/sub-skills/js-run/scripts/jsrun.py +112 -0
- package/sub-skills/js-run/scripts/shim.js +188 -0
- package/sub-skills/js-run/tests/fixtures/challenge_like.js +27 -0
- package/sub-skills/js-run/tests/test_jsrun.py +165 -0
- package/sub-skills/local-search/SKILL.md +50 -53
- package/sub-skills/local-search/bm25.py +162 -0
- package/sub-skills/local-search/config.yaml +152 -55
- package/sub-skills/local-search/engine_registry.py +91 -26
- package/sub-skills/local-search/{health_check.py → local_health_check.py} +140 -10
- package/sub-skills/local-search/local_search_adapter.py +13 -2
- package/sub-skills/local-search/parse_maps.yaml +40 -42
- package/sub-skills/local-search/search_v3.py +1182 -65
- package/sub-skills/local-search/smart_router.py +47 -7
- package/sub-skills/local-seek/SKILL.md +110 -0
- package/sub-skills/local-seek/config/domains.yaml +20 -0
- package/sub-skills/local-seek/references/auto-behaviors.md +24 -0
- package/sub-skills/local-seek/references/fd-mdfind-recipes.md +62 -0
- package/sub-skills/local-seek/references/git-integration.md +54 -0
- package/sub-skills/local-seek/references/remote-handoff.md +46 -0
- package/sub-skills/local-seek/references/rg-recipes.md +96 -0
- package/sub-skills/local-seek/references/strategies.md +52 -0
- package/sub-skills/local-seek/references/structural-search.md +59 -0
- package/sub-skills/local-seek/scripts/eval_rank.py +216 -0
- package/sub-skills/local-seek/scripts/eval_seek.py +224 -0
- package/sub-skills/local-seek/scripts/seek.py +1465 -0
- package/sub-skills/local-seek/scripts/seek_frecency.py +126 -0
- package/sub-skills/local-seek/scripts/seek_fts.py +430 -0
- package/sub-skills/local-seek/scripts/seek_rank.py +193 -0
- package/sub-skills/local-seek/tests/test_content_rank.py +222 -0
- package/sub-skills/local-seek/tests/test_determinism_20261006.py +145 -0
- package/sub-skills/local-seek/tests/test_noise_tier.py +101 -0
- package/sub-skills/local-seek/tests/test_pcre2_probe.py +136 -0
- package/sub-skills/local-seek/tests/test_rank_pinyin.py +77 -0
- package/sub-skills/local-seek/tests/test_regression_20261003.py +169 -0
- package/sub-skills/local-seek/tests/test_regression_20261006.py +146 -0
- package/sub-skills/local-seek/tests/test_seek_degradation.py +111 -0
- package/sub-skills/local-seek/tests/test_seek_fts.py +313 -0
package/scripts/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,145 +102,270 @@ 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
|
+
)
|
|
237
|
+
|
|
238
|
+
return (best_score, best_reason, tier, domain, bool(is_serp))
|
|
127
239
|
|
|
128
|
-
tier = "high" if best_score >= 0.8 else "medium" if best_score >= 0.6 else "low" if best_score >= 0.4 else "very_low"
|
|
129
240
|
|
|
241
|
+
def score_authority(url: str, source: str = "") -> dict[str, Any]:
|
|
242
|
+
"""评估 URL 的权威性(Selection 门槛)。
|
|
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
|
# ── 交叉验证 ──────────────────────────────────────────────────────────────────
|
|
@@ -227,22 +379,26 @@ def cross_validate(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
|
|
|
227
379
|
"detail": f"仅 {len(results)} 个结果,无法交叉验证",
|
|
228
380
|
"agreement_count": 0,
|
|
229
381
|
"total_sources": len(results),
|
|
382
|
+
"unique_domains": 0,
|
|
383
|
+
"url_overlap": 0.0,
|
|
384
|
+
"content_domains": 0,
|
|
230
385
|
}
|
|
231
386
|
|
|
232
|
-
# 检查 URL 重叠(不同来源指向同一页面 = 强佐证)
|
|
233
387
|
urls = [r.get("url", "") for r in results if r.get("url")]
|
|
234
388
|
unique_urls = set(urls)
|
|
235
389
|
url_overlap = 1 - (len(unique_urls) / max(len(urls), 1))
|
|
236
390
|
|
|
237
|
-
# 检查域名多样性
|
|
238
391
|
domains = set()
|
|
392
|
+
content_domains = set()
|
|
239
393
|
for r in results:
|
|
240
394
|
url = r.get("url", "")
|
|
241
|
-
if url:
|
|
242
|
-
|
|
243
|
-
|
|
395
|
+
if not url:
|
|
396
|
+
continue
|
|
397
|
+
domain = _normalize_domain(url)
|
|
398
|
+
domains.add(domain)
|
|
399
|
+
if not is_serp_or_jump_url(url):
|
|
400
|
+
content_domains.add(domain)
|
|
244
401
|
|
|
245
|
-
# 检查标题/内容相似度(简单关键词匹配)
|
|
246
402
|
query_words = set(query.lower().split())
|
|
247
403
|
content_matches = 0
|
|
248
404
|
for r in results:
|
|
@@ -251,82 +407,332 @@ def cross_validate(results: list[dict[str, Any]], query: str) -> dict[str, Any]:
|
|
|
251
407
|
content_matches += 1
|
|
252
408
|
|
|
253
409
|
match_ratio = content_matches / max(len(results), 1)
|
|
410
|
+
n_content = len(content_domains)
|
|
254
411
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
score = 0.
|
|
259
|
-
elif match_ratio >= 0.6 and len(domains) >= 2:
|
|
260
|
-
level = "moderate"
|
|
261
|
-
score = 0.7
|
|
412
|
+
if match_ratio >= 0.8 and n_content >= 3:
|
|
413
|
+
level, score = "strong", 0.9
|
|
414
|
+
elif match_ratio >= 0.6 and n_content >= 2:
|
|
415
|
+
level, score = "moderate", 0.7
|
|
262
416
|
elif match_ratio >= 0.4:
|
|
263
|
-
level = "weak"
|
|
264
|
-
score = 0.5
|
|
417
|
+
level, score = "weak", 0.5
|
|
265
418
|
else:
|
|
266
|
-
level = "minimal"
|
|
267
|
-
score = 0.3
|
|
419
|
+
level, score = "minimal", 0.3
|
|
268
420
|
|
|
269
421
|
return {
|
|
270
422
|
"corroboration_level": level,
|
|
271
423
|
"score": round(score, 2),
|
|
272
|
-
"detail":
|
|
424
|
+
"detail": (
|
|
425
|
+
f"{content_matches}/{len(results)} 与查询相关,"
|
|
426
|
+
f"{n_content} 个可吸收域名(总域名 {len(domains)})"
|
|
427
|
+
),
|
|
273
428
|
"agreement_count": content_matches,
|
|
274
429
|
"total_sources": len(results),
|
|
275
430
|
"unique_domains": len(domains),
|
|
431
|
+
"content_domains": n_content,
|
|
276
432
|
"url_overlap": round(url_overlap, 2),
|
|
277
433
|
}
|
|
278
434
|
|
|
279
435
|
|
|
436
|
+
# ── 可溯源性评分 ──────────────────────────────────────────────────────────────
|
|
437
|
+
|
|
438
|
+
# 一手信源平台特征(用户直接发布的原始内容)
|
|
439
|
+
_PRIMARY_PLATFORM_PATTERNS = {
|
|
440
|
+
"weibo.com": 0.95, # 微博原帖
|
|
441
|
+
"zhihu.com": 0.90, # 知乎回答/文章
|
|
442
|
+
"github.com": 0.90, # GitHub issue/PR/commit
|
|
443
|
+
"stackoverflow.com": 0.90,
|
|
444
|
+
"xiaohongshu.com": 0.85, # 小红书笔记
|
|
445
|
+
"bilibili.com": 0.85, # B站视频/动态
|
|
446
|
+
"twitter.com": 0.85, # X/Twitter 原帖
|
|
447
|
+
"x.com": 0.85,
|
|
448
|
+
"youtube.com": 0.85, # YouTube 视频
|
|
449
|
+
"reddit.com": 0.80, # Reddit 原帖
|
|
450
|
+
"medium.com": 0.80, # Medium 原文
|
|
451
|
+
"dev.to": 0.80,
|
|
452
|
+
"juejin.cn": 0.75, # 掘金原文
|
|
453
|
+
"cnblogs.com": 0.75, # 博客园原文
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
# 原始媒体报道(新闻机构原创报道)
|
|
457
|
+
_ORIGINAL_MEDIA_PATTERNS = {
|
|
458
|
+
"xinhuanet.com": 0.95, # 新华社
|
|
459
|
+
"people.com.cn": 0.95, # 人民网
|
|
460
|
+
"reuters.com": 0.95, # 路透社
|
|
461
|
+
"bloomberg.com": 0.95, # 彭博社
|
|
462
|
+
"wsj.com": 0.95, # 华尔街日报
|
|
463
|
+
"caixin.com": 0.90, # 财新
|
|
464
|
+
"yicai.com": 0.88, # 第一财经
|
|
465
|
+
"wallstreetcn.com": 0.88,# 华尔街见闻
|
|
466
|
+
"cls.cn": 0.88, # 财联社
|
|
467
|
+
"cs.com.cn": 0.90, # 中证网
|
|
468
|
+
"stcn.com": 0.85, # 证券时报
|
|
469
|
+
"cnr.cn": 0.88, # 央广网
|
|
470
|
+
"thepaper.cn": 0.82, # 澎湃新闻
|
|
471
|
+
"jiemian.com": 0.80, # 界面新闻
|
|
472
|
+
"36kr.com": 0.75, # 36氪
|
|
473
|
+
"eastmoney.com": 0.80, # 东方财富
|
|
474
|
+
"163.com": 0.70, # 网易新闻
|
|
475
|
+
"sohu.com": 0.70, # 搜狐新闻
|
|
476
|
+
"sina.com.cn": 0.70, # 新浪新闻
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
# 二手/聚合/洗稿平台特征
|
|
480
|
+
_SECONDARY_PLATFORM_PATTERNS = {
|
|
481
|
+
"baijiahao.baidu.com": 0.35, # 百家号(自媒体洗稿重灾区)
|
|
482
|
+
"toutiao.com": 0.35, # 头条号(聚合+洗稿)
|
|
483
|
+
"weixin.qq.com": 0.40, # 微信公众号(质量参差不齐)
|
|
484
|
+
"guba.eastmoney.com": 0.30, # 股吧(UGC 讨论)
|
|
485
|
+
"zhuanlan.zhihu.com": 0.55, # 知乎专栏(部分原创部分转载)
|
|
486
|
+
"k.sina.com.cn": 0.40, # 新浪博客/聚合
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
# 转载/引用标记(出现在标题或 snippet 中)
|
|
490
|
+
_REPOST_MARKERS = [
|
|
491
|
+
"转载", "转自", "摘自", "编译自", "译自", "综合自",
|
|
492
|
+
"来源:", "来源:", "原文:", "原文:", "援引", "据援引",
|
|
493
|
+
"reposted", "via@", "via @", "credit:", "courtesy",
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def score_traceability(result: dict[str, Any]) -> dict[str, Any]:
|
|
498
|
+
"""评估搜索结果的可溯源性(Traceability)。
|
|
499
|
+
|
|
500
|
+
借鉴 doubao-sentiment-tracker 的溯源四场景方法论,将「能否追溯到原始
|
|
501
|
+
来源」量化为 0-1 评分。与 Selection(域名权威)正交:一个结果可以
|
|
502
|
+
权威但不可溯源(如聚合媒体的转载稿),也可以不权威但可溯源(如个人
|
|
503
|
+
微博原帖)。
|
|
504
|
+
|
|
505
|
+
评分规则:
|
|
506
|
+
- 一手信源(用户直接发布的原始帖子):0.80-0.95
|
|
507
|
+
- 原始媒体报道(新闻机构原创报道):0.70-0.95
|
|
508
|
+
- 综合报道/评论(引用了原始来源):0.50-0.65
|
|
509
|
+
- 二手转述/洗稿:0.30-0.45
|
|
510
|
+
- 无法判断:0.50(中性)
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
{"score": float, "reason": str, "is_primary": bool, "platform_type": str}
|
|
514
|
+
"""
|
|
515
|
+
url = result.get("url", "") or ""
|
|
516
|
+
title = result.get("title", "") or ""
|
|
517
|
+
snippet = result.get("snippet", "") or ""
|
|
518
|
+
source = result.get("source", "") or ""
|
|
519
|
+
content_type = result.get("content_type", "") or ""
|
|
520
|
+
social_meta = result.get("social_meta")
|
|
521
|
+
text = f"{title} {snippet}"
|
|
522
|
+
|
|
523
|
+
if not url:
|
|
524
|
+
return {
|
|
525
|
+
"score": 0.5,
|
|
526
|
+
"reason": "无 URL,无法判断可溯源性",
|
|
527
|
+
"is_primary": False,
|
|
528
|
+
"platform_type": "unknown",
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
domain = _normalize_domain(url)
|
|
532
|
+
|
|
533
|
+
# 1) 检查是否为 SERP/跳转 URL(不可溯源)
|
|
534
|
+
if is_serp_or_jump_url(url):
|
|
535
|
+
return {
|
|
536
|
+
"score": 0.10,
|
|
537
|
+
"reason": "搜索结果页/跳转链,非原始内容",
|
|
538
|
+
"is_primary": False,
|
|
539
|
+
"platform_type": "serp",
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
# 2) 检查是否为一手信源平台
|
|
543
|
+
best_primary_score = 0.0
|
|
544
|
+
best_primary_domain = ""
|
|
545
|
+
for pattern, score in _PRIMARY_PLATFORM_PATTERNS.items():
|
|
546
|
+
if domain == pattern or domain.endswith("." + pattern):
|
|
547
|
+
if score > best_primary_score:
|
|
548
|
+
best_primary_score = score
|
|
549
|
+
best_primary_domain = pattern
|
|
550
|
+
|
|
551
|
+
if best_primary_score > 0:
|
|
552
|
+
# 检查是否有转载标记(即使是原帖平台,也可能是转载)
|
|
553
|
+
has_repost = any(marker in text for marker in _REPOST_MARKERS)
|
|
554
|
+
if has_repost:
|
|
555
|
+
return {
|
|
556
|
+
"score": 0.45,
|
|
557
|
+
"reason": f"原帖平台({best_primary_domain})但含转载标记",
|
|
558
|
+
"is_primary": False,
|
|
559
|
+
"platform_type": "repost_on_primary",
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
"score": best_primary_score,
|
|
563
|
+
"reason": f"一手信源({best_primary_domain})",
|
|
564
|
+
"is_primary": True,
|
|
565
|
+
"platform_type": "primary",
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
# 3) 检查是否为原始媒体报道
|
|
569
|
+
best_media_score = 0.0
|
|
570
|
+
best_media_domain = ""
|
|
571
|
+
for pattern, score in _ORIGINAL_MEDIA_PATTERNS.items():
|
|
572
|
+
if domain == pattern or domain.endswith("." + pattern):
|
|
573
|
+
if score > best_media_score:
|
|
574
|
+
best_media_score = score
|
|
575
|
+
best_media_domain = pattern
|
|
576
|
+
|
|
577
|
+
if best_media_score > 0:
|
|
578
|
+
has_repost = any(marker in text for marker in _REPOST_MARKERS)
|
|
579
|
+
if has_repost:
|
|
580
|
+
return {
|
|
581
|
+
"score": 0.55,
|
|
582
|
+
"reason": f"媒体平台({best_media_domain})但含转载标记",
|
|
583
|
+
"is_primary": False,
|
|
584
|
+
"platform_type": "media_repost",
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
"score": best_media_score,
|
|
588
|
+
"reason": f"原始媒体报道({best_media_domain})",
|
|
589
|
+
"is_primary": True,
|
|
590
|
+
"platform_type": "official_media",
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
# 4) 检查是否为二手/聚合平台
|
|
594
|
+
for pattern, score in _SECONDARY_PLATFORM_PATTERNS.items():
|
|
595
|
+
if domain == pattern or domain.endswith("." + pattern):
|
|
596
|
+
return {
|
|
597
|
+
"score": score,
|
|
598
|
+
"reason": f"二手/聚合平台({pattern})",
|
|
599
|
+
"is_primary": False,
|
|
600
|
+
"platform_type": "aggregator",
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
# 5) 检查 content_type 和 social_meta
|
|
604
|
+
if content_type == "social_post" or social_meta:
|
|
605
|
+
return {
|
|
606
|
+
"score": 0.75,
|
|
607
|
+
"reason": "社交帖子(content_type=social_post)",
|
|
608
|
+
"is_primary": True,
|
|
609
|
+
"platform_type": "social_post",
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
# 6) 检查转载标记
|
|
613
|
+
has_repost = any(marker in text for marker in _REPOST_MARKERS)
|
|
614
|
+
if has_repost:
|
|
615
|
+
return {
|
|
616
|
+
"score": 0.40,
|
|
617
|
+
"reason": "含转载/引用标记",
|
|
618
|
+
"is_primary": False,
|
|
619
|
+
"platform_type": "repost",
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
# 7) 无法判断(中性)
|
|
623
|
+
return {
|
|
624
|
+
"score": 0.50,
|
|
625
|
+
"reason": "无法判断可溯源性(默认中性)",
|
|
626
|
+
"is_primary": False,
|
|
627
|
+
"platform_type": "unknown",
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
|
|
280
631
|
# ── 综合可信度 ────────────────────────────────────────────────────────────────
|
|
281
632
|
|
|
282
|
-
def compute_credibility(
|
|
283
|
-
|
|
633
|
+
def compute_credibility(
|
|
634
|
+
results: list[dict[str, Any]],
|
|
635
|
+
query: str,
|
|
636
|
+
*,
|
|
637
|
+
high_stakes: bool = False,
|
|
638
|
+
) -> dict[str, Any]:
|
|
639
|
+
"""两阶段综合可信度:Selection × Absorption + Freshness。"""
|
|
284
640
|
query_time = time.time()
|
|
285
|
-
scored_results = []
|
|
641
|
+
scored_results: list[dict[str, Any]] = []
|
|
286
642
|
|
|
287
643
|
for r in results:
|
|
288
644
|
url = r.get("url", "")
|
|
289
645
|
source = r.get("source", "")
|
|
646
|
+
title = r.get("title", "") or ""
|
|
647
|
+
snippet = r.get("snippet", "") or ""
|
|
290
648
|
|
|
291
649
|
auth = score_authority(url, source)
|
|
292
650
|
fresh = score_freshness(r, query_time)
|
|
651
|
+
density = score_evidence_density(snippet, title)
|
|
293
652
|
|
|
294
|
-
#
|
|
295
|
-
|
|
653
|
+
# Selection:权威为主;SERP 直接压到低分
|
|
654
|
+
selection = auth["score"]
|
|
655
|
+
if auth.get("is_serp"):
|
|
656
|
+
selection = min(selection, 0.15)
|
|
657
|
+
|
|
658
|
+
# Absorption:证据密度
|
|
659
|
+
absorption = density["absorption_score"]
|
|
660
|
+
|
|
661
|
+
original_score = r.get("score")
|
|
662
|
+
if original_score is None:
|
|
663
|
+
original_score = 0.5
|
|
664
|
+
else:
|
|
665
|
+
try:
|
|
666
|
+
original_score = float(original_score)
|
|
667
|
+
except (TypeError, ValueError):
|
|
668
|
+
original_score = 0.5
|
|
669
|
+
# Traceability:可溯源性
|
|
670
|
+
trace = score_traceability(r)
|
|
671
|
+
|
|
672
|
+
# MECE 加权(量化)— 五维度
|
|
296
673
|
credibility = (
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
674
|
+
selection * 0.35
|
|
675
|
+
+ absorption * 0.30
|
|
676
|
+
+ fresh["score"] * 0.15
|
|
677
|
+
+ float(original_score) * 0.10
|
|
678
|
+
+ trace["score"] * 0.10
|
|
300
679
|
)
|
|
301
680
|
|
|
302
681
|
scored_results.append({
|
|
303
|
-
"title":
|
|
682
|
+
"title": title,
|
|
304
683
|
"url": url,
|
|
305
684
|
"source": source,
|
|
306
|
-
"snippet":
|
|
685
|
+
"snippet": snippet[:180],
|
|
307
686
|
"credibility": {
|
|
308
687
|
"final": round(credibility, 3),
|
|
688
|
+
"selection": round(selection, 3),
|
|
689
|
+
"absorption": round(absorption, 3),
|
|
309
690
|
"authority": auth,
|
|
310
691
|
"freshness": fresh,
|
|
692
|
+
"evidence_density": density,
|
|
311
693
|
"original_score": original_score,
|
|
694
|
+
"traceability": trace,
|
|
312
695
|
},
|
|
313
696
|
})
|
|
314
697
|
|
|
315
|
-
# 交叉验证
|
|
316
698
|
cross = cross_validate(results, query)
|
|
317
699
|
|
|
318
|
-
|
|
700
|
+
if high_stakes and scored_results:
|
|
701
|
+
# 共识调节:强佐证略抬高 top 可吸收源;弱共识不抬
|
|
702
|
+
boost = 0.0
|
|
703
|
+
if cross["corroboration_level"] == "strong":
|
|
704
|
+
boost = 0.05
|
|
705
|
+
elif cross["corroboration_level"] == "moderate":
|
|
706
|
+
boost = 0.02
|
|
707
|
+
for item in scored_results:
|
|
708
|
+
if not item["credibility"]["authority"].get("is_serp"):
|
|
709
|
+
item["credibility"]["final"] = round(
|
|
710
|
+
min(item["credibility"]["final"] + boost, 1.0), 3
|
|
711
|
+
)
|
|
712
|
+
item["credibility"]["consensus_boost"] = boost
|
|
713
|
+
|
|
319
714
|
scored_results.sort(key=lambda x: x["credibility"]["final"], reverse=True)
|
|
320
715
|
|
|
321
716
|
return {
|
|
322
717
|
"query": query,
|
|
718
|
+
"framework": "selection_x_absorption_v2.3_traceability",
|
|
323
719
|
"results": scored_results,
|
|
324
720
|
"cross_validation": cross,
|
|
325
721
|
"summary": {
|
|
326
722
|
"total": len(scored_results),
|
|
327
|
-
"high_credibility": sum(
|
|
328
|
-
|
|
329
|
-
|
|
723
|
+
"high_credibility": sum(
|
|
724
|
+
1 for r in scored_results if r["credibility"]["final"] >= 0.7
|
|
725
|
+
),
|
|
726
|
+
"medium_credibility": sum(
|
|
727
|
+
1 for r in scored_results if 0.5 <= r["credibility"]["final"] < 0.7
|
|
728
|
+
),
|
|
729
|
+
"low_credibility": sum(
|
|
730
|
+
1 for r in scored_results if r["credibility"]["final"] < 0.5
|
|
731
|
+
),
|
|
732
|
+
"serp_filtered": sum(
|
|
733
|
+
1 for r in scored_results
|
|
734
|
+
if r["credibility"]["authority"].get("is_serp")
|
|
735
|
+
),
|
|
330
736
|
"best_source": scored_results[0] if scored_results else None,
|
|
331
737
|
},
|
|
332
738
|
}
|
|
@@ -335,36 +741,53 @@ def compute_credibility(results: list[dict[str, Any]], query: str) -> dict[str,
|
|
|
335
741
|
# ── CLI ───────────────────────────────────────────────────────────────────────
|
|
336
742
|
|
|
337
743
|
def main():
|
|
338
|
-
|
|
744
|
+
ensure_utf8_stdio() # 入口第一件事:钉住 stdout/stderr 编码(见 cli_io)
|
|
745
|
+
parser = argparse.ArgumentParser(description="来源可信度评估工具(Selection×Absorption)")
|
|
339
746
|
parser.add_argument("query", nargs="?", default="", help="搜索查询词")
|
|
340
747
|
parser.add_argument("--stdin", action="store_true", help="从 stdin 读取 JSON 搜索结果")
|
|
341
748
|
parser.add_argument("--json", action="store_true", help="JSON 输出")
|
|
749
|
+
parser.add_argument(
|
|
750
|
+
"--high-stakes",
|
|
751
|
+
action="store_true",
|
|
752
|
+
help="高后果模式:启用共识微调",
|
|
753
|
+
)
|
|
342
754
|
args = parser.parse_args()
|
|
343
755
|
|
|
344
756
|
if args.stdin:
|
|
345
757
|
data = json.load(sys.stdin)
|
|
346
758
|
results = data.get("results", [])
|
|
347
759
|
else:
|
|
348
|
-
# 从搜索结果文件读取
|
|
349
760
|
results = []
|
|
350
761
|
|
|
351
762
|
if not results and args.query:
|
|
352
|
-
# 无输入结果时,输出提示
|
|
353
763
|
print("需要提供搜索结果进行评估。用法:")
|
|
354
764
|
print(' echo \'{"results": [...]}\' | python3 evidence.py --stdin --query "查询词"')
|
|
355
765
|
sys.exit(1)
|
|
356
766
|
|
|
357
|
-
report = compute_credibility(
|
|
767
|
+
report = compute_credibility(
|
|
768
|
+
results, args.query, high_stakes=args.high_stakes
|
|
769
|
+
)
|
|
358
770
|
|
|
359
771
|
if args.json:
|
|
360
|
-
print(
|
|
772
|
+
print(dumps(report))
|
|
361
773
|
else:
|
|
362
|
-
print(f"\n来源可信度评估:{report['query']}")
|
|
774
|
+
print(f"\n来源可信度评估:{report['query']} [{report['framework']}]")
|
|
363
775
|
print(f"{'='*50}")
|
|
364
776
|
for r in report["results"]:
|
|
365
777
|
c = r["credibility"]
|
|
366
778
|
level = "🟢" if c["final"] >= 0.7 else "🟡" if c["final"] >= 0.5 else "🔴"
|
|
367
|
-
|
|
779
|
+
ed = c.get("evidence_density") or {}
|
|
780
|
+
flags = []
|
|
781
|
+
if ed.get("has_numbers"):
|
|
782
|
+
flags.append("数")
|
|
783
|
+
if ed.get("has_comparison"):
|
|
784
|
+
flags.append("比")
|
|
785
|
+
if ed.get("has_definition"):
|
|
786
|
+
flags.append("义")
|
|
787
|
+
if c["authority"].get("is_serp"):
|
|
788
|
+
flags.append("SERP")
|
|
789
|
+
flag_s = ",".join(flags) if flags else "-"
|
|
790
|
+
print(f"{level} [{c['final']:.2f}] sel={c['selection']:.2f} abs={c['absorption']:.2f} [{flag_s}] {r['title'][:48]}")
|
|
368
791
|
print(f" 权威:{c['authority']['score']:.2f} ({c['authority']['reason']})")
|
|
369
792
|
print(f" 时效:{c['freshness']['score']:.2f} ({c['freshness']['reason']})")
|
|
370
793
|
print()
|
|
@@ -374,7 +797,10 @@ def main():
|
|
|
374
797
|
print(f" {cv['detail']}")
|
|
375
798
|
|
|
376
799
|
s = report["summary"]
|
|
377
|
-
print(
|
|
800
|
+
print(
|
|
801
|
+
f"\n总结:🟢高={s['high_credibility']} 🟡中={s['medium_credibility']} "
|
|
802
|
+
f"🔴低={s['low_credibility']} SERP={s.get('serp_filtered', 0)}"
|
|
803
|
+
)
|
|
378
804
|
|
|
379
805
|
|
|
380
806
|
if __name__ == "__main__":
|