qwen-sci 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (549) hide show
  1. qwen_sci-0.2.2.dist-info/METADATA +492 -0
  2. qwen_sci-0.2.2.dist-info/RECORD +549 -0
  3. qwen_sci-0.2.2.dist-info/WHEEL +4 -0
  4. qwen_sci-0.2.2.dist-info/entry_points.txt +6 -0
  5. qwen_sci-0.2.2.dist-info/licenses/LICENSE +21 -0
  6. src/1.txt +1 -0
  7. src/__init__.py +1 -0
  8. src/__main__.py +5 -0
  9. src/agents/__init__.py +1 -0
  10. src/agents/experiment_design_agent/__init__.py +268 -0
  11. src/agents/experiment_design_agent/artifacts.py +747 -0
  12. src/agents/experiment_design_agent/cache.py +319 -0
  13. src/agents/experiment_design_agent/completeness.py +130 -0
  14. src/agents/experiment_design_agent/contracts.py +771 -0
  15. src/agents/experiment_design_agent/counterexample_analyzer.py +187 -0
  16. src/agents/experiment_design_agent/design_evidence_paper_screener.py +470 -0
  17. src/agents/experiment_design_agent/discipline_catalog.py +178 -0
  18. src/agents/experiment_design_agent/evidence_cards.py +338 -0
  19. src/agents/experiment_design_agent/evidence_planner.py +452 -0
  20. src/agents/experiment_design_agent/formal_reasoning_planner.py +884 -0
  21. src/agents/experiment_design_agent/fulltext_acquisition.py +863 -0
  22. src/agents/experiment_design_agent/idea_adapter.py +100 -0
  23. src/agents/experiment_design_agent/idea_intake.py +87 -0
  24. src/agents/experiment_design_agent/llm_json.py +328 -0
  25. src/agents/experiment_design_agent/orchestrator.py +1220 -0
  26. src/agents/experiment_design_agent/reasoning_context.py +183 -0
  27. src/agents/experiment_design_agent/reasoning_validation.py +546 -0
  28. src/agents/experiment_design_agent/run.py +481 -0
  29. src/agents/experiment_design_agent/run_logging.py +292 -0
  30. src/agents/experiment_design_agent/scope_gate.py +198 -0
  31. src/agents/experiment_design_agent/study_type_composer.py +1360 -0
  32. src/agents/experiment_design_agent/survey_evidence.py +1381 -0
  33. src/agents/experiment_design_agent/template_router.py +195 -0
  34. src/agents/experiment_design_agent/variable_claim_extractor.py +93 -0
  35. src/agents/idea_agent/__init__.py +1 -0
  36. src/agents/idea_agent/agent/__init__.py +6 -0
  37. src/agents/idea_agent/agent/artifacts.py +308 -0
  38. src/agents/idea_agent/agent/base.py +122 -0
  39. src/agents/idea_agent/agent/ligagent.py +488 -0
  40. src/agents/idea_agent/agent/mcts.py +2060 -0
  41. src/agents/idea_agent/agent/prompts/__init__.py +48 -0
  42. src/agents/idea_agent/agent/prompts/advanced_analysis.py +235 -0
  43. src/agents/idea_agent/agent/prompts/algorithm_alignment.py +55 -0
  44. src/agents/idea_agent/agent/prompts/algorithm_structuring.py +48 -0
  45. src/agents/idea_agent/agent/prompts/component_extraction.py +62 -0
  46. src/agents/idea_agent/agent/prompts/component_novelty_evaluation.py +54 -0
  47. src/agents/idea_agent/agent/prompts/experiment_findings_extraction.py +39 -0
  48. src/agents/idea_agent/agent/prompts/idea_fusion.py +295 -0
  49. src/agents/idea_agent/agent/prompts/idea_introduction.py +30 -0
  50. src/agents/idea_agent/agent/prompts/idea_result_alignment.py +39 -0
  51. src/agents/idea_agent/agent/prompts/input_interpreter.py +52 -0
  52. src/agents/idea_agent/agent/prompts/keynote_ops.py +78 -0
  53. src/agents/idea_agent/agent/prompts/mcts_evaluation.py +235 -0
  54. src/agents/idea_agent/agent/prompts/mcts_generation.py +140 -0
  55. src/agents/idea_agent/agent/prompts/mechanism_commit_query.py +99 -0
  56. src/agents/idea_agent/agent/prompts/memory_selection.py +0 -0
  57. src/agents/idea_agent/agent/prompts/prompt_modes.py +16 -0
  58. src/agents/idea_agent/agent/prompts/rag_query.py +34 -0
  59. src/agents/idea_agent/agent/prompts/re_analysis_replan.py +48 -0
  60. src/agents/idea_agent/agent/prompts/reference_grounding.py +34 -0
  61. src/agents/idea_agent/agent/prompts/root_domain_classification.py +32 -0
  62. src/agents/idea_agent/agent/prompts/scientific_debate.py +88 -0
  63. src/agents/idea_agent/agent/prompts/scientific_materialization.py +45 -0
  64. src/agents/idea_agent/agent/prompts/skill_instantiation.py +193 -0
  65. src/agents/idea_agent/agent/prompts/theory_transfer_query.py +93 -0
  66. src/agents/idea_agent/agent/prompts/topic_background.py +15 -0
  67. src/agents/idea_agent/agent/skills/DEFAULT_SKILL_TEMPLATES.json +263 -0
  68. src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/SKILL.md +40 -0
  69. src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/references/contrast_patterns.md +22 -0
  70. src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/SKILL.md +40 -0
  71. src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/references/mechanism_cards.md +24 -0
  72. src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/SKILL.md +39 -0
  73. src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/references/hierarchy_patterns.md +22 -0
  74. src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/SKILL.md +39 -0
  75. src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/references/mechanism_commit_patterns.md +24 -0
  76. src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/SKILL.md +39 -0
  77. src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/references/coordination_patterns.md +22 -0
  78. src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/SKILL.md +41 -0
  79. src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/references/speculation_patterns.md +22 -0
  80. src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/SKILL.md +39 -0
  81. src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/references/modularity_patterns.md +22 -0
  82. src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/SKILL.md +40 -0
  83. src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/references/transfer_patterns.md +24 -0
  84. src/agents/idea_agent/example/idea_result.json +900 -0
  85. src/agents/idea_agent/run.py +276 -0
  86. src/agents/idea_agent/tests/test_advanced_analysis_contract.py +90 -0
  87. src/agents/idea_agent/tests/test_gap_hypothesis_bridge.py +166 -0
  88. src/agents/idea_agent/tests/test_idea_debate.py +480 -0
  89. src/agents/idea_agent/tests/test_idea_result_v5.py +99 -0
  90. src/agents/idea_agent/tests/test_idea_synthesis.py +92 -0
  91. src/agents/idea_agent/tests/test_mature_idea_sources.py +27 -0
  92. src/agents/idea_agent/tests/test_multimodal_idea_integration.py +386 -0
  93. src/agents/idea_agent/tests/test_profile_materialization_and_scoring.py +219 -0
  94. src/agents/idea_agent/tests/test_profile_native_skill_rendering.py +122 -0
  95. src/agents/idea_agent/tests/test_result_persistence.py +183 -0
  96. src/agents/idea_agent/tests/test_scientific_intervention_profiles.py +241 -0
  97. src/agents/idea_agent/tests/test_scientific_rubric_and_prompts.py +559 -0
  98. src/agents/idea_agent/tests/test_strict_json_object_responses.py +48 -0
  99. src/agents/idea_agent/utils/core/__init__.py +1 -0
  100. src/agents/idea_agent/utils/core/ablation_inputs.py +60 -0
  101. src/agents/idea_agent/utils/core/chat_errors.py +95 -0
  102. src/agents/idea_agent/utils/core/chat_router.py +130 -0
  103. src/agents/idea_agent/utils/core/chat_transport.py +188 -0
  104. src/agents/idea_agent/utils/core/config_loader.py +166 -0
  105. src/agents/idea_agent/utils/core/json_utils.py +28 -0
  106. src/agents/idea_agent/utils/core/logger.py +287 -0
  107. src/agents/idea_agent/utils/core/progress.py +58 -0
  108. src/agents/idea_agent/utils/core/response_parsing.py +57 -0
  109. src/agents/idea_agent/utils/core/run_inputs.py +198 -0
  110. src/agents/idea_agent/utils/mcts/__init__.py +1 -0
  111. src/agents/idea_agent/utils/mcts/component_novelty.py +342 -0
  112. src/agents/idea_agent/utils/mcts/defect_registry.py +302 -0
  113. src/agents/idea_agent/utils/mcts/idea_routes.py +61 -0
  114. src/agents/idea_agent/utils/mcts/idea_taste_presets.py +312 -0
  115. src/agents/idea_agent/utils/mcts/mcts_helpers.py +738 -0
  116. src/agents/idea_agent/utils/mcts/mcts_runtime.py +3000 -0
  117. src/agents/idea_agent/utils/mcts/scientific_intervention_ontology.py +1561 -0
  118. src/agents/idea_agent/utils/mcts/scientific_rubric.py +211 -0
  119. src/agents/idea_agent/utils/mcts/skill_parsing.py +92 -0
  120. src/agents/idea_agent/utils/papers/__init__.py +1 -0
  121. src/agents/idea_agent/utils/papers/paper_graph_vector_store.py +499 -0
  122. src/agents/idea_agent/utils/papers/paper_repository.py +372 -0
  123. src/agents/idea_agent/utils/prompting/__init__.py +1 -0
  124. src/agents/idea_agent/utils/prompting/prompt_views.py +637 -0
  125. src/agents/idea_agent/utils/workflow/__init__.py +1 -0
  126. src/agents/idea_agent/utils/workflow/advanced_analysis_contract.py +244 -0
  127. src/agents/idea_agent/utils/workflow/gap_hypothesis_bridge.py +461 -0
  128. src/agents/idea_agent/utils/workflow/idea_contract.py +426 -0
  129. src/agents/idea_agent/utils/workflow/idea_debate.py +1238 -0
  130. src/agents/idea_agent/utils/workflow/idea_diversity.py +163 -0
  131. src/agents/idea_agent/utils/workflow/idea_fusion.py +889 -0
  132. src/agents/idea_agent/utils/workflow/idea_helpers.py +478 -0
  133. src/agents/idea_agent/utils/workflow/idea_portfolio.py +422 -0
  134. src/agents/idea_agent/utils/workflow/idea_synthesis.py +482 -0
  135. src/agents/idea_agent/utils/workflow/ligagent_flow.py +581 -0
  136. src/agents/idea_agent/utils/workflow/ligagent_handlers.py +1836 -0
  137. src/agents/idea_agent/utils/workflow/ligagent_helpers.py +1331 -0
  138. src/agents/idea_agent/utils/workflow/ligagent_utils.py +322 -0
  139. src/agents/idea_agent/utils/workflow/mature_idea_sources.py +648 -0
  140. src/agents/idea_agent/utils/workflow/multimodal_data_anchoring.py +578 -0
  141. src/agents/idea_agent/utils/workflow/result_persistence.py +774 -0
  142. src/agents/idea_agent/utils/workflow/stage_contract.py +76 -0
  143. src/agents/idea_agent/utils/workflow/workflow_runtime.py +288 -0
  144. src/agents/research_plan_author/__init__.py +50 -0
  145. src/agents/research_plan_author/artifacts.py +270 -0
  146. src/agents/research_plan_author/authoring_blueprint.py +1246 -0
  147. src/agents/research_plan_author/bibtex_renderer.py +252 -0
  148. src/agents/research_plan_author/contract_repair.py +131 -0
  149. src/agents/research_plan_author/contracts.py +745 -0
  150. src/agents/research_plan_author/cross_section_editor.py +232 -0
  151. src/agents/research_plan_author/document_quality.py +987 -0
  152. src/agents/research_plan_author/idea_evolution.py +223 -0
  153. src/agents/research_plan_author/input_loader.py +76 -0
  154. src/agents/research_plan_author/latex_compiler.py +260 -0
  155. src/agents/research_plan_author/latex_safety.py +159 -0
  156. src/agents/research_plan_author/llm_json.py +74 -0
  157. src/agents/research_plan_author/markdown_renderer.py +275 -0
  158. src/agents/research_plan_author/pdf_validator.py +149 -0
  159. src/agents/research_plan_author/render.py +166 -0
  160. src/agents/research_plan_author/render_artifacts.py +187 -0
  161. src/agents/research_plan_author/run.py +765 -0
  162. src/agents/research_plan_author/run_logging.py +209 -0
  163. src/agents/research_plan_author/section_cache.py +184 -0
  164. src/agents/research_plan_author/section_composer.py +1764 -0
  165. src/agents/research_plan_author/section_router.py +209 -0
  166. src/agents/research_plan_author/semantic_validator.py +254 -0
  167. src/agents/research_plan_author/source_bundle.py +46 -0
  168. src/agents/research_plan_author/source_registry.py +208 -0
  169. src/agents/research_plan_author/survey_source_loader.py +137 -0
  170. src/agents/research_plan_author/template_adapter.py +173 -0
  171. src/agents/research_plan_author/template_profile.py +204 -0
  172. src/agents/research_plan_author/tex_renderer.py +496 -0
  173. src/agents/research_plan_author/theory_presentation.py +134 -0
  174. src/agents/research_plan_author/theory_spine.py +703 -0
  175. src/agents/survey_agent/Dockerfile +54 -0
  176. src/agents/survey_agent/README.md +350 -0
  177. src/agents/survey_agent/config/deep_survey.yaml +217 -0
  178. src/agents/survey_agent/config/deep_survey_batch.yaml +176 -0
  179. src/agents/survey_agent/config/deep_survey_fast.yaml +183 -0
  180. src/agents/survey_agent/config/evaluate_baseline.yaml +197 -0
  181. src/agents/survey_agent/config/outcomeRAG.yaml +159 -0
  182. src/agents/survey_agent/env_report.md +348 -0
  183. src/agents/survey_agent/modules/__init__.py +0 -0
  184. src/agents/survey_agent/modules/code_collector.py +2194 -0
  185. src/agents/survey_agent/modules/code_report_generator.py +1282 -0
  186. src/agents/survey_agent/modules/data_manager.py +1713 -0
  187. src/agents/survey_agent/modules/database.py +167 -0
  188. src/agents/survey_agent/modules/fulltext_download_cache.py +462 -0
  189. src/agents/survey_agent/modules/fulltext_http.py +134 -0
  190. src/agents/survey_agent/modules/fulltext_resolution.py +749 -0
  191. src/agents/survey_agent/modules/judge.py +434 -0
  192. src/agents/survey_agent/modules/outcome_RAG.py +236 -0
  193. src/agents/survey_agent/modules/paper_graph_retriever.py +1075 -0
  194. src/agents/survey_agent/modules/pe.py +1729 -0
  195. src/agents/survey_agent/modules/pseudo_creator.py +782 -0
  196. src/agents/survey_agent/modules/pseudo_reviser.py +537 -0
  197. src/agents/survey_agent/modules/refine_agent.py +1952 -0
  198. src/agents/survey_agent/modules/survey_generator.py +5691 -0
  199. src/agents/survey_agent/modules/survey_visualizer.py +1817 -0
  200. src/agents/survey_agent/modules/visual_prompts.py +327 -0
  201. src/agents/survey_agent/modules/work_analyzer.py +2750 -0
  202. src/agents/survey_agent/modules/work_collector.py +5649 -0
  203. src/agents/survey_agent/scripts/baseline_evaluation.py +411 -0
  204. src/agents/survey_agent/scripts/run_deep_survey.py +224 -0
  205. src/agents/survey_agent/scripts/run_deep_survey_adapter.py +407 -0
  206. src/agents/survey_agent/scripts/run_deep_survey_adapter_with_input.py +409 -0
  207. src/agents/survey_agent/scripts/run_deep_survey_batch.py +228 -0
  208. src/agents/survey_agent/scripts/run_deep_survey_batch_arg.py +243 -0
  209. src/agents/survey_agent/scripts/run_deep_survey_batch_settings.py +218 -0
  210. src/agents/survey_agent/scripts/run_keynotes_gen.py +128 -0
  211. src/agents/survey_agent/scripts/run_work_clusters.py +140 -0
  212. src/agents/survey_agent/utils/__init__.py +0 -0
  213. src/agents/survey_agent/utils/ablation_utils.py +317 -0
  214. src/agents/survey_agent/utils/api_call.py +2518 -0
  215. src/agents/survey_agent/utils/chat_utils.py +82 -0
  216. src/agents/survey_agent/utils/config_utils.py +70 -0
  217. src/agents/survey_agent/utils/convert_to_md.py +29 -0
  218. src/agents/survey_agent/utils/err_info.py +24 -0
  219. src/agents/survey_agent/utils/file_utils.py +176 -0
  220. src/agents/survey_agent/utils/gpu_utils.py +145 -0
  221. src/agents/survey_agent/utils/graph_visualizer.py +279 -0
  222. src/agents/survey_agent/utils/html_utils.py +181 -0
  223. src/agents/survey_agent/utils/latex_generator_intra_cluster.py +529 -0
  224. src/agents/survey_agent/utils/latex_generator_relation_table.py +193 -0
  225. src/agents/survey_agent/utils/mineru_section_packer.py +334 -0
  226. src/agents/survey_agent/utils/mineru_utils.py +396 -0
  227. src/agents/survey_agent/utils/paper.py +144 -0
  228. src/agents/survey_agent/utils/read_yaml.py +15 -0
  229. src/agents/survey_agent/utils/repo_utils.py +167 -0
  230. src/agents/survey_agent/utils/rich_logger.py +77 -0
  231. src/agents/survey_agent/utils/step2v2.py +874 -0
  232. src/agents/survey_agent/utils/step2v2_extractor.py +856 -0
  233. src/agents/survey_agent/utils/topic_survey_storage.py +309 -0
  234. src/agents/survey_agent/utils/utils.py +118 -0
  235. src/agents/survey_agent/utils/work.py +2 -0
  236. src/cli.py +1611 -0
  237. src/config/__init__.py +213 -0
  238. src/config/default.yaml +1122 -0
  239. src/harness/LICENSE +21 -0
  240. src/harness/VENDORED.md +14 -0
  241. src/harness/src/__init__.py +0 -0
  242. src/harness/src/openharness/__init__.py +0 -0
  243. src/harness/src/openharness/__main__.py +6 -0
  244. src/harness/src/openharness/api/__init__.py +21 -0
  245. src/harness/src/openharness/api/client.py +281 -0
  246. src/harness/src/openharness/api/codex_client.py +407 -0
  247. src/harness/src/openharness/api/copilot_auth.py +240 -0
  248. src/harness/src/openharness/api/copilot_client.py +134 -0
  249. src/harness/src/openharness/api/errors.py +19 -0
  250. src/harness/src/openharness/api/openai_client.py +577 -0
  251. src/harness/src/openharness/api/provider.py +186 -0
  252. src/harness/src/openharness/api/registry.py +437 -0
  253. src/harness/src/openharness/api/usage.py +17 -0
  254. src/harness/src/openharness/auth/__init__.py +29 -0
  255. src/harness/src/openharness/auth/external.py +610 -0
  256. src/harness/src/openharness/auth/flows.py +189 -0
  257. src/harness/src/openharness/auth/manager.py +482 -0
  258. src/harness/src/openharness/auth/storage.py +269 -0
  259. src/harness/src/openharness/autopilot/__init__.py +23 -0
  260. src/harness/src/openharness/autopilot/service.py +2239 -0
  261. src/harness/src/openharness/autopilot/types.py +91 -0
  262. src/harness/src/openharness/bridge/__init__.py +20 -0
  263. src/harness/src/openharness/bridge/manager.py +106 -0
  264. src/harness/src/openharness/bridge/session_runner.py +46 -0
  265. src/harness/src/openharness/bridge/types.py +37 -0
  266. src/harness/src/openharness/bridge/work_secret.py +41 -0
  267. src/harness/src/openharness/channels/UPSTREAM +6 -0
  268. src/harness/src/openharness/channels/__init__.py +22 -0
  269. src/harness/src/openharness/channels/adapter.py +131 -0
  270. src/harness/src/openharness/channels/bus/__init__.py +6 -0
  271. src/harness/src/openharness/channels/bus/events.py +38 -0
  272. src/harness/src/openharness/channels/bus/queue.py +44 -0
  273. src/harness/src/openharness/channels/impl/__init__.py +6 -0
  274. src/harness/src/openharness/channels/impl/base.py +141 -0
  275. src/harness/src/openharness/channels/impl/dingtalk.py +445 -0
  276. src/harness/src/openharness/channels/impl/discord.py +311 -0
  277. src/harness/src/openharness/channels/impl/email.py +410 -0
  278. src/harness/src/openharness/channels/impl/feishu.py +1342 -0
  279. src/harness/src/openharness/channels/impl/manager.py +257 -0
  280. src/harness/src/openharness/channels/impl/matrix.py +700 -0
  281. src/harness/src/openharness/channels/impl/mochat.py +897 -0
  282. src/harness/src/openharness/channels/impl/qq.py +141 -0
  283. src/harness/src/openharness/channels/impl/slack.py +284 -0
  284. src/harness/src/openharness/channels/impl/telegram.py +525 -0
  285. src/harness/src/openharness/channels/impl/whatsapp.py +159 -0
  286. src/harness/src/openharness/cli.py +2551 -0
  287. src/harness/src/openharness/commands/__init__.py +21 -0
  288. src/harness/src/openharness/commands/registry.py +2783 -0
  289. src/harness/src/openharness/config/__init__.py +34 -0
  290. src/harness/src/openharness/config/paths.py +160 -0
  291. src/harness/src/openharness/config/schema.py +119 -0
  292. src/harness/src/openharness/config/settings.py +1104 -0
  293. src/harness/src/openharness/coordinator/__init__.py +12 -0
  294. src/harness/src/openharness/coordinator/agent_definitions.py +975 -0
  295. src/harness/src/openharness/coordinator/coordinator_mode.py +520 -0
  296. src/harness/src/openharness/engine/__init__.py +80 -0
  297. src/harness/src/openharness/engine/cost_tracker.py +24 -0
  298. src/harness/src/openharness/engine/messages.py +222 -0
  299. src/harness/src/openharness/engine/query.py +1188 -0
  300. src/harness/src/openharness/engine/query_engine.py +313 -0
  301. src/harness/src/openharness/engine/stream_events.py +90 -0
  302. src/harness/src/openharness/hooks/__init__.py +50 -0
  303. src/harness/src/openharness/hooks/events.py +20 -0
  304. src/harness/src/openharness/hooks/executor.py +242 -0
  305. src/harness/src/openharness/hooks/hot_reload.py +31 -0
  306. src/harness/src/openharness/hooks/loader.py +68 -0
  307. src/harness/src/openharness/hooks/schemas.py +66 -0
  308. src/harness/src/openharness/hooks/types.py +38 -0
  309. src/harness/src/openharness/keybindings/__init__.py +14 -0
  310. src/harness/src/openharness/keybindings/default_bindings.py +11 -0
  311. src/harness/src/openharness/keybindings/loader.py +22 -0
  312. src/harness/src/openharness/keybindings/parser.py +18 -0
  313. src/harness/src/openharness/keybindings/resolver.py +13 -0
  314. src/harness/src/openharness/mcp/__init__.py +79 -0
  315. src/harness/src/openharness/mcp/client.py +298 -0
  316. src/harness/src/openharness/mcp/config.py +16 -0
  317. src/harness/src/openharness/mcp/types.py +76 -0
  318. src/harness/src/openharness/memory/__init__.py +25 -0
  319. src/harness/src/openharness/memory/agent.py +91 -0
  320. src/harness/src/openharness/memory/manager.py +183 -0
  321. src/harness/src/openharness/memory/memdir.py +52 -0
  322. src/harness/src/openharness/memory/migrate.py +137 -0
  323. src/harness/src/openharness/memory/paths.py +22 -0
  324. src/harness/src/openharness/memory/relevance.py +145 -0
  325. src/harness/src/openharness/memory/scan.py +116 -0
  326. src/harness/src/openharness/memory/schema.py +443 -0
  327. src/harness/src/openharness/memory/search.py +71 -0
  328. src/harness/src/openharness/memory/team.py +90 -0
  329. src/harness/src/openharness/memory/types.py +33 -0
  330. src/harness/src/openharness/memory/usage.py +153 -0
  331. src/harness/src/openharness/output_styles/__init__.py +5 -0
  332. src/harness/src/openharness/output_styles/loader.py +42 -0
  333. src/harness/src/openharness/permissions/__init__.py +26 -0
  334. src/harness/src/openharness/permissions/checker.py +200 -0
  335. src/harness/src/openharness/permissions/modes.py +13 -0
  336. src/harness/src/openharness/personalization/__init__.py +6 -0
  337. src/harness/src/openharness/personalization/extractor.py +138 -0
  338. src/harness/src/openharness/personalization/rules.py +64 -0
  339. src/harness/src/openharness/personalization/session_hook.py +65 -0
  340. src/harness/src/openharness/platforms.py +86 -0
  341. src/harness/src/openharness/plugins/__init__.py +53 -0
  342. src/harness/src/openharness/plugins/bundled/__init__.py +0 -0
  343. src/harness/src/openharness/plugins/installer.py +39 -0
  344. src/harness/src/openharness/plugins/loader.py +730 -0
  345. src/harness/src/openharness/plugins/schemas.py +24 -0
  346. src/harness/src/openharness/plugins/types.py +59 -0
  347. src/harness/src/openharness/prompts/__init__.py +14 -0
  348. src/harness/src/openharness/prompts/claudemd.py +48 -0
  349. src/harness/src/openharness/prompts/context.py +187 -0
  350. src/harness/src/openharness/prompts/environment.py +135 -0
  351. src/harness/src/openharness/prompts/system_prompt.py +109 -0
  352. src/harness/src/openharness/sandbox/Dockerfile +6 -0
  353. src/harness/src/openharness/sandbox/__init__.py +33 -0
  354. src/harness/src/openharness/sandbox/adapter.py +148 -0
  355. src/harness/src/openharness/sandbox/docker_backend.py +232 -0
  356. src/harness/src/openharness/sandbox/docker_image.py +103 -0
  357. src/harness/src/openharness/sandbox/path_validator.py +37 -0
  358. src/harness/src/openharness/sandbox/session.py +63 -0
  359. src/harness/src/openharness/services/__init__.py +30 -0
  360. src/harness/src/openharness/services/autodream/__init__.py +34 -0
  361. src/harness/src/openharness/services/autodream/backup.py +104 -0
  362. src/harness/src/openharness/services/autodream/lock.py +138 -0
  363. src/harness/src/openharness/services/autodream/prompt.py +128 -0
  364. src/harness/src/openharness/services/autodream/service.py +313 -0
  365. src/harness/src/openharness/services/compact/__init__.py +1725 -0
  366. src/harness/src/openharness/services/cron.py +137 -0
  367. src/harness/src/openharness/services/cron_scheduler.py +595 -0
  368. src/harness/src/openharness/services/lsp/__init__.py +216 -0
  369. src/harness/src/openharness/services/memory_extract/__init__.py +261 -0
  370. src/harness/src/openharness/services/oauth/__init__.py +0 -0
  371. src/harness/src/openharness/services/session_backend.py +97 -0
  372. src/harness/src/openharness/services/session_memory/__init__.py +139 -0
  373. src/harness/src/openharness/services/session_storage.py +230 -0
  374. src/harness/src/openharness/services/token_estimation.py +15 -0
  375. src/harness/src/openharness/services/tool_outputs.py +55 -0
  376. src/harness/src/openharness/skills/__init__.py +44 -0
  377. src/harness/src/openharness/skills/_frontmatter.py +97 -0
  378. src/harness/src/openharness/skills/bundled/__init__.py +75 -0
  379. src/harness/src/openharness/skills/bundled/content/commit.md +25 -0
  380. src/harness/src/openharness/skills/bundled/content/debug.md +25 -0
  381. src/harness/src/openharness/skills/bundled/content/diagnose.md +35 -0
  382. src/harness/src/openharness/skills/bundled/content/plan.md +39 -0
  383. src/harness/src/openharness/skills/bundled/content/review.md +33 -0
  384. src/harness/src/openharness/skills/bundled/content/simplify.md +26 -0
  385. src/harness/src/openharness/skills/bundled/content/skill-creator.md +175 -0
  386. src/harness/src/openharness/skills/bundled/content/test.md +38 -0
  387. src/harness/src/openharness/skills/loader.py +229 -0
  388. src/harness/src/openharness/skills/registry.py +29 -0
  389. src/harness/src/openharness/skills/types.py +24 -0
  390. src/harness/src/openharness/state/__init__.py +6 -0
  391. src/harness/src/openharness/state/app_state.py +30 -0
  392. src/harness/src/openharness/state/store.py +40 -0
  393. src/harness/src/openharness/swarm/__init__.py +70 -0
  394. src/harness/src/openharness/swarm/in_process.py +693 -0
  395. src/harness/src/openharness/swarm/lockfile.py +24 -0
  396. src/harness/src/openharness/swarm/mailbox.py +522 -0
  397. src/harness/src/openharness/swarm/permission_sync.py +1168 -0
  398. src/harness/src/openharness/swarm/registry.py +410 -0
  399. src/harness/src/openharness/swarm/spawn_utils.py +229 -0
  400. src/harness/src/openharness/swarm/subprocess_backend.py +171 -0
  401. src/harness/src/openharness/swarm/team_lifecycle.py +910 -0
  402. src/harness/src/openharness/swarm/types.py +398 -0
  403. src/harness/src/openharness/swarm/worktree.py +315 -0
  404. src/harness/src/openharness/tasks/__init__.py +18 -0
  405. src/harness/src/openharness/tasks/local_agent_task.py +28 -0
  406. src/harness/src/openharness/tasks/local_shell_task.py +17 -0
  407. src/harness/src/openharness/tasks/manager.py +475 -0
  408. src/harness/src/openharness/tasks/stop_task.py +11 -0
  409. src/harness/src/openharness/tasks/types.py +32 -0
  410. src/harness/src/openharness/themes/__init__.py +21 -0
  411. src/harness/src/openharness/themes/builtin.py +89 -0
  412. src/harness/src/openharness/themes/loader.py +55 -0
  413. src/harness/src/openharness/themes/schema.py +54 -0
  414. src/harness/src/openharness/tools/__init__.py +107 -0
  415. src/harness/src/openharness/tools/agent_tool.py +141 -0
  416. src/harness/src/openharness/tools/ask_user_question_tool.py +46 -0
  417. src/harness/src/openharness/tools/base.py +80 -0
  418. src/harness/src/openharness/tools/bash_tool.py +259 -0
  419. src/harness/src/openharness/tools/brief_tool.py +33 -0
  420. src/harness/src/openharness/tools/config_tool.py +54 -0
  421. src/harness/src/openharness/tools/cron_create_tool.py +105 -0
  422. src/harness/src/openharness/tools/cron_delete_tool.py +32 -0
  423. src/harness/src/openharness/tools/cron_list_tool.py +69 -0
  424. src/harness/src/openharness/tools/cron_toggle_tool.py +37 -0
  425. src/harness/src/openharness/tools/enter_plan_mode_tool.py +28 -0
  426. src/harness/src/openharness/tools/enter_worktree_tool.py +80 -0
  427. src/harness/src/openharness/tools/exit_plan_mode_tool.py +28 -0
  428. src/harness/src/openharness/tools/exit_worktree_tool.py +42 -0
  429. src/harness/src/openharness/tools/file_edit_tool.py +95 -0
  430. src/harness/src/openharness/tools/file_read_tool.py +72 -0
  431. src/harness/src/openharness/tools/file_write_tool.py +87 -0
  432. src/harness/src/openharness/tools/glob_tool.py +175 -0
  433. src/harness/src/openharness/tools/grep_tool.py +374 -0
  434. src/harness/src/openharness/tools/image_generation_tool.py +447 -0
  435. src/harness/src/openharness/tools/image_to_text_tool.py +237 -0
  436. src/harness/src/openharness/tools/list_mcp_resources_tool.py +36 -0
  437. src/harness/src/openharness/tools/lsp_tool.py +154 -0
  438. src/harness/src/openharness/tools/mcp_auth_tool.py +71 -0
  439. src/harness/src/openharness/tools/mcp_tool.py +72 -0
  440. src/harness/src/openharness/tools/notebook_edit_tool.py +97 -0
  441. src/harness/src/openharness/tools/read_mcp_resource_tool.py +38 -0
  442. src/harness/src/openharness/tools/remote_trigger_tool.py +74 -0
  443. src/harness/src/openharness/tools/send_message_tool.py +58 -0
  444. src/harness/src/openharness/tools/skill_tool.py +43 -0
  445. src/harness/src/openharness/tools/sleep_tool.py +32 -0
  446. src/harness/src/openharness/tools/task_create_tool.py +56 -0
  447. src/harness/src/openharness/tools/task_get_tool.py +33 -0
  448. src/harness/src/openharness/tools/task_list_tool.py +35 -0
  449. src/harness/src/openharness/tools/task_output_tool.py +35 -0
  450. src/harness/src/openharness/tools/task_stop_tool.py +30 -0
  451. src/harness/src/openharness/tools/task_update_tool.py +50 -0
  452. src/harness/src/openharness/tools/team_create_tool.py +31 -0
  453. src/harness/src/openharness/tools/team_delete_tool.py +30 -0
  454. src/harness/src/openharness/tools/todo_write_tool.py +46 -0
  455. src/harness/src/openharness/tools/tool_search_tool.py +38 -0
  456. src/harness/src/openharness/tools/web_fetch_tool.py +117 -0
  457. src/harness/src/openharness/tools/web_search_tool.py +119 -0
  458. src/harness/src/openharness/ui/__init__.py +5 -0
  459. src/harness/src/openharness/ui/app.py +320 -0
  460. src/harness/src/openharness/ui/backend_host.py +941 -0
  461. src/harness/src/openharness/ui/coordinator_drain.py +198 -0
  462. src/harness/src/openharness/ui/input.py +32 -0
  463. src/harness/src/openharness/ui/output.py +265 -0
  464. src/harness/src/openharness/ui/permission_dialog.py +14 -0
  465. src/harness/src/openharness/ui/protocol.py +247 -0
  466. src/harness/src/openharness/ui/react_launcher.py +179 -0
  467. src/harness/src/openharness/ui/runtime.py +799 -0
  468. src/harness/src/openharness/ui/textual_app.py +495 -0
  469. src/harness/src/openharness/utils/__init__.py +0 -0
  470. src/harness/src/openharness/utils/file_lock.py +86 -0
  471. src/harness/src/openharness/utils/fs.py +98 -0
  472. src/harness/src/openharness/utils/helpers.py +77 -0
  473. src/harness/src/openharness/utils/network_guard.py +340 -0
  474. src/harness/src/openharness/utils/shell.py +147 -0
  475. src/harness/src/openharness/vim/__init__.py +5 -0
  476. src/harness/src/openharness/vim/transitions.py +8 -0
  477. src/harness/src/openharness/voice/__init__.py +7 -0
  478. src/harness/src/openharness/voice/keyterms.py +10 -0
  479. src/harness/src/openharness/voice/stream_stt.py +8 -0
  480. src/harness/src/openharness/voice/voice_mode.py +44 -0
  481. src/llm/__init__.py +29 -0
  482. src/llm/image_generation.py +402 -0
  483. src/llm/provider_registry.py +419 -0
  484. src/llm/runtime_env.py +64 -0
  485. src/llm/structured_output.py +131 -0
  486. src/llm/vision.py +198 -0
  487. src/memory/__init__.py +2 -0
  488. src/memory/api/__init__.py +1 -0
  489. src/memory/api/base_memory_system_api.py +98 -0
  490. src/memory/api/base_symbolic_memory_system_api.py +190 -0
  491. src/memory/api/base_vector_memory_system_api.py +116 -0
  492. src/memory/api/faiss_memory_system_api.py +228 -0
  493. src/memory/api/slot_process_api.py +827 -0
  494. src/memory/api/symbolic_memory_system_api.py +572 -0
  495. src/memory/memory_system/__init__.py +48 -0
  496. src/memory/memory_system/component_taxonomy.py +430 -0
  497. src/memory/memory_system/llm.py +437 -0
  498. src/memory/memory_system/models.py +168 -0
  499. src/memory/memory_system/user_prompt.py +418 -0
  500. src/memory/memory_system/utils.py +282 -0
  501. src/memory/memory_system/vectorstore.py +311 -0
  502. src/memory/memory_system/working_slot.py +99 -0
  503. src/pipeline/__init__.py +129 -0
  504. src/pipeline/contracts.py +531 -0
  505. src/pipeline/discipline_taxonomy.py +916 -0
  506. src/pipeline/evidence_coverage_ledger.py +1150 -0
  507. src/pipeline/evidence_refinement.py +620 -0
  508. src/pipeline/experiment_to_symbolic.py +137 -0
  509. src/pipeline/multimodal_evidence/__init__.py +28 -0
  510. src/pipeline/multimodal_evidence/capabilities.py +107 -0
  511. src/pipeline/multimodal_evidence/claims.py +135 -0
  512. src/pipeline/multimodal_evidence/contract.py +291 -0
  513. src/pipeline/multimodal_evidence/data_sh_compiler.py +505 -0
  514. src/pipeline/multimodal_evidence/handoff_binding.py +135 -0
  515. src/pipeline/multimodal_evidence/manifest.py +254 -0
  516. src/pipeline/multimodal_evidence/native_analysis.py +479 -0
  517. src/pipeline/multimodal_evidence/perception.py +277 -0
  518. src/pipeline/multimodal_evidence/query_binding.py +65 -0
  519. src/pipeline/multimodal_evidence/rendering.py +120 -0
  520. src/pipeline/multimodal_evidence/safety.py +42 -0
  521. src/pipeline/multimodal_evidence/sampling.py +69 -0
  522. src/pipeline/multimodal_evidence/service.py +148 -0
  523. src/pipeline/multimodal_evidence/survey_integration.py +460 -0
  524. src/pipeline/paper_identity.py +52 -0
  525. src/pipeline/research_design_inventory.py +338 -0
  526. src/pipeline/research_domain_resolution.py +669 -0
  527. src/pipeline/research_identity.py +548 -0
  528. src/pipeline/research_question_contract.py +646 -0
  529. src/pipeline/retrieval_lanes.py +889 -0
  530. src/pipeline/run_loop.py +862 -0
  531. src/pipeline/science_events.py +30 -0
  532. src/pipeline/science_manifests.py +602 -0
  533. src/pipeline/science_run.py +918 -0
  534. src/pipeline/science_stages.py +709 -0
  535. src/pipeline/science_workflow.py +807 -0
  536. src/pipeline/sh_cluster_projection.py +306 -0
  537. src/pipeline/sh_graph_provenance.py +648 -0
  538. src/pipeline/subhypothesis_decomposition.py +392 -0
  539. src/pipeline/subhypothesis_evidence.py +856 -0
  540. src/pipeline/survey_evidence_plan.py +858 -0
  541. src/pipeline/survey_gap_adjudication.py +647 -0
  542. src/pipeline/survey_gap_candidates.py +840 -0
  543. src/pipeline/survey_gap_ledger.py +683 -0
  544. src/pipeline/survey_gap_triage.py +401 -0
  545. src/pipeline/survey_handoff_persistence.py +481 -0
  546. src/pipeline/survey_handoff_projection.py +457 -0
  547. src/pipeline/survey_idea_handoff.py +1112 -0
  548. src/pipeline/survey_idea_loader.py +289 -0
  549. src/research_run_ids.py +11 -0
@@ -0,0 +1,167 @@
1
+ import os, torch
2
+ from sentence_transformers import SentenceTransformer, util
3
+ from utils.rich_logger import get_logger
4
+ from utils.gpu_utils import load_sentence_transformer_auto
5
+
6
+ class Database:
7
+ def __init__(self, config, work_collector):
8
+ self.config = config
9
+ self.logger = get_logger("Database")
10
+ self.work_collector = work_collector
11
+ self.valid_paper_ids = set()
12
+ self.model, self.device = load_sentence_transformer_auto(
13
+ config.ModuleInfo.WorkCollector.sentence_transformer_model,
14
+ logger=self.logger,
15
+ )
16
+ self.use_title_in_draft = True
17
+ # self.index_path = os.path.join(config.BasicInfo.cache_path, "paper_embedding_index.pt")
18
+ # self.title_index_path = os.path.join(config.BasicInfo.cache_path, "paper_title_embedding_index.pt")
19
+ self.emb_dict = None
20
+ self.title_emb_dict = None
21
+
22
+ def build_with_graph(self):
23
+ self.valid_paper_ids = set()
24
+ paper_ids = list(self.work_collector.graph_paper_ids)
25
+ self.logger.info(f"Building database with {len(paper_ids)} papers from the reference graph.")
26
+ self.build(paper_ids)
27
+
28
+ def _text_for(self, pid: str) -> str:
29
+ if not self.work_collector.expand_in_local_paper_graph:
30
+ node = self.work_collector.reference_graph.nodes.get(pid, {})
31
+ title = node.get("title", "")
32
+ abstract = node.get("abstract", "")
33
+ else:
34
+ abstract = ""
35
+ title = ""
36
+ if not abstract:
37
+ try:
38
+ title, abstract = self.work_collector.get_paper_title_abstract(pid)
39
+ except Exception as e:
40
+ self.logger.error(f"Error getting title and abstract for paper ID: {pid}: {e}")
41
+ title, abstract = "", ""
42
+
43
+ if not abstract or not title:
44
+ self.logger.warning(f"No abstract found for paper ID: {pid}")
45
+ return None, None, None
46
+ return f"Title: {title}\nAbstract: {abstract}", title, abstract
47
+
48
+ def build(self, paper_ids):
49
+ # Reset state for a clean rebuild
50
+ self.valid_paper_ids.clear()
51
+
52
+ texts = []
53
+ titles = []
54
+ valid_paper_embed_ids = []
55
+ for pid in paper_ids:
56
+ text, title_only, abstract_only = self._text_for(pid)
57
+ if text is not None:
58
+ texts.append(text)
59
+ self.valid_paper_ids.add(pid)
60
+ valid_paper_embed_ids.append(pid)
61
+ titles.append(title_only)
62
+
63
+ if not texts:
64
+ self.logger.warning("No valid texts found for embedding. Database build aborted.")
65
+ return
66
+ self.logger.info(f"Encoding {len(valid_paper_embed_ids)} paper abstracts for database.")
67
+ embs = self.model.encode(
68
+ texts,
69
+ convert_to_tensor=True,
70
+ batch_size=self.config.ModuleInfo.WorkCollector.sentence_transformer_batch_size,
71
+ show_progress_bar=True,
72
+ )
73
+ # Title-only embeddings stay aligned with the same ids
74
+ title_embs = self.model.encode(
75
+ titles,
76
+ convert_to_tensor=True,
77
+ batch_size=self.config.ModuleInfo.WorkCollector.sentence_transformer_batch_size,
78
+ show_progress_bar=False,
79
+ )
80
+
81
+ self.emb_dict = {"ids": valid_paper_embed_ids, "embs": embs.cpu()}
82
+ self.title_emb_dict = {"ids": valid_paper_embed_ids, "embs": title_embs.cpu()}
83
+
84
+ def query(self, query_text: str, top_k: int = None):
85
+ if self.emb_dict is None:
86
+ raise RuntimeError("Database not built; call build/build_with_graph first.")
87
+ if top_k is None:
88
+ top_k = self.config.ModuleInfo.Database.default_top_k
89
+ data = self.emb_dict
90
+ embs = data["embs"].to(self.device)
91
+ q = self.model.encode([query_text], convert_to_tensor=True)
92
+
93
+ scores = util.pytorch_cos_sim(q, embs)[0]
94
+
95
+ top_k = min(top_k, scores.shape[0])
96
+ vals, idxs = torch.topk(scores, k=top_k)
97
+ return [data["ids"][i] for i in idxs.cpu().tolist()], vals.cpu().tolist()
98
+
99
+ def query_titles(self, query_text: str, top_k: int = None):
100
+ """Query against the title-only embedding index."""
101
+ if self.title_emb_dict is None:
102
+ raise RuntimeError("Database not built; call build/build_with_graph first.")
103
+ if top_k is None:
104
+ top_k = self.config.ModuleInfo.Database.default_top_k
105
+ data = self.title_emb_dict
106
+ embs = data["embs"].to(self.device)
107
+ q = self.model.encode([query_text], convert_to_tensor=True)
108
+
109
+ scores = util.pytorch_cos_sim(q, embs)[0]
110
+
111
+ top_k = min(top_k, scores.shape[0])
112
+ vals, idxs = torch.topk(scores, k=top_k)
113
+ return [data["ids"][i] for i in idxs.cpu().tolist()], vals.cpu().tolist()
114
+
115
+ def query_and_text(self, query_text: str, top_k: int = None, include_paper_id: bool = False):
116
+ if top_k is None:
117
+ top_k = self.config.ModuleInfo.Database.default_top_k
118
+ paper_ids, _ = self.query(query_text, top_k=top_k)
119
+ # if self.config.BasicInfo.debug:
120
+ # self.logger.info(f"Database query for '{query_text}' returned paper IDs: {paper_ids}")
121
+ texts = ""
122
+ for pid in paper_ids:
123
+ text, _, _ = self._text_for(pid)
124
+ if not text:
125
+ continue
126
+ if self.use_title_in_draft and not include_paper_id:
127
+ texts += f"{text}\n\n"
128
+ else:
129
+ texts += f"Paper_id: {pid}\n{text}\n\n"
130
+ return texts
131
+
132
+ def resolve_title_to_paper_id(self, title_text: str, min_title_similarity: float = 0.0):
133
+ """Given a title string, return (paper_id, matched_title, similarity) if confident; raise if likely hallucination."""
134
+ if not isinstance(title_text, str) or not title_text.strip():
135
+ raise ValueError("title_text must be a non-empty string")
136
+
137
+ # Find nearest paper using the title-only index to avoid abstract influence.
138
+ paper_ids, sims = self.query_titles(title_text, top_k=1)
139
+ if not paper_ids:
140
+ raise ValueError("No papers available to match title")
141
+
142
+ top_pid = paper_ids[0]
143
+ top_sim = sims[0]
144
+
145
+ try:
146
+ matched_title, _ = self.work_collector.get_paper_title_abstract(top_pid)
147
+ matched_title = matched_title or ""
148
+ except Exception as e:
149
+ raise ValueError(f"No title found for matched paper ID {top_pid}")
150
+
151
+ # Recompute similarity between input title text and the retrieved title for a sanity check.
152
+ q_vec = self.model.encode([title_text], convert_to_tensor=True, device=self.device)
153
+ t_vec = self.model.encode([matched_title], convert_to_tensor=True, device=self.device)
154
+ title_sim = util.cos_sim(q_vec, t_vec)[0][0].item()
155
+
156
+ # if self.config.BasicInfo.debug:
157
+ # self.logger.info(
158
+ # f"Title similarity check: input '{title_text}' vs retrieved '{matched_title}' (ID {top_pid}) = {title_sim:.3f}"
159
+ # )
160
+
161
+
162
+ if title_sim < min_title_similarity:
163
+ raise ValueError(
164
+ f"Title mismatch: input '{title_text}' vs retrieved '{matched_title}' (ID {top_pid}) with similarity {title_sim:.3f} < {min_title_similarity}"
165
+ )
166
+
167
+ return top_pid, matched_title, title_sim
@@ -0,0 +1,462 @@
1
+ """Bounded, access-context-aware coordination for lawful full-text requests.
2
+
3
+ This module coordinates requests; it never manufactures an access path. It
4
+ prevents duplicate anonymous requests to the same OA URL and temporarily
5
+ remembers classified failures. The access-context generation is deliberately
6
+ part of every cache key: a future, legitimately configured TDM client must not
7
+ be blocked by an old anonymous 403.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from concurrent.futures import Future
13
+ from copy import deepcopy
14
+ from hashlib import sha256
15
+ import os
16
+ import shutil
17
+ import threading
18
+ import time
19
+ from typing import Any, Callable, Mapping
20
+ from urllib.parse import urlsplit, urlunsplit
21
+
22
+ import diskcache as dc
23
+
24
+
25
+ # v2 changes both failure lifetimes and circuit-host attribution. Isolating
26
+ # keys prevents old anonymous DOI-wide circuits from governing the new policy.
27
+ FULLTEXT_DOWNLOAD_CACHE_SCHEMA_VERSION = "fulltext_download_cache_v2"
28
+
29
+ # DOI and Handle services are redirectors, not content hosts. A publisher's
30
+ # response after a DOI redirect must never disable every later DOI lookup.
31
+ _RESOLVER_HOSTS = frozenset({"doi.org", "dx.doi.org", "hdl.handle.net"})
32
+
33
+
34
+ def normalize_download_url(value: Any) -> str:
35
+ """Normalize only the URL portions safe for identity comparison.
36
+
37
+ Query strings remain intact because signed repository URLs can legitimately
38
+ differ only by query. Fragment identifiers are never sent in HTTP
39
+ requests and would only defeat de-duplication.
40
+ """
41
+
42
+ raw = str(value or "").strip()
43
+ if not raw:
44
+ return ""
45
+ parsed = urlsplit(raw)
46
+ scheme = parsed.scheme.lower()
47
+ host = parsed.netloc.lower()
48
+ if scheme not in {"http", "https"} or not host:
49
+ return ""
50
+ return urlunsplit((scheme, host, parsed.path, parsed.query, ""))
51
+
52
+
53
+ def sanitize_url_for_storage(value: Any) -> str:
54
+ """Keep URL provenance useful without persisting signed query parameters."""
55
+
56
+ normalized = normalize_download_url(value)
57
+ if not normalized:
58
+ return ""
59
+ parsed = urlsplit(normalized)
60
+ return urlunsplit((parsed.scheme, parsed.netloc, parsed.path, "", ""))
61
+
62
+
63
+ def _has_url_query(value: Any) -> bool:
64
+ normalized = normalize_download_url(value)
65
+ return bool(normalized and urlsplit(normalized).query)
66
+
67
+
68
+ def _is_resolver_host(host: str) -> bool:
69
+ normalized = str(host or "").strip().casefold().rstrip(".")
70
+ return normalized in _RESOLVER_HOSTS
71
+
72
+
73
+ class FulltextDownloadCoordinator:
74
+ """Coordinate same-URL work and cache safe, classified acquisition state.
75
+
76
+ The coordinator is intentionally process-local for single-flight locks and
77
+ per-host semaphores. Its result cache is persistent, so completed failure
78
+ classifications can protect later runs without sharing credentials.
79
+ """
80
+
81
+ _SUCCESS_STATUS = "downloaded_response"
82
+ _LANDING_SUCCESS_STATUSES = frozenset({"resolved_to_pdf", "pdf_links_found"})
83
+
84
+ def __init__(self, cache_path: str, settings: Any, logger: Any = None) -> None:
85
+ self.settings = settings
86
+ self.logger = logger
87
+ self.access_context_generation = str(
88
+ self._setting("fulltext_access_context_generation", "anonymous-v1")
89
+ or "anonymous-v1"
90
+ ).strip() or "anonymous-v1"
91
+ self.per_host_concurrency = max(
92
+ 1, int(self._setting("fulltext_per_host_concurrency", 2) or 2)
93
+ )
94
+ self._cache = dc.Cache(os.path.join(str(cache_path), "fulltext_download_state"))
95
+ self._lock = threading.RLock()
96
+ self._inflight: dict[str, Future] = {}
97
+ self._host_semaphores: dict[str, threading.BoundedSemaphore] = {}
98
+ self._transient_counts: dict[str, int] = {}
99
+
100
+ if self._as_bool("invalidate_fulltext_failure_cache", False):
101
+ self.invalidate_failure_cache()
102
+
103
+ def _setting(self, name: str, default: Any) -> Any:
104
+ return getattr(self.settings, name, default)
105
+
106
+ def _as_bool(self, name: str, default: bool) -> bool:
107
+ value = self._setting(name, default)
108
+ return str(value).strip().casefold() not in {"0", "false", "no", "off", ""}
109
+
110
+ @staticmethod
111
+ def _url_digest(normalized_url: str) -> str:
112
+ return sha256(normalized_url.encode("utf-8")).hexdigest()
113
+
114
+ @staticmethod
115
+ def _host(normalized_url: str) -> str:
116
+ return str(urlsplit(normalized_url).netloc or "").casefold()
117
+
118
+ def _url_cache_key(self, normalized_url: str) -> str:
119
+ return (
120
+ f"url-result:{FULLTEXT_DOWNLOAD_CACHE_SCHEMA_VERSION}:"
121
+ f"{self.access_context_generation}:{self._url_digest(normalized_url)}"
122
+ )
123
+
124
+ def _host_circuit_key(self, host: str) -> str:
125
+ return (
126
+ f"host-circuit:{FULLTEXT_DOWNLOAD_CACHE_SCHEMA_VERSION}:"
127
+ f"{self.access_context_generation}:{host}"
128
+ )
129
+
130
+ def _host_denials_key(self, host: str) -> str:
131
+ return (
132
+ f"host-denials:{FULLTEXT_DOWNLOAD_CACHE_SCHEMA_VERSION}:"
133
+ f"{self.access_context_generation}:{host}"
134
+ )
135
+
136
+ def _host_semaphore(self, host: str) -> threading.BoundedSemaphore:
137
+ with self._lock:
138
+ semaphore = self._host_semaphores.get(host)
139
+ if semaphore is None:
140
+ semaphore = threading.BoundedSemaphore(self.per_host_concurrency)
141
+ self._host_semaphores[host] = semaphore
142
+ return semaphore
143
+
144
+ def _failure_ttl(self, status: str, normalized_url: str) -> int:
145
+ # Signed object-store URLs commonly expire between attempts. Holding a
146
+ # negative result for days prevents a fresh metadata lookup from ever
147
+ # being useful, so keep those failures intentionally short.
148
+ if _has_url_query(normalized_url) and status != "not_found":
149
+ return max(
150
+ 1,
151
+ int(self._setting("fulltext_failure_signed_url_ttl_seconds", 300)),
152
+ )
153
+ if status == "not_found":
154
+ return max(1, int(self._setting("fulltext_failure_404_ttl_seconds", 86400)))
155
+ if status in {"access_denied", "authentication_required"}:
156
+ return max(1, int(self._setting("fulltext_failure_access_denied_ttl_seconds", 1800)))
157
+ if status == "rate_limited":
158
+ return max(1, int(self._setting("fulltext_failure_rate_limited_ttl_seconds", 900)))
159
+ if status in {"non_pdf", "no_declared_pdf", "landing_page_only"}:
160
+ return max(1, int(self._setting("fulltext_failure_non_pdf_ttl_seconds", 21600)))
161
+ if status in {"timeout", "network_error", "transient_failure", "fetch_failed", "http_error"}:
162
+ with self._lock:
163
+ failures = self._transient_counts.get(normalized_url, 0) + 1
164
+ self._transient_counts[normalized_url] = failures
165
+ if failures <= 1:
166
+ return max(1, int(self._setting("fulltext_failure_transient_first_ttl_seconds", 60)))
167
+ return max(1, int(self._setting("fulltext_failure_transient_ttl_seconds", 180)))
168
+ return max(1, int(self._setting("fulltext_failure_transient_first_ttl_seconds", 60)))
169
+
170
+ def _success_ttl(self, kind: str) -> int:
171
+ if kind == "landing_page":
172
+ return max(1, int(self._setting("fulltext_oa_resolution_cache_ttl_seconds", 604800)))
173
+ return max(1, int(self._setting("fulltext_pdf_success_cache_ttl_seconds", 2592000)))
174
+
175
+ def _cache_entry(self, normalized_url: str) -> dict[str, Any] | None:
176
+ entry = self._cache.get(self._url_cache_key(normalized_url))
177
+ return dict(entry) if isinstance(entry, Mapping) else None
178
+
179
+ def _store_entry(
180
+ self,
181
+ normalized_url: str,
182
+ *,
183
+ kind: str,
184
+ result: Mapping[str, Any],
185
+ source_path: str = "",
186
+ successful: bool,
187
+ ) -> None:
188
+ status = str(result.get("status") or "fetch_failed")
189
+ ttl = self._success_ttl(kind) if successful else self._failure_ttl(status, normalized_url)
190
+ safe_result = self._cache_safe_result(result)
191
+ requires_refetch = (
192
+ kind == "landing_page"
193
+ and any(
194
+ _has_url_query(candidate.get("url"))
195
+ for candidate in result.get("pdf_candidates") or []
196
+ if isinstance(candidate, Mapping)
197
+ )
198
+ )
199
+ payload = {
200
+ "schema_version": FULLTEXT_DOWNLOAD_CACHE_SCHEMA_VERSION,
201
+ "kind": kind,
202
+ "status": status,
203
+ "successful": bool(successful),
204
+ "source_path": str(source_path or ""),
205
+ "result": safe_result,
206
+ "requires_refetch": requires_refetch,
207
+ "cached_at": time.time(),
208
+ "access_context_generation": self.access_context_generation,
209
+ }
210
+ self._cache.set(self._url_cache_key(normalized_url), payload, expire=ttl)
211
+
212
+ @staticmethod
213
+ def _cache_safe_result(value: Any) -> Any:
214
+ if isinstance(value, Mapping):
215
+ safe: dict[str, Any] = {}
216
+ for key, item in value.items():
217
+ if key in {"requested_url", "final_url", "url"}:
218
+ safe[key] = sanitize_url_for_storage(item)
219
+ elif key == "error":
220
+ safe[key] = "redacted"
221
+ else:
222
+ safe[key] = FulltextDownloadCoordinator._cache_safe_result(item)
223
+ return safe
224
+ if isinstance(value, (list, tuple)):
225
+ return [FulltextDownloadCoordinator._cache_safe_result(item) for item in value]
226
+ return deepcopy(value)
227
+
228
+ def _host_circuit_result(self, normalized_url: str) -> dict[str, Any] | None:
229
+ host = self._host(normalized_url)
230
+ # Ignore legacy circuits produced by the pre-v2 DOI attribution logic
231
+ # as well as any future accidental resolver circuit.
232
+ if _is_resolver_host(host):
233
+ return None
234
+ circuit = self._cache.get(self._host_circuit_key(host))
235
+ if not isinstance(circuit, Mapping):
236
+ return None
237
+ retry_after = max(0, int(float(circuit.get("until", 0)) - time.time()))
238
+ return {
239
+ "downloaded": False,
240
+ "status": "host_circuit_open",
241
+ "requested_url": normalized_url,
242
+ "final_url": normalized_url,
243
+ "http_status": None,
244
+ "content_type": "",
245
+ "bytes_written": 0,
246
+ "host": host,
247
+ "retry_after_seconds": retry_after,
248
+ "cache_hit": True,
249
+ }
250
+
251
+ def _record_access_denial(self, normalized_url: str) -> None:
252
+ host = self._host(normalized_url)
253
+ if _is_resolver_host(host):
254
+ return
255
+ now = time.time()
256
+ window = max(1, int(self._setting("fulltext_host_denial_window_seconds", 600)))
257
+ threshold = max(1, int(self._setting("fulltext_host_denial_threshold", 2)))
258
+ denial_key = self._host_denials_key(host)
259
+ existing = self._cache.get(denial_key) or []
260
+ records = [
261
+ item
262
+ for item in existing
263
+ if isinstance(item, Mapping)
264
+ and float(item.get("timestamp", 0)) >= now - window
265
+ and str(item.get("url_digest") or "")
266
+ ]
267
+ url_digest = self._url_digest(normalized_url)
268
+ if not any(str(item.get("url_digest")) == url_digest for item in records):
269
+ records.append({"url_digest": url_digest, "timestamp": now})
270
+ self._cache.set(denial_key, records, expire=window)
271
+ if len(records) < threshold:
272
+ return
273
+ ttl = max(1, int(self._setting("fulltext_host_circuit_ttl_seconds", 3600)))
274
+ self._cache.set(
275
+ self._host_circuit_key(host),
276
+ {"until": now + ttl, "denial_count": len(records)},
277
+ expire=ttl,
278
+ )
279
+ if self.logger is not None:
280
+ self.logger.warning(
281
+ "Full-text host circuit opened host=%s access_context=%s denials=%s ttl_seconds=%s.",
282
+ host,
283
+ self.access_context_generation,
284
+ len(records),
285
+ ttl,
286
+ )
287
+
288
+ @staticmethod
289
+ def _copy_success(source_path: str, destination_path: str, validator: Callable[[str], bool]) -> bool:
290
+ if not source_path or not destination_path or not os.path.isfile(source_path):
291
+ return False
292
+ try:
293
+ if not validator(source_path):
294
+ return False
295
+ if os.path.abspath(source_path) != os.path.abspath(destination_path):
296
+ os.makedirs(os.path.dirname(destination_path), exist_ok=True)
297
+ shutil.copyfile(source_path, destination_path)
298
+ return bool(validator(destination_path))
299
+ except OSError:
300
+ return False
301
+
302
+ def _read_cached_result(
303
+ self,
304
+ normalized_url: str,
305
+ *,
306
+ kind: str,
307
+ destination_path: str | None,
308
+ validator: Callable[[str], bool] | None,
309
+ ) -> dict[str, Any] | None:
310
+ entry = self._cache_entry(normalized_url)
311
+ if entry is None or entry.get("kind") != kind:
312
+ return None
313
+ if entry.get("requires_refetch"):
314
+ return None
315
+ result = deepcopy(dict(entry.get("result") or {}))
316
+ if not entry.get("successful"):
317
+ result["cache_hit"] = True
318
+ result["cache_state"] = "failure"
319
+ return result
320
+ if kind == "pdf":
321
+ if destination_path is None or validator is None:
322
+ return None
323
+ if not self._copy_success(str(entry.get("source_path") or ""), destination_path, validator):
324
+ self._cache.delete(self._url_cache_key(normalized_url))
325
+ return None
326
+ result["cache_hit"] = True
327
+ result["cache_state"] = "success"
328
+ if kind == "pdf":
329
+ result["downloaded"] = True
330
+ return result
331
+
332
+ def execute(
333
+ self,
334
+ *,
335
+ url: str,
336
+ kind: str,
337
+ operation: Callable[[], Mapping[str, Any]],
338
+ destination_path: str | None = None,
339
+ validator: Callable[[str], bool] | None = None,
340
+ is_success: Callable[[Mapping[str, Any]], bool] | None = None,
341
+ ) -> dict[str, Any]:
342
+ """Run one request once per URL/context, respecting the host semaphore."""
343
+
344
+ normalized_url = normalize_download_url(url)
345
+ if not normalized_url:
346
+ return {
347
+ "downloaded": False,
348
+ "status": "invalid_url",
349
+ "requested_url": str(url or ""),
350
+ "final_url": str(url or ""),
351
+ "http_status": None,
352
+ "content_type": "",
353
+ "bytes_written": 0,
354
+ }
355
+
356
+ cached = self._read_cached_result(
357
+ normalized_url,
358
+ kind=kind,
359
+ destination_path=destination_path,
360
+ validator=validator,
361
+ )
362
+ if cached is not None:
363
+ return cached
364
+ circuit = self._host_circuit_result(normalized_url)
365
+ if circuit is not None:
366
+ return circuit
367
+
368
+ singleflight_key = f"{kind}:{self.access_context_generation}:{normalized_url}"
369
+ leader = False
370
+ with self._lock:
371
+ future = self._inflight.get(singleflight_key)
372
+ if future is None:
373
+ future = Future()
374
+ self._inflight[singleflight_key] = future
375
+ leader = True
376
+
377
+ if leader:
378
+ try:
379
+ host = self._host(normalized_url)
380
+ with self._host_semaphore(host):
381
+ result = dict(operation() or {})
382
+ result.setdefault("requested_url", normalized_url)
383
+ result.setdefault("final_url", normalized_url)
384
+ result.setdefault("host", host)
385
+ succeeded = bool(is_success(result)) if is_success else bool(result.get("downloaded"))
386
+ source_path = destination_path if succeeded and kind == "pdf" else ""
387
+ self._store_entry(
388
+ normalized_url,
389
+ kind=kind,
390
+ result=result,
391
+ source_path=source_path,
392
+ successful=succeeded,
393
+ )
394
+ if str(result.get("status") or "") == "access_denied":
395
+ # Redirectors are not the host that denied access. Record
396
+ # the final content host when it is available, so a 403
397
+ # from one publisher cannot open a DOI-wide circuit.
398
+ final_url = normalize_download_url(result.get("final_url"))
399
+ self._record_access_denial(final_url or normalized_url)
400
+ future.set_result(deepcopy(result))
401
+ except Exception as exc: # Preserve a classified result for waiters.
402
+ result = {
403
+ "downloaded": False,
404
+ "status": "fetch_failed",
405
+ "requested_url": normalized_url,
406
+ "final_url": normalized_url,
407
+ "http_status": None,
408
+ "content_type": "",
409
+ "bytes_written": 0,
410
+ "error": type(exc).__name__,
411
+ }
412
+ self._store_entry(
413
+ normalized_url,
414
+ kind=kind,
415
+ result=result,
416
+ successful=False,
417
+ )
418
+ future.set_result(deepcopy(result))
419
+ finally:
420
+ with self._lock:
421
+ self._inflight.pop(singleflight_key, None)
422
+ else:
423
+ result = deepcopy(dict(future.result()))
424
+ result["singleflight_shared"] = True
425
+ if kind == "pdf":
426
+ cached_after_wait = self._read_cached_result(
427
+ normalized_url,
428
+ kind=kind,
429
+ destination_path=destination_path,
430
+ validator=validator,
431
+ )
432
+ if cached_after_wait is not None:
433
+ cached_after_wait["singleflight_shared"] = True
434
+ return cached_after_wait
435
+ result["downloaded"] = False
436
+ result["status"] = "shared_result_unavailable"
437
+ return result
438
+
439
+ result = deepcopy(dict(future.result()))
440
+ result.setdefault("cache_hit", False)
441
+ result.setdefault("singleflight_shared", False)
442
+ return result
443
+
444
+ def invalidate_url(self, url: str) -> None:
445
+ normalized_url = normalize_download_url(url)
446
+ if normalized_url:
447
+ self._cache.delete(self._url_cache_key(normalized_url))
448
+
449
+ def invalidate_failure_cache(self) -> None:
450
+ """Explicitly clear only this context's cached failures and circuits."""
451
+
452
+ prefix = f"{FULLTEXT_DOWNLOAD_CACHE_SCHEMA_VERSION}:{self.access_context_generation}:"
453
+ for key in list(self._cache.iterkeys()):
454
+ text = str(key)
455
+ if text.startswith("url-result:") and prefix in text:
456
+ entry = self._cache.get(key)
457
+ if isinstance(entry, Mapping) and not entry.get("successful"):
458
+ self._cache.delete(key)
459
+ elif text.startswith("host-circuit:") and prefix in text:
460
+ self._cache.delete(key)
461
+ elif text.startswith("host-denials:") and prefix in text:
462
+ self._cache.delete(key)