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,2194 @@
1
+ import re
2
+ import requests
3
+ from typing import List, Optional, Dict, Tuple
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ import json
8
+ from concurrent.futures import ThreadPoolExecutor, as_completed
9
+ from tqdm import tqdm
10
+
11
+ from typing import Optional
12
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
13
+ from utils.rich_logger import get_logger
14
+ # from utils.rich_logger import get_logger
15
+ from utils.api_call import ChatAgent
16
+ from utils.config_utils import merge_with_default_survey_config
17
+ from modules.work_collector import WorkCollector
18
+ from modules.pseudo_reviser import PseudoReviser
19
+ from modules.pseudo_creator import PseudoWriter
20
+ from modules.code_report_generator import CodeReportGenerator
21
+ from utils.utils import extract_json
22
+ from utils.repo_utils import format_repo_structure
23
+ import hydra
24
+
25
+
26
+ MAIN_AND_CORE_FILTER = """
27
+ You are an assistant that classifies whether a file chunk is a *core implementation* for the main algorithm/architecture
28
+ of the repository of a paper titled {paper_title}.
29
+
30
+ Context:
31
+ - paper title and abstract:
32
+ {paper_title}
33
+ {paper_abstract}
34
+
35
+ - repository structure:
36
+ {repo_structure}
37
+ - file_path: {file_path}
38
+ - file chunk:
39
+ {chunk_text}
40
+ (chunk end)
41
+
42
+ TASK 1: Classify Core Code
43
+ Rules for is_core:
44
+ - If the chunk contains core algorithm implementations, model definitions (class names like Model, Transformer, UNet, forward/ call methods),
45
+ important data pipeline steps, or central algorithmic logic => is_core true.
46
+ - If it is helper utils, CI configs, docs, tests, or setup scripts => likely not core.
47
+
48
+ Scoring Standard for "score" (1-10):
49
+ [Score 1-2] Pure Boilerplate & Config (is_core: false)
50
+ Pure configurations, documentation, environment setup, CI/CD scripts, basic `argparse`, or trivial dependency management (e.g., requirements.txt, Dockerfiles, README, completely empty init files).
51
+ [Score 3-4] Generic Utilities & Helpers (is_core: false)
52
+ Generic software engineering tools that do not reflect the paper's algorithm. Examples: logging (Logger), checkpoint saving/loading, visualization/plotting scripts, standard file I/O, or standard metric calculations (e.g., generic Accuracy/MSE).
53
+ [Score 5-6] Standard Pipelines & Training Loops (is_core: false)
54
+ Standard Deep Learning skeletons. Examples: standard PyTorch/TensorFlow training loops (e.g., standard `for epoch in epochs`, basic optimizer steps), generic `Dataset`/`DataLoader` definitions, or standard text/image preprocessing. While necessary to run the code, they lack the paper's specific novelties.
55
+ [Score 7-8] Key Components & Novel Sub-modules (is_core: true)
56
+ Code implementing specific, secondary innovations from the paper. Examples: custom loss functions, novel data sampling/augmentation strategies, customized attention mechanisms, or specific sub-network blocks (e.g., a custom Adapter or ResBlock variation specifically proposed in the paper).
57
+ [Score 9-10] Absolute Core & Main Algorithm (is_core: true)
58
+ The absolute "soul" of the paper. Main model class definitions (e.g., `class ProposedModel`), the complete `forward` or `call` methods that tie the novel architecture together, or direct code translations of the paper's primary mathematical equations and pseudocode. If this chunk is removed, the paper's main contribution ceases to exist.
59
+
60
+ TASK 2: Identify Main Entry Point Code (is_main_code)
61
+ This is a SEPARATE classification from is_core. Even if is_core is false, a file can be is_main_code if it contains:
62
+ - Functions that drive the main execution flow: main()
63
+ - Scripts that orchestrate the entire pipeline
64
+ - The skeleton of the pipeline
65
+
66
+ SCORING (main_score):
67
+ Assign a `main_score` from 0 to 10 indicating how central this file is to the overall execution flow:
68
+ - 9-10: Unambiguous top-level main entry point (orchestrates the entire pipeline).
69
+ - 7-8: High-level orchestrator for a major subsystem, but maybe not the top-most script.
70
+ - 0-6: Sub-modules, core algorithm implementations (without orchestration), utilities, data loaders, etc. (For these, is_main_code MUST be false).
71
+
72
+ IMPORTANT:
73
+ is_main_code is about WHETHER THIS FILE IS AN ENTRY POINT that orchestrates the algorithm execution or is the skeleton script of the pipeline, NOT about whether it contains the core algorithm implementation.
74
+ I will use is_main_code True file to build the skeleton of the repository pseudocode.
75
+
76
+ Output:
77
+ - Return EXACTLY a JSON object with keys for TASK 1 and TASK 2:
78
+ - is_core: boolean
79
+ - score: between 0 to 10 (core level)
80
+ - reason: short string explanation for core classification (one or two sentences)
81
+ - is_main_code: boolean (separate from is_core)
82
+ - main_score: between 0 to 10 (main level)
83
+ - main_reason: short string explanation for main code classification (one or two sentences)
84
+
85
+ - Generate JSON directly without any other things.
86
+ """
87
+
88
+ CORE_FILTER = """
89
+ You are an assistant that classifies whether a file chunk is a *core implementation* for the main algorithm/architecture
90
+ of the repository of a paper titled {paper_title}.
91
+
92
+ Context:
93
+ - paper title and abstract:
94
+ {paper_title}
95
+ {paper_abstract}
96
+
97
+ - repository structure:
98
+ {repo_structure}
99
+ - file_path: {file_path}
100
+ - file chunk:
101
+ {chunk_text}
102
+ (chunk end)
103
+
104
+ TASK: Classify Core Code
105
+ Rules for is_core:
106
+ - If the chunk contains core algorithm implementations, model definitions (class names like Model, Transformer, UNet, forward/ call methods),
107
+ important data pipeline steps, or central algorithmic logic => is_core true.
108
+ - If it is helper utils, CI configs, docs, tests, or setup scripts => likely not core.
109
+
110
+ Scoring Standard for "score" (1-10):
111
+ [Score 1-2] Pure Boilerplate & Config (is_core: false)
112
+ Pure configurations, documentation, environment setup, CI/CD scripts, basic `argparse`, or trivial dependency management (e.g., requirements.txt, Dockerfiles, README, completely empty init files).
113
+ [Score 3-4] Generic Utilities & Helpers (is_core: false)
114
+ Generic software engineering tools that do not reflect the paper's algorithm. Examples: logging (Logger), checkpoint saving/loading, visualization/plotting scripts, standard file I/O, or standard metric calculations (e.g., generic Accuracy/MSE).
115
+ [Score 5-6] Standard Pipelines & Training Loops (is_core: false)
116
+ Standard Deep Learning skeletons. Examples: standard PyTorch/TensorFlow training loops (e.g., standard `for epoch in epochs`, basic optimizer steps), generic `Dataset`/`DataLoader` definitions, or standard text/image preprocessing. While necessary to run the code, they lack the paper's specific novelties.
117
+ [Score 7-8] Key Components & Novel Sub-modules (is_core: true)
118
+ Code implementing specific, secondary innovations from the paper. Examples: custom loss functions, novel data sampling/augmentation strategies, customized attention mechanisms, or specific sub-network blocks (e.g., a custom Adapter or ResBlock variation specifically proposed in the paper).
119
+ [Score 9-10] Absolute Core & Main Algorithm (is_core: true)
120
+ The absolute "soul" of the paper. Main model class definitions (e.g., `class ProposedModel`), the complete `forward` or `call` methods that tie the novel architecture together, or direct code translations of the paper's primary mathematical equations and pseudocode. If this chunk is removed, the paper's main contribution ceases to exist.
121
+
122
+ Output:
123
+ - Return EXACTLY a JSON object with keys for TASK 1 and TASK 2:
124
+ - is_core: boolean
125
+ - score: between 0 to 10 (core level)
126
+ - reason: short string explanation for core classification (one or two sentences)
127
+ - Output json example:
128
+ {{
129
+ "is_core": False,
130
+ "score": 5,
131
+ "reason": ".....",
132
+ }}
133
+ - Generate JSON directly without any other things.
134
+ """
135
+
136
+ MAIN_FILTER = """
137
+ You are an assistant that classifies whether a file chunk is the main code for the algorithm/architecture
138
+ of the repository of a paper titled {paper_title}.
139
+
140
+ Context:
141
+ - paper title and abstract:
142
+ {paper_title}
143
+ {paper_abstract}
144
+
145
+ - repository structure:
146
+ {repo_structure}
147
+ - file_path: {file_path}
148
+ - file chunk:
149
+ {chunk_text}
150
+ (chunk end)
151
+
152
+
153
+ TASK: Identify Main Entry Point Code (is_main_code) and Assign a Score
154
+ A file can be is_main_code if it contains:
155
+ - Functions that drive the main execution flow: main()
156
+ - Scripts that orchestrate the entire pipeline
157
+ - The skeleton of the pipeline
158
+
159
+ IMPORTANT STRICTNESS REQUIREMENT (CRITICAL):
160
+ You must be CAUTIOUS when setting `is_main_code` to true.
161
+ is_main_code is about WHETHER THIS FILE IS AN ENTRY POINT that orchestrates the algorithm execution or is the skeleton script of the pipeline, NOT about whether it contains the core algorithm implementation.
162
+ I will concatenate ALL `is_main_code=True` files at once to build the SKELETON of the repository pseudocode. Too many files or including sub-modules/utilities will exceed the context window.
163
+ - Only the absolute top-level entry points (the "conductor" of the orchestra, e.g., main.py, train.py) should be marked True.
164
+ - When in doubt, or if the file is just a module called by the main script, set `is_main_code` to false.
165
+ - Be cautious about marking the data-collect code and evaluation code as main code because sometimes they are unrelated to the core pipeline of repo.
166
+
167
+ SCORING (main_score):
168
+ Assign a `main_score` from 0 to 10 indicating how central this file is to the overall execution flow:
169
+ - 9-10: Unambiguous top-level main entry point (orchestrates the entire pipeline).
170
+ - 7-8: High-level orchestrator for a major subsystem, but maybe not the top-most script.
171
+ - 0-6: Sub-modules, core algorithm implementations (without orchestration), utilities, data loaders, etc. (For these, is_main_code MUST be false).
172
+
173
+ Output:
174
+ - Return EXACTLY a JSON object with the following keys:
175
+ - is_main_code: boolean (Extremely strict! Only true for the absolute main skeleton/entry point)
176
+ - main_score: integer (0-10, representing the confidence and centrality of the file)
177
+ - main_reason: short string explanation for the classification and score (one or two sentences)
178
+
179
+ - Generate JSON directly without any markdown formatting (do not use ```json) or other text.
180
+ """
181
+
182
+ PSEUDOCODE_WRITER = """
183
+ Analyze the following code file from a research paper repository.
184
+
185
+ [File Info]
186
+ - File: {file_path}
187
+
188
+ - Core File
189
+ Core file means the file contains core algorithm implementations, model definitions, important data pipeline steps, or central logic.
190
+ Core score measure the core level of the file.
191
+ You need to focus on the key points of the core files and reflect them in the analysis and pseudocode
192
+ -- is_core_file: {is_core_file}
193
+ -- Core Score: {core_score}/10 (higher = more central to paper's contribution)
194
+ -- Reason for the score: {reason}
195
+
196
+ - Main File
197
+ Main file means the file orchestrates the algorithm execution or is the skeleton script of the pipeline.
198
+ The pseudocode for main file will serve as the foundation pseudocode when building pseudocode for the whole repo.
199
+ You need to clearly present the logic and structure of the main code in pseudocode and analysis.
200
+ -- is_main_file: {is_main_code}
201
+ -- Main Score: {main_score}/10
202
+ -- Reason for is_main_file: {main_reason}
203
+
204
+
205
+ [Paper Context]
206
+ Title: {paper_title}
207
+ Abstract: {paper_abstract}
208
+
209
+ [Code]
210
+ {file_content}
211
+
212
+ ---
213
+
214
+ Generate a concise analysis with:
215
+ 1. One-sentence summary of what this file does
216
+ 2. Pseudocode (algorithm-style, focus on core logic, skip boilerplate)
217
+ 3. Any additional insights you think are valuable (be concise)
218
+ """
219
+
220
+ # Template for generating project pseudocode from main files
221
+ PROJECT_PSEUDOCODE_FROM_MAIN = """
222
+ You are an expert at analyzing research paper repositories and creating high-level pseudocode.
223
+
224
+ Your task is to create a project-level pseudocode framework based on the main entry point files of a repository.
225
+
226
+ [Paper Context]
227
+ Title: {paper_title}
228
+ Abstract: {paper_abstract}
229
+
230
+ [Repository Structure]
231
+ {repo_structure}
232
+
233
+ [Main Files Pseudocode and Analysis]
234
+ The following are the main entry point files, ordered by their main_score (higher = more central to the overall execution flow):
235
+ {main_files_pseudocode}
236
+
237
+ ---
238
+
239
+ TASK: Create a project-level pseudocode framework
240
+
241
+ Based on the main files above, create a comprehensive project pseudocode that:
242
+ 1. Shows the overall execution flow and pipeline structure
243
+ 2. Identifies key components and their interactions
244
+ 3. Serves as a foundation that can be refined with core implementation details
245
+
246
+ Requirements:
247
+ - Structure the pseudocode to reflect the main execution flow.
248
+ - You should build a well structed pseudocode with clear logic rather than simply listing pseudocode form main file.
249
+ - Use clear section divisions (e.g., for different phases like initialization, training, evaluation)
250
+ - The section number should be no more than 5.
251
+ - Do NOT include actual code - use algorithm-style pseudocode
252
+ - You are encourage to annotate file name (source code or called code) parsed by <> besides the corresponding pseudocode.
253
+ - You can also refine the analysis after pseudocode but must keep conciseness.
254
+
255
+ Output format:
256
+ Provide a clear, structured pseudocode with section headers, explanations. and concise analysis.
257
+ """
258
+
259
+ # Template for refining project pseudocode with core files (batch mode)
260
+ REFINE_PROJECT_PSEUDOCODE_WITH_CORE = """
261
+ You are an expert at analyzing research paper repositories and refining pseudocode.
262
+
263
+ Your task is to refine the existing project pseudocode by incorporating core implementation details.
264
+
265
+ [Paper Context]
266
+ Title: {paper_title}
267
+ Abstract: {paper_abstract}
268
+
269
+ [Repository Structure]
270
+ {repo_structure}
271
+
272
+ [Current Project Pseudocode Framework]
273
+ {current_pseudocode}
274
+
275
+ [Core Files Batch - Pseudocode and Analysis]
276
+ The following is a batch of core implementation files (with their pseudocode and analysis):
277
+ {core_files_pseudocode}
278
+
279
+ ---
280
+
281
+ TASK: Refine the project pseudocode
282
+
283
+ Incorporate the core implementation details from the above files into the existing project pseudocode:
284
+ 1. Add specific algorithm implementations where relevant
285
+ 2. Update function/class definitions with actual logic from core files
286
+ 3. Fill in any placeholder sections with concrete details
287
+ 4. Maintain the overall structure while adding depth
288
+
289
+ Requirements:
290
+ - Preserve the existing structure and flow
291
+ - Add meaningful implementation details from the core files
292
+ - Focus on the most important details that contribute to understanding the paper's contribution
293
+ - Keep the pseudocode at a reasonable length - if it gets too long, prioritize the most important additions
294
+ - You should build a well structed pseudocode with clear logic rather than simply listing pseudocode from core file.
295
+ - Use clear section divisions (e.g., for different phases like initialization, training, evaluation)
296
+ - You are encourage to annotate file name (source code or called code) parsed by <> besides the corresponding pseudocode.
297
+ - Keep the pseudocode concise. Only add necessary content to the existing pseudocode. The section number should be no more than 5.
298
+ - You can also refine the analysis after pseudocode but must keep conciseness.
299
+
300
+ Output format:
301
+ Provide the refined project pseudocode with the new details incorporated.
302
+ """
303
+
304
+ class CodeCollector:
305
+ def __init__(self, config):
306
+ self.logger = get_logger("CodeCollector")
307
+ self.pwc_api_base = "https://paperswithcode.com/api/v1"
308
+ # Use absolute path to avoid issues with relative paths when script runs from different directories
309
+ script_dir = os.path.dirname(os.path.abspath(__file__)) # modules/
310
+ project_root = os.path.dirname(script_dir) # deep-survey/
311
+ self.repo_cache_path = os.path.join(project_root, "database", "code_repo_cache")
312
+ self.config = config
313
+ os.makedirs(self.repo_cache_path, exist_ok=True)
314
+
315
+ def extract_code_links(self, paper_id: str, paper_markdown: str, valid_num: int = 2) -> List[str]:
316
+ """
317
+ Extracts GitHub repository links for a given paper using PapersWithCode API
318
+ and Markdown Regex parsing.
319
+ """
320
+ self.logger.info(f"Attempting to extract code links for Paper ID: {paper_id}")
321
+ extracted_links = set()
322
+
323
+ if paper_markdown:
324
+ regex_links = self._get_links_from_markdown(paper_markdown)
325
+ if regex_links:
326
+ self.logger.info(f"Found {len(regex_links)} links via Markdown Regex.")
327
+ extracted_links.update(regex_links)
328
+
329
+ # Clean and deduplicate
330
+ cleaned_links = {self._clean_github_url(url) for url in extracted_links if url}
331
+
332
+ # Filter for valid GitHub repos (e.g., ignoring 'https://github.com/microsoft' without a repo name)
333
+ valid_repos = [url for url in cleaned_links if len(url.strip("/").split("/")) >= 5]
334
+
335
+ if not valid_repos:
336
+ self.logger.warning(f"No valid GitHub links found for Paper ID: {paper_id}")
337
+
338
+ valid_repos = [valid_repos[i] for i in range(min(valid_num,len(valid_repos)))]
339
+ return valid_repos
340
+
341
+ ### the paperwithcode api has broken
342
+ def _get_links_from_paperswithcode(self, paper_id: str) -> List[str]:
343
+ """
344
+ Queries the PapersWithCode API using an ArXiv ID.
345
+ """
346
+ # Clean the ID in case it has 'arxiv:' prefix or version numbers (e.g., '2408.08464v1')
347
+ clean_id = re.sub(r"(?i)^arxiv:?", "", paper_id).split('v')[0]
348
+
349
+ try:
350
+ # Step 1: Search for the paper to get the internal PWC ID
351
+ search_url = f"{self.pwc_api_base}/papers/?arxiv_id={clean_id}"
352
+ response = requests.get(search_url, timeout=10)
353
+
354
+ if response.status_code != 200:
355
+ return []
356
+
357
+ data = response.json()
358
+ if not data.get("results"):
359
+ return []
360
+
361
+ pwc_paper_id = data["results"][0]["id"]
362
+
363
+ # Step 2: Fetch repositories associated with that PWC ID
364
+ repo_url = f"{self.pwc_api_base}/papers/{pwc_paper_id}/repositories/"
365
+ repo_response = requests.get(repo_url, timeout=10)
366
+
367
+ if repo_response.status_code == 200:
368
+ repo_data = repo_response.json()
369
+ # Ensure it's marked as official if possible, but return all linked repos
370
+ return [repo["url"] for repo in repo_data.get("results", []) if "github.com" in repo["url"]]
371
+
372
+ except Exception as e:
373
+ self.logger.error(f"PapersWithCode API failed for {paper_id}: {e}")
374
+
375
+ return []
376
+
377
+ def _get_links_from_markdown(self, markdown_text: str) -> List[str]:
378
+ """
379
+ Extracts GitHub URLs from raw markdown using Regex.
380
+ Matches formats like https://github.com/user/repo
381
+
382
+ Note: Content after # References section (case-insensitive) is excluded
383
+ to avoid extracting GitHub links from the bibliography.
384
+ """
385
+ # Remove content after # References (case-insensitive)
386
+ # Match # References or # REFERENCES or # references, etc.
387
+ references_pattern = r"(?i)^#\s*references.*$"
388
+ match = re.search(references_pattern, markdown_text, re.MULTILINE)
389
+
390
+ if match:
391
+ markdown_text = markdown_text[:match.start()]
392
+ self.logger.info("Removed content after # References section before extracting links")
393
+
394
+ # Pattern looks for github.com/ followed by two path segments (user/repo)
395
+ # It handles optional trailing slashes but stops at whitespace, parentheses, or brackets.
396
+ pattern = r"https?://github\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+"
397
+ matches = re.findall(pattern, markdown_text)
398
+ return matches
399
+
400
+ def _clean_github_url(self, url: str) -> str:
401
+ """
402
+ Cleans trailing punctuation that might get caught in the regex from markdown parsing.
403
+ """
404
+ # Remove trailing punctuation like dots, commas, or closing brackets
405
+ url = re.sub(r"[.,;)\]]+$", "", url.strip())
406
+
407
+ # Strip .git extension if present
408
+ if url.endswith(".git"):
409
+ url = url[:-4]
410
+
411
+ return url
412
+
413
+ def _extract_repo_name(self, repo_url: str):
414
+ repo_name = repo_url.rstrip("/").split("/")[-1]
415
+
416
+ # remove .git if present
417
+ if repo_name.endswith(".git"):
418
+ repo_name = repo_name[:-4]
419
+
420
+ return repo_name
421
+
422
+
423
+ def _clone_repo(self, repo_url: str, depth: int = 1, max_retry: int = 3, use_token: bool = False) -> Optional[str]:
424
+ """
425
+ Clone a GitHub repo into base_dir.
426
+
427
+ Args:
428
+ repo_url: GitHub repository url
429
+ base_dir: directory where repos are stored
430
+ depth: git clone depth (default shallow clone)
431
+
432
+ Returns:
433
+ local repo path if success, otherwise None
434
+ """
435
+ repo_name = self._extract_repo_name(repo_url)
436
+
437
+ target_path = os.path.join(self.repo_cache_path, repo_name)
438
+
439
+ # skip if already cloned
440
+ if os.path.exists(target_path):
441
+ self.logger.info(f"Repo already exists: {target_path}")
442
+ return target_path
443
+
444
+ os.makedirs(self.repo_cache_path, exist_ok=True)
445
+
446
+ clone_url = repo_url
447
+ # Check if GitHub token is configured
448
+ github_token = getattr(self.config.ModuleInfo.CodeAnalysis.CodeCollector, 'github_token', "") if hasattr(self.config, 'ModuleInfo') else ""
449
+ clone_timeout = getattr(self.config.ModuleInfo.CodeAnalysis.CodeCollector, 'clone_timeout', 300) if hasattr(self.config, 'ModuleInfo') else 300
450
+ if github_token and "github.com" in repo_url.lower() and use_token:
451
+ # Replace https://github.com/ with https://x-access-token@github.com/
452
+ # This allows authentication without interactive prompts
453
+ clone_url = repo_url.replace("https://github.com/", f"https://x-access-token:{github_token}@github.com/")
454
+ clone_url = clone_url.replace("http://github.com/", f"http://x-access-token:{github_token}@github.com/")
455
+ self.logger.info(f"Using GitHub token authentication for cloning")
456
+ # Convert HTTPS URL to use token authentication if token is provided
457
+
458
+ retry = 0
459
+ while retry <= max_retry:
460
+ try:
461
+ cmd = [
462
+ "git",
463
+ "clone",
464
+ "--depth",
465
+ str(depth),
466
+ "--quiet",
467
+ clone_url,
468
+ target_path
469
+ ]
470
+
471
+ # Set stdin to DEVNULL to prevent interactive prompts (e.g., username/password)
472
+ # This ensures the process doesn't hang waiting for user input
473
+
474
+ # Create environment with GIT_TERMINAL_PROMPT=0 to disable Git's terminal prompts
475
+ # Also disable credential helpers to prevent any authentication prompts
476
+ env = os.environ.copy()
477
+ env["GIT_TERMINAL_PROMPT"] = "0"
478
+
479
+ # Ensure proxy environment variables are inherited
480
+ # These are commonly set by VPN or system proxy configuration
481
+ proxy_vars = [
482
+ "http_proxy", "HTTP_PROXY",
483
+ "https_proxy", "HTTPS_PROXY",
484
+ "no_proxy", "NO_PROXY",
485
+ "all_proxy", "ALL_PROXY",
486
+ "git_proxy", "GIT_PROXY",
487
+ ]
488
+ for var in proxy_vars:
489
+ if var in os.environ:
490
+ env[var] = os.environ[var]
491
+
492
+ # Also check for VPN-specific proxy settings (e.g., from Clash, V2Ray, etc.)
493
+ vpn_proxy_vars = [
494
+ "VPN_HTTP_PROXY", "VPN_HTTPS_PROXY",
495
+ "CLASH_HTTP_PROXY", "CLASH_HTTPS_PROXY",
496
+ "V2RAY_HTTP_PROXY", "V2RAY_HTTPS_PROXY",
497
+ ]
498
+ for var in vpn_proxy_vars:
499
+ if var in os.environ:
500
+ env[var] = os.environ[var]
501
+
502
+ result = subprocess.run(
503
+ cmd,
504
+ check=True,
505
+ stdin=subprocess.DEVNULL,
506
+ stdout=subprocess.PIPE,
507
+ stderr=subprocess.PIPE,
508
+ timeout=clone_timeout, # Configurable timeout (default 5 minutes)
509
+ env=env,
510
+ cwd=self.repo_cache_path
511
+ )
512
+
513
+ # Verify clone success: check if .git directory exists
514
+ git_dir = os.path.join(target_path, ".git")
515
+ if os.path.exists(git_dir) and os.path.isdir(git_dir):
516
+ self.logger.info(f"Cloned repo to {target_path}")
517
+ return target_path
518
+ else:
519
+ # Clone command succeeded but .git not found - clone may have failed silently
520
+ self.logger.warning(f"Clone command completed but .git not found in {target_path}, treating as failure")
521
+ # Clean up and retry
522
+ if os.path.exists(target_path) and os.path.isdir(target_path):
523
+ import shutil
524
+ try:
525
+ shutil.rmtree(target_path)
526
+ except Exception:
527
+ pass
528
+ raise ValueError(f"Clone verification failed for {clone_url}")
529
+
530
+ except subprocess.TimeoutExpired as e:
531
+ retry += 1
532
+ if retry <= max_retry:
533
+ self.logger.warning(f"Clone repo {clone_url} timed out: {e}, retrying for {retry}/{max_retry}")
534
+ else:
535
+ self.logger.error(f"Clone repo {clone_url} timed out: {e}, max retry reached")
536
+ # Clean up partial clone if exists
537
+ if os.path.exists(target_path) and os.path.isdir(target_path):
538
+ import shutil
539
+ try:
540
+ shutil.rmtree(target_path)
541
+ except Exception:
542
+ pass
543
+
544
+ except subprocess.CalledProcessError as e:
545
+ # Check for authentication-related errors
546
+ stderr_output = e.stderr.decode() if e.stderr else ""
547
+
548
+ # Log the actual error message for debugging
549
+ self.logger.error(f"Clone failed with stderr: {stderr_output}")
550
+
551
+ if "authentication" in stderr_output.lower() or "username" in stderr_output.lower() or "password" in stderr_output.lower():
552
+ # Check if we already tried with token
553
+ if github_token and use_token:
554
+ self.logger.error(f"Clone with token failed for {clone_url}. Token may be invalid or insufficient permissions. Skipping.")
555
+ else:
556
+ self.logger.error(f"Repo {clone_url} requires authentication (private repo or rate limit). Consider adding a GitHub token to config.")
557
+ return None
558
+
559
+ retry += 1
560
+ if retry <= max_retry:
561
+ self.logger.warning(f"Failed to clone repo {clone_url}: {e}, retrying for {retry}/{max_retry}")
562
+ else:
563
+ self.logger.error(f"Failed to clone repo {clone_url}: {e}, max retry reached")
564
+ if not use_token and github_token:
565
+ self.logger.info(f"Failed to clone repo {clone_url}, trying again with token (max 1 attempt).")
566
+ return self._clone_repo(repo_url, depth, 0, use_token=True) # Changed to 0 to try only once
567
+ # Clean up partial clone if exists
568
+ if os.path.exists(target_path) and os.path.isdir(target_path):
569
+ import shutil
570
+ try:
571
+ shutil.rmtree(target_path)
572
+ except Exception:
573
+ pass
574
+
575
+ return None
576
+
577
+ class CodeAnalyzer():
578
+ def __init__(
579
+ self, config,
580
+ code_collector: CodeCollector,
581
+ work_collector: WorkCollector
582
+ ):
583
+ self.config = config
584
+ self.chat_agent = ChatAgent(config)
585
+ self.logger = get_logger("CodeAnalyzer")
586
+ self.filter_code_only = True
587
+ self.code_extensions = {".py", ".ipynb", ".yaml", ".yml", ".json", ".cfg", ".toml", ".sh"}
588
+ self.ignore_dirs = {".git", "__pycache__", "node_modules", "build", "dist", ".idea", ".vscode"}
589
+ self.ignore_filenames = {"__init__.py"}
590
+ self.heur_keywords = {"train", "model", "main", "run", "inference", "predict", "forward", "pipeline"}
591
+ self.code_collector = code_collector
592
+ self.work_collector = work_collector
593
+ self.MAX_FILE_SIZE = 10 * 1024 * 1024
594
+ self.repo_cache_path = self.code_collector.repo_cache_path
595
+ self.CHUNK_SIZE = 40000
596
+ self.CHUNK_OVERLAP = 10000
597
+ self.CORE_SCORE_THRESHOLD = 8
598
+ self.chunk_file = False
599
+ self.cache_result_json = True
600
+ self.force_regenerate = True
601
+ self.filter_in_steps = True
602
+ self.agentic_repo_anlysis = True
603
+ self.max_sites_per_paper = 3
604
+ self.max_papers = 150
605
+ self.max_sites = 60
606
+ self.github_clone_in_parallel = 4
607
+ self.min_code_files_threshold = 3 # Minimum number of code files required for analysis
608
+ self.min_total_files_threshold = 5 # Minimum number of total files required for analysis
609
+ self.other_model_pseudocode_cache_enabled = True
610
+ self.parallel_generating_pseudocode = True # Enable parallel pseudocode generation
611
+ self.pseudocode_co_writer_num = 2 # Number of parallel workers for pseudocode generation
612
+
613
+ def _list_repo_files(self, repo_name):
614
+ repo_path = os.path.join(self.repo_cache_path, repo_name)
615
+ if not os.path.isdir(repo_path):
616
+ raise FileNotFoundError(f"repo not found: {repo_path}")
617
+
618
+ files = []
619
+ for root, dirs, filenames in os.walk(repo_path):
620
+ # filter out large irrelevant dirs
621
+ dirs[:] = [d for d in dirs if d not in self.ignore_filenames]
622
+ for fname in filenames:
623
+ if (fname.endswith(tuple(self.code_extensions)) or not self.filter_code_only) and fname not in self.ignore_filenames:
624
+ full = os.path.join(root, fname)
625
+ files.append(full)
626
+ return files
627
+
628
+ def _read_file(self, path: str) -> Optional[str]:
629
+ try:
630
+ size = os.path.getsize(path)
631
+ if size > self.MAX_FILE_SIZE:
632
+ self.logger.warning(f"reading very large file: {path} ({size} bytes/maximum {self.MAX_FILE_SIZE}), chunking...")
633
+ with open(path, "rb") as f:
634
+ return f.read(self.MAX_FILE_SIZE)
635
+ with open(path, "r", encoding="utf-8", errors="ignore") as f:
636
+ return f.read()
637
+ except Exception as e:
638
+ self.logger.error(f"[Error] cannot read {path}: {e}")
639
+ return None
640
+
641
+ def _chunk_text(self, text: str) -> List[str]:
642
+ if not text:
643
+ return []
644
+ chunks = []
645
+ start = 0
646
+ L = len(text)
647
+ while start < L:
648
+ end = start + self.CHUNK_SIZE
649
+ chunk = text[start:end]
650
+ chunks.append(chunk)
651
+ if end >= L:
652
+ break
653
+ start = end - self.CHUNK_OVERLAP # overlap
654
+ return chunks
655
+
656
+
657
+ def scan_repo_structure(self, repo_name: str) -> Optional[dict]:
658
+ """
659
+ Scan a cloned repo in cache and return its file tree structure.
660
+
661
+ Args:
662
+ repo_name: repository name (folder name in cache)
663
+
664
+ Returns:
665
+ dict representing repo tree
666
+ """
667
+
668
+ repo_path = os.path.join(self.repo_cache_path, repo_name)
669
+
670
+ if not os.path.exists(repo_path):
671
+ self.logger.error(f"Repo not found in cache: {repo_path}")
672
+ return None
673
+
674
+ tree = {}
675
+
676
+ for root, dirs, files in os.walk(repo_path):
677
+ dirs[:] = [d for d in dirs if d not in self.ignore_dirs]
678
+ # relative path from repo root
679
+ rel_path = os.path.relpath(root, repo_path)
680
+
681
+ # navigate tree
682
+ current = tree
683
+ if rel_path != ".":
684
+ for part in rel_path.split(os.sep):
685
+ current = current.setdefault(part, {})
686
+
687
+ # add directories
688
+ for d in dirs:
689
+ current.setdefault(d, {})
690
+
691
+ # add files
692
+ for f in files:
693
+ full_path = os.path.join(root, f)
694
+ try:
695
+ size = os.path.getsize(full_path)
696
+ except OSError:
697
+ size = 0
698
+
699
+ _, ext = os.path.splitext(f)
700
+
701
+ current[f] = {
702
+ "_is_file": True,
703
+ "size": size,
704
+ "type": ext.lower() if ext else "file"
705
+ }
706
+
707
+ return tree
708
+
709
+ def format_repo_structure(self, structure: dict, max_lines: int = 300) -> str:
710
+ """
711
+ Returns a string representing the repository structure like the 'tree' command.
712
+ Uses the shared format_repo_structure function with fallback logic.
713
+
714
+ Fallback mechanism:
715
+ 1. If lines exceed max_lines, filter to show only code files
716
+ 2. If still exceeds, filter to show only files with heuristic keywords
717
+ 3. If still exceeds, truncate and show warning
718
+
719
+ Args:
720
+ structure: The repository structure dict
721
+ max_lines: Maximum lines before fallback (default: 300)
722
+
723
+ Returns:
724
+ Formatted tree string with fallback handling for large repositories
725
+ """
726
+ return format_repo_structure(
727
+ structure,
728
+ max_lines=max_lines,
729
+ code_extensions=self.code_extensions,
730
+ heur_keywords=self.heur_keywords,
731
+ include_annotations=True
732
+ )
733
+
734
+ def build_mainfest(self,
735
+ paper_id: str,
736
+ repo_url: str,
737
+ min_core_score: float = None,
738
+ per_file_score_method: str = "avg"
739
+ ):
740
+ repo_name = self.code_collector._extract_repo_name(repo_url)
741
+ self.logger.info(f"Generating mainfest for {repo_name}:{repo_url}")
742
+
743
+ repo_path = os.path.join(self.repo_cache_path, repo_name)
744
+ if not os.path.isdir(repo_path):
745
+ self.logger.info(f"repo not found in cache: {repo_path}, cloning...")
746
+ target_path = self.code_collector._clone_repo(repo_url, 1, 3)
747
+ if not target_path:
748
+ raise ValueError(f"fail to clone target repository: {repo_name}")
749
+
750
+ if not self.force_regenerate:
751
+ mainfest = self.load_cached_mainfest(repo_name)
752
+ if not mainfest:
753
+ self.logger.warning("no mainfest in cache. Generating...")
754
+ elif self.validate_mainfest(mainfest):
755
+ return mainfest
756
+ else:
757
+ self.logger.warning("Cached mainfest invalid. Regenerate.")
758
+ try:
759
+ mainfest = self.identify_and_collect_core_code(paper_id, repo_url, repo_name, min_core_score, per_file_score_method)
760
+ except Exception as e:
761
+ raise ValueError(f"Fail to build mainfest for {repo_name}: {e}")
762
+
763
+ # Validate that the repo contains actual code files before proceeding
764
+ self._validate_repo_has_code(mainfest, repo_name)
765
+
766
+ if self.agentic_repo_anlysis:
767
+ self.logger.info("Agentic mode, skip core file pseudocode generation")
768
+ return mainfest
769
+ mainfest = self.generate_pseudocode(mainfest)
770
+
771
+ if self.cache_result_json:
772
+ self.cache_mainfest(mainfest)
773
+
774
+ return mainfest
775
+
776
+
777
+ def _validate_filter_result(self, input, info_dict: dict = None):
778
+ input_dict = extract_json(input)
779
+ keys = ["is_core", "score", "reason", "is_main_code","main_score", "main_reason"]
780
+ for key in keys:
781
+ if key not in input_dict.keys():
782
+ raise ValueError(f"key {key} not in result original text: <{input}>")
783
+ if not isinstance(input_dict["is_core"], bool) or not isinstance(input_dict["is_main_code"], bool):
784
+ raise ValueError(f"key is_core or is_main_code not boolean")
785
+ return True, input
786
+
787
+ def _validate_filter_result_core(self, input, info_dict: dict = None):
788
+ """Validation function for core filter results (step 1 of 2)."""
789
+ input_dict = extract_json(input)
790
+ keys = ["is_core", "score", "reason"]
791
+ for key in keys:
792
+ if key not in input_dict.keys():
793
+ raise ValueError(f"key {key} not in result: original text: <{input}>")
794
+ if not isinstance(input_dict["is_core"], bool):
795
+ raise ValueError(f"key is_core not boolean")
796
+ return True, input
797
+
798
+ def _validate_filter_result_main(self, input, info_dict: dict = None):
799
+ """Validation function for main filter results (step 2 of 2)."""
800
+ input_dict = extract_json(input)
801
+ keys = ["is_main_code", "main_reason", "main_score"]
802
+ for key in keys:
803
+ if key not in input_dict.keys():
804
+ raise ValueError(f"key {key} not in result original text: <{input}>")
805
+ if not isinstance(input_dict["is_main_code"], bool):
806
+ raise ValueError(f"key is_main_code not boolean")
807
+ return True, input
808
+
809
+ def _filter_call_model_in_steps(self,
810
+ files_chunks: dict,
811
+ paper_title: str,
812
+ paper_abstract: str,
813
+ repo_structure: dict,
814
+ max_retry: int = 5,
815
+ ):
816
+ all_chunks_prompts_core = []
817
+ all_chunks_prompts_main = []
818
+
819
+ for file_chunks in files_chunks:
820
+ chunks = file_chunks["chunks"]
821
+ rel_path = file_chunks["path"]
822
+ for chunk in chunks:
823
+ prompt_core = CORE_FILTER.format(
824
+ paper_title = paper_title,
825
+ paper_abstract = paper_abstract,
826
+ file_path = rel_path,
827
+ repo_structure = self.format_repo_structure(repo_structure),
828
+ chunk_text = chunk
829
+ )
830
+ all_chunks_prompts_core.append(prompt_core)
831
+
832
+ for chunk in chunks:
833
+ prompt_main = MAIN_FILTER.format(
834
+ paper_title = paper_title,
835
+ paper_abstract = paper_abstract,
836
+ file_path = rel_path,
837
+ repo_structure = self.format_repo_structure(repo_structure),
838
+ chunk_text = chunk
839
+ )
840
+ all_chunks_prompts_main.append(prompt_main)
841
+
842
+ main_results = self.chat_agent.batch_remote_chat_with_retry(
843
+ prompts = all_chunks_prompts_main,
844
+ validate_fn = self._validate_filter_result_main,
845
+ max_retry = max_retry,
846
+ desc = "filtering out main code",
847
+ temperature = 0
848
+ )
849
+
850
+ core_results = self.chat_agent.batch_remote_chat_with_retry(
851
+ prompts = all_chunks_prompts_core,
852
+ validate_fn = self._validate_filter_result_core,
853
+ max_retry = max_retry,
854
+ desc = "filtering out core code",
855
+ temperature = 0
856
+ )
857
+
858
+ # Parse JSON strings into dictionaries
859
+ results = []
860
+ for core_result_str, main_result_str in zip(core_results, main_results):
861
+ core_dict = extract_json(core_result_str)
862
+ main_dict = extract_json(main_result_str)
863
+ result = {
864
+ "is_core": core_dict["is_core"],
865
+ "score": core_dict["score"],
866
+ "reason": core_dict["reason"],
867
+ "is_main_code": main_dict["is_main_code"],
868
+ "main_score": main_dict["main_score"],
869
+ "main_reason": main_dict["main_reason"],
870
+ }
871
+ results.append(result)
872
+
873
+ return results
874
+
875
+ def _filter_call_model_one_step(self,
876
+ files_chunks: dict,
877
+ paper_title: str,
878
+ paper_abstract: str,
879
+ repo_structure: dict,
880
+ max_retry: int = 5,
881
+ ):
882
+
883
+ all_chunks_prompts = []
884
+
885
+ for file_chunks in files_chunks:
886
+ chunks = file_chunks["chunks"]
887
+ rel_path = file_chunks["path"]
888
+ for chunk in chunks:
889
+ prompt = MAIN_AND_CORE_FILTER.format(
890
+ paper_title = paper_title,
891
+ paper_abstract = paper_abstract,
892
+ file_path = rel_path,
893
+ repo_structure = self.format_repo_structure(repo_structure),
894
+ chunk_text = chunk
895
+ )
896
+ all_chunks_prompts.append(prompt)
897
+
898
+ result_strings = self.chat_agent.batch_remote_chat_with_retry(
899
+ prompts = all_chunks_prompts,
900
+ validate_fn = self._validate_filter_result,
901
+ max_retry = max_retry,
902
+ desc = "filtering out main and core code",
903
+ temperature = 0
904
+ )
905
+
906
+ # Parse JSON strings into dictionaries
907
+ results = [extract_json(result_str) for result_str in result_strings]
908
+
909
+ return results
910
+
911
+
912
+
913
+ def identify_and_collect_core_code(self,
914
+ paper_id: str,
915
+ repo_url: str,
916
+ repo_name: str,
917
+ min_core_score: float = None,
918
+ per_file_score_method: str = "avg" , ## or max
919
+ max_retry: int = 8,
920
+ ):
921
+
922
+ all_files = self._list_repo_files(repo_name)
923
+ paper_title, paper_abstract = self.work_collector.get_paper_title_abstract(paper_id)
924
+
925
+ self.logger.info(f"Identifyng code for {repo_name}:{repo_url}")
926
+
927
+ mainfest = {
928
+ "repo_info": {
929
+ "repo_url": repo_url,
930
+ "repo_name": repo_name,
931
+ "paper_title": paper_title,
932
+ "paper_abstract": paper_abstract,
933
+ "repo_structure": {},
934
+ },
935
+ "scanned_files": 0,
936
+ "core_files": [],
937
+ "main_files": [], # Main entry point files (subset of core_files)
938
+ "edge_files": [] ,
939
+ "skipped": [] ,
940
+ "pseudocode_input": []
941
+ }
942
+ repo_structure = self.scan_repo_structure(repo_name)
943
+ mainfest["repo_info"]["repo_structure"] = repo_structure
944
+
945
+ ## fast mode: do not identify core/main file but directly generate repository code report
946
+ if self.agentic_repo_anlysis:
947
+ return mainfest
948
+
949
+ files_chunks = []
950
+ file_num = len(all_files)
951
+
952
+ for file_abs in all_files:
953
+ rel_path = os.path.relpath(file_abs, repo_path)
954
+ content = self._read_file(file_abs)
955
+ if content is None:
956
+ mainfest["skipped"].append({"path": rel_path, "reason": "unreadable/too large"})
957
+ file_num -= 1
958
+ continue
959
+
960
+ mainfest["scanned_files"] += 1
961
+
962
+ # quick filename heuristics to short-circuit some obvious cores
963
+ fname = os.path.basename(file_abs).lower()
964
+ if any(k in fname for k in self.heur_keywords):
965
+ pass
966
+
967
+ if self.chunk_file:
968
+ chunks = self._chunk_text(content)
969
+ else:
970
+ chunks = [content]
971
+ if not chunks:
972
+ ## !! The file orders in "scanned" and "skipped" are uncertain !! ##
973
+ mainfest["skipped"].append({"path": rel_path, "reason": "empty or no chunks"})
974
+ file_num -= 1
975
+ continue
976
+
977
+ files_chunks.append({"path": rel_path, "chunks": chunks})
978
+
979
+ self.logger.info(f"file number: {file_num}")
980
+
981
+ if self.filter_in_steps:
982
+ results = self._filter_call_model_in_steps(files_chunks = files_chunks,
983
+ paper_title = paper_title,
984
+ paper_abstract = paper_abstract,
985
+ repo_structure = repo_structure,
986
+ max_retry = max_retry)
987
+ else:
988
+ results = self._filter_call_model_one_step(files_chunks = files_chunks,
989
+ paper_title = paper_title,
990
+ paper_abstract = paper_abstract,
991
+ repo_structure = repo_structure,
992
+ max_retry = max_retry)
993
+
994
+ index = 0
995
+ for file_idx, chunks_file_info in enumerate(files_chunks):
996
+ chunks = chunks_file_info["chunks"]
997
+ chunk_results = []
998
+ for chunk_idx in range(len(chunks)):
999
+ result = results[index]
1000
+ chunk_results.append(result)
1001
+ index += 1
1002
+
1003
+ # aggregate chunk scores to file score
1004
+ scores = [c["score"] for c in chunk_results]
1005
+ main_scores = [c["main_score"] for c in chunk_results]
1006
+ is_core_flags = [c["is_core"] for c in chunk_results]
1007
+
1008
+ if per_file_score_method == "avg":
1009
+ file_score = float(sum(scores) / max(1, len(scores)))
1010
+ else:
1011
+ file_score = float(max(scores))
1012
+
1013
+ # Use default threshold if not provided
1014
+ effective_threshold = min_core_score if min_core_score is not None else self.CORE_SCORE_THRESHOLD
1015
+
1016
+ # determine final boolean:
1017
+ any_core_chunk = any(c["is_core"] and c["score"] >= effective_threshold for c in chunk_results)
1018
+ is_core_file = (file_score >= effective_threshold) or any_core_chunk
1019
+ is_main_code = any(c.get("is_main_code", False) for c in chunk_results)
1020
+
1021
+ rel_path = chunks_file_info["path"]
1022
+ reason = sorted(chunk_results, key=lambda x: x["score"], reverse=True)[0]["reason"]
1023
+ main_chunk = next((c for c in chunk_results if c.get("is_main_code", False)), None)
1024
+ main_reason = main_chunk["main_reason"] if main_chunk else chunk_results[-1]["main_reason"]
1025
+
1026
+ pseudocode_entry = {
1027
+ "path": rel_path,
1028
+ "is_core_file": is_core_file,
1029
+ "scores": scores,
1030
+ "reason": reason,
1031
+ "is_main_file": is_main_code,
1032
+ "main_scores": main_scores,
1033
+ "main_reason": main_reason
1034
+ }
1035
+
1036
+ if is_core_file or is_main_code:
1037
+ mainfest["pseudocode_input"].append(pseudocode_entry)
1038
+
1039
+ if is_core_file:
1040
+ core_file_entry = {
1041
+ "path": rel_path,
1042
+ "scores": scores,
1043
+ "reason": reason,
1044
+ }
1045
+ mainfest["core_files"].append(core_file_entry)
1046
+ try:
1047
+ parts = rel_path.split(os.sep)
1048
+ node = repo_structure
1049
+ for p in parts[:-1]:
1050
+ node = node[p]
1051
+ filename = parts[-1]
1052
+ if filename in node and isinstance(node[filename], dict):
1053
+ node[filename]["is_core_code"] = True
1054
+ else:
1055
+ self.logger.debug(f"Could not set is_core_code for {rel_path} (not found in repo_structure)")
1056
+ except Exception as e:
1057
+ self.logger.debug(f"Error marking repo_structure for {rel_path}: {e}")
1058
+ else:
1059
+ mainfest["edge_files"].append({"path": rel_path, "scores": scores, "reason": reason})
1060
+
1061
+ if is_main_code:
1062
+ # Add to main_files even if not core
1063
+ main_file_entry = {
1064
+ "path": rel_path,
1065
+ "reason": main_chunk["main_reason"],
1066
+ }
1067
+ mainfest["main_files"].append(main_file_entry)
1068
+
1069
+ # add core file information in the structure for cache
1070
+ try:
1071
+ parts = rel_path.split(os.sep)
1072
+ node = repo_structure
1073
+ for p in parts[:-1]:
1074
+ node = node[p]
1075
+ filename = parts[-1]
1076
+ if filename in node and isinstance(node[filename], dict):
1077
+ node[filename]["is_main_code"] = True
1078
+ else:
1079
+ self.logger.debug(f"Could not set is_main_code for {rel_path} (not found in repo_structure)")
1080
+ except Exception as e:
1081
+ self.logger.debug(f"Error marking repo_structure for {rel_path}: {e}")
1082
+
1083
+ return mainfest
1084
+
1085
+ def format_mainfest(self, mainfest: dict, include_pseudocode = False) -> str:
1086
+ lines = []
1087
+ lines.append("==== mainfest ====\n")
1088
+ lines.append(f"repository name: {mainfest['repo_info']['repo_name']}")
1089
+ lines.append(f"scanned files: {mainfest['scanned_files']}")
1090
+ lines.append(f"core files: {len(mainfest['core_files'])}")
1091
+ lines.append(f"skip files: {len(mainfest['skipped'])}")
1092
+
1093
+ lines.append(f"--- skipped_files ----")
1094
+ for skipped_file in mainfest["skipped"]:
1095
+ lines.append(f"skipped file path{skipped_file['path']}")
1096
+ lines.append(f"reason {skipped_file['reason']}")
1097
+
1098
+ lines.append(f"--- main_files (entry points) ----")
1099
+ main_files = mainfest.get("main_files", [])
1100
+ for main_file in main_files:
1101
+ lines.append(f"main file path: {main_file['path']}")
1102
+ lines.append(f"reason: {main_file.get('reason', '')}")
1103
+
1104
+ lines.append(f"--- core_files ----")
1105
+ for core_file in mainfest["core_files"]:
1106
+ lines.append(f"core file path{core_file['path']}")
1107
+ lines.append(f"core level: {core_file['scores']}")
1108
+ lines.append(f"reason {core_file['reason']}")
1109
+
1110
+ lines.append(f"--- edge_files ----")
1111
+ for edge_file in mainfest["edge_files"]:
1112
+ lines.append(f"edge file path{edge_file['path']}")
1113
+ lines.append(f"core level: {edge_file['scores']}")
1114
+ lines.append(f"reason {edge_file['reason']}")
1115
+
1116
+ if include_pseudocode:
1117
+ lines.append(f"--- pseudocode_files ----")
1118
+ for pseudo_file in mainfest["pseudocode_input"]:
1119
+ lines.append(f"{pseudo_file['path']}")
1120
+ lines.append(f"{pseudo_file['is_core_file']}")
1121
+ lines.append(f"{pseudo_file['is_main_file']}")
1122
+ lines.append(f"PSEUDOCODE:")
1123
+ lines.append(f"{pseudo_file.get('pseudocode', 'no pseudocode')}")
1124
+
1125
+ return "\n".join(lines)
1126
+
1127
+ def validate_mainfest(self, mainfest: dict) -> bool:
1128
+ """
1129
+ Validate if the mainfest structure is complete and contains all required fields.
1130
+
1131
+ Required structure:
1132
+ - repo_info: {repo_name, paper_title, paper_abstract, repo_structure}
1133
+ - scanned_files: int
1134
+ - core_files: list (each with path, scores, reason)
1135
+ - edge_files: list
1136
+ - skipped: list
1137
+
1138
+ For core_files, each entry must have:
1139
+ - path: str
1140
+ - scores: list of numbers
1141
+ - reason: str
1142
+ - pesudocode: str, no less than 50 chars
1143
+
1144
+ Returns True if mainfest is valid and complete, False otherwise.
1145
+ """
1146
+ if not mainfest or not isinstance(mainfest, dict):
1147
+ return False
1148
+
1149
+
1150
+ self.logger.info("Loading and validating mainfest from cache....")
1151
+
1152
+ required_keys = ["repo_info", "scanned_files", "core_files", "main_files", "edge_files", "skipped", "pseudocode_input"]
1153
+ repo_info_keys = ["repo_name", "paper_title", "paper_abstract", "repo_structure"]
1154
+ core_file_keys = ["path", "scores", "reason"]
1155
+
1156
+ # Check top-level keys
1157
+ for key in required_keys:
1158
+ if key not in mainfest:
1159
+ self.logger.warning(f"mainfest missing required key: {key}")
1160
+ return False
1161
+
1162
+ # Check repo_info keys
1163
+ for key in repo_info_keys:
1164
+ if key not in mainfest["repo_info"]:
1165
+ self.logger.warning(f"mainfest repo_info missing required key: {key}")
1166
+ return False
1167
+
1168
+ # Check that core_files is not empty
1169
+ if not mainfest.get("core_files"):
1170
+ self.logger.warning("mainfest has no core_files")
1171
+ return False
1172
+
1173
+ # Check each core_file has required fields
1174
+ for idx, core_file in enumerate(mainfest["core_files"]):
1175
+ for key in core_file_keys:
1176
+ if key not in core_file:
1177
+ self.logger.warning(f"Core file {idx} missing required key: {key}")
1178
+ return False
1179
+
1180
+ # Validate scores is a non-empty list of numbers
1181
+ scores = core_file.get("scores", [])
1182
+ if not isinstance(scores, list) or not scores:
1183
+ self.logger.warning(f"Core file {idx} has invalid scores: {scores}")
1184
+ return False
1185
+
1186
+ if not all(isinstance(s, (int, float)) for s in scores):
1187
+ self.logger.warning(f"Core file {idx} scores contain non-numeric values")
1188
+ return False
1189
+
1190
+ for idx, pseudocode_file in enumerate(mainfest["pseudocode_input"]):
1191
+ if not pseudocode_file or not isinstance(pseudocode_file, dict):
1192
+ self.logger.warning(f"Core file {idx} has wrong pseudo input format, not dict but {type(pseudocode_file)}")
1193
+ return False
1194
+
1195
+ if "pseudocode" not in pseudocode_file.keys():
1196
+ self.logger.warning(f"Core file {idx} does not have pseudocode")
1197
+ return False
1198
+
1199
+ # Check if pseudocode is a valid string before checking length
1200
+ pseudocode_value = pseudocode_file["pseudocode"]
1201
+ if not isinstance(pseudocode_value, str):
1202
+ self.logger.warning(f"Core file {idx} pseudocode is not a string, got {type(pseudocode_value)}: {pseudocode_value}")
1203
+ return False
1204
+
1205
+ if len(pseudocode_value) < 30:
1206
+ self.logger.warning(f"Core file {idx} pseudocode too short")
1207
+ return False
1208
+ return True
1209
+
1210
+ def _validate_repo_has_code(self, mainfest: dict, repo_name: str) -> None:
1211
+ repo_structure = mainfest.get("repo_info", {}).get("repo_structure")
1212
+
1213
+ if not repo_structure:
1214
+ raise ValueError(
1215
+ f"Repository {repo_name} has no valid repo structure. "
1216
+ "This may be a non-code repository or the code has not been uploaded yet. "
1217
+ "Skipping analysis for this repository."
1218
+ )
1219
+
1220
+ # Count code files and total files in the repo structure
1221
+ code_file_count, total_file_count = self._count_code_and_total_files_in_structure(repo_structure)
1222
+
1223
+ # Minimum thresholds (configurable via instance variables)
1224
+ min_code_files = self.min_code_files_threshold
1225
+ min_total_files = self.min_total_files_threshold
1226
+
1227
+ if total_file_count == 0:
1228
+ raise ValueError(
1229
+ f"Repository {repo_name} contains no files. "
1230
+ "This may be an empty repository or the code has not been uploaded yet. "
1231
+ "Skipping analysis for this repository."
1232
+ )
1233
+
1234
+ if total_file_count < min_total_files:
1235
+ raise ValueError(
1236
+ f"Repository {repo_name} contains only {total_file_count} total file(s), "
1237
+ f"which is below the minimum threshold of {min_total_files}. "
1238
+ "This repository does not have sufficient files for analysis. "
1239
+ "Skipping analysis for this repository."
1240
+ )
1241
+
1242
+ if code_file_count == 0:
1243
+ raise ValueError(
1244
+ f"Repository {repo_name} contains no code files (only {total_file_count} total files). "
1245
+ "This may be a documentation-only repository or code has not been uploaded yet. "
1246
+ "Skipping analysis for this repository."
1247
+ )
1248
+
1249
+ if code_file_count < min_code_files:
1250
+ raise ValueError(
1251
+ f"Repository {repo_name} contains only {code_file_count} code file(s) "
1252
+ f"(out of {total_file_count} total files), "
1253
+ f"which is below the minimum threshold of {min_code_files}. "
1254
+ "This repository does not have sufficient code for analysis. "
1255
+ "Skipping analysis for this repository."
1256
+ )
1257
+
1258
+ self.logger.info(
1259
+ f"Repository {repo_name} contains {code_file_count} code files "
1260
+ f"(out of {total_file_count} total files), validation passed."
1261
+ )
1262
+
1263
+ def _count_code_and_total_files_in_structure(self, repo_structure: dict) -> Tuple[int, int]:
1264
+ """
1265
+ Recursively count code files and total files in a repo structure tree.
1266
+
1267
+ Args:
1268
+ repo_structure: the repo_structure dict from mainfest
1269
+
1270
+ Returns:
1271
+ Tuple of (code_file_count, total_file_count)
1272
+ - code_file_count: count of files with valid code extensions
1273
+ - total_file_count: count of all files
1274
+ """
1275
+ code_count = 0
1276
+ total_count = 0
1277
+
1278
+ for key, value in repo_structure.items():
1279
+ if isinstance(value, dict):
1280
+ if value.get("_is_file"):
1281
+ total_count += 1
1282
+ # Count files with valid code extensions as code files
1283
+ file_type = value.get("type", "").lower()
1284
+ if file_type in self.code_extensions:
1285
+ code_count += 1
1286
+ else:
1287
+ # It's a directory, recurse into it
1288
+ sub_code_count, sub_total_count = self._count_code_and_total_files_in_structure(value)
1289
+ code_count += sub_code_count
1290
+ total_count += sub_total_count
1291
+
1292
+ return code_count, total_count
1293
+
1294
+ def load_cached_mainfest(self, repo_name: str) -> Optional[dict]:
1295
+ model_name = getattr(self.chat_agent, "model_name", "default")
1296
+ cache_path = os.path.join(self.repo_cache_path, repo_name, f"mainfest_{model_name}.json")
1297
+
1298
+ if not os.path.exists(cache_path):
1299
+ self.logger.info(f"Cache file not found: {cache_path}")
1300
+ return None
1301
+
1302
+ try:
1303
+ with open(cache_path, "r", encoding="utf-8") as f:
1304
+ mainfest = json.load(f)
1305
+
1306
+ self.logger.info(f"Loaded cached mainfest from {cache_path}")
1307
+ return mainfest
1308
+ except Exception as e:
1309
+ self.logger.warning(f"Failed to load cached mainfest: {e}")
1310
+ return None
1311
+
1312
+ def generate_pseudocode(self, mainfest: dict) -> dict:
1313
+ """
1314
+ Internal method to generate pseudocode using LLM.
1315
+
1316
+ For each core file:
1317
+ 1. Read the file content
1318
+ 2. Build a prompt using PSEUDOCODE_WRITER template
1319
+ 3. Use batch_remote_chat to get the analysis
1320
+ 4. Add the pseudocode result to the core_file entry
1321
+ 5. Save to cache
1322
+ """
1323
+ repo_name = mainfest["repo_info"]["repo_name"]
1324
+ paper_title = mainfest["repo_info"]["paper_title"]
1325
+ paper_abstract = mainfest["repo_info"]["paper_abstract"]
1326
+ repo_path = os.path.join(self.repo_cache_path, repo_name)
1327
+
1328
+ prompts = []
1329
+ pseudo_input_indices = []
1330
+ pseudo_input_list = mainfest["pseudocode_input"]
1331
+
1332
+ for idx, input_file in enumerate(pseudo_input_list):
1333
+ rel_path = input_file["path"]
1334
+ file_abs = os.path.join(repo_path, rel_path)
1335
+
1336
+ # Read file content
1337
+ content = self._read_file(file_abs)
1338
+ if content is None:
1339
+ self.logger.warning(f"Could not read core file: {rel_path}, skipping...")
1340
+ input_file["pseudocode"] = {"error": "Could not read file"}
1341
+ continue
1342
+
1343
+ # Get the score reason from the core file info
1344
+ score_reason = input_file.get("reason", "No reason provided")
1345
+ main_reason = input_file.get("main_reason", "No reason provided")
1346
+
1347
+ # Get the highest score for this file
1348
+ scores = input_file.get("scores", [])
1349
+ main_scores = input_file.get("main_scores", [])
1350
+ core_score = max(scores) if scores else 0
1351
+ main_score = max(main_scores) if main_scores else 0
1352
+ is_main_code = input_file.get("is_main_file", "Unknown")
1353
+ is_core_file = input_file.get("is_core_code", "Unknown")
1354
+
1355
+ # Build the prompt using PSEUDOCODE_WRITER template
1356
+ prompt = PSEUDOCODE_WRITER.format(
1357
+ file_path=rel_path,
1358
+ core_score=core_score,
1359
+ is_core_file = is_core_file,
1360
+ reason=score_reason,
1361
+ is_main_code = is_main_code,
1362
+ main_score = main_score,
1363
+ main_reason = main_reason,
1364
+ paper_title=paper_title,
1365
+ paper_abstract=paper_abstract,
1366
+ file_content=content
1367
+ )
1368
+
1369
+ prompts.append(prompt)
1370
+ pseudo_input_indices.append(idx)
1371
+
1372
+ if not prompts:
1373
+ self.logger.info("No prompts to process for pseudocode generation")
1374
+ return mainfest
1375
+
1376
+ self.logger.info(f"Generating pseudocode for {len(prompts)} core files...")
1377
+
1378
+ # Use batch_remote_chat to get the analysis
1379
+ results = self.chat_agent.batch_remote_chat(prompts, desc="Generating pseudocode for core and main files...")
1380
+
1381
+ # Process results and add to mainfest
1382
+ for i, result in enumerate(results):
1383
+ idx = pseudo_input_indices[i]
1384
+ mainfest["pseudocode_input"][idx]["pseudocode"] = result
1385
+ input_file = pseudo_input_list[idx]
1386
+
1387
+ rel_path = input_file["path"]
1388
+ repo_structure = mainfest["repo_info"]["repo_structure"]
1389
+
1390
+ try:
1391
+ parts = rel_path.split(os.sep)
1392
+ node = repo_structure
1393
+ for p in parts[:-1]:
1394
+ node = node[p]
1395
+ filename = parts[-1]
1396
+ if filename in node and isinstance(node[filename], dict):
1397
+ node[filename]["pseudocode"] = result
1398
+ else:
1399
+ self.logger.debug(f"Could not set pseudocode result for {rel_path} (not found in repo_structure)")
1400
+ except Exception as e:
1401
+ self.logger.debug(f"Error marking repo_structure for {rel_path}: {e}")
1402
+
1403
+ return mainfest
1404
+
1405
+ def cache_mainfest(self, mainfest):
1406
+ try:
1407
+ with open(f"{self.repo_cache_path}/{mainfest['repo_info']['repo_name']}/mainfest_{self.chat_agent.model_name}.json", "w") as f:
1408
+ json.dump(mainfest, f)
1409
+ except Exception as e:
1410
+ self.logger.error(f"error in mainfest cache for {mainfest.get('repo_info', {}).get('repo_name', 'Unknown')}: {e}")
1411
+ return
1412
+
1413
+ def _get_pseudocode_from_repo_structure(self, repo_structure: dict, rel_path: str) -> Optional[str]:
1414
+ """
1415
+ Used only in non-agent mode: Agent mode do not include pseudocode in structure
1416
+ Helper method to retrieve pseudocode from repo_structure by file path.
1417
+
1418
+ Args:
1419
+ repo_structure: the repo_structure dict from mainfest
1420
+ rel_path: relative path to the file
1421
+
1422
+ Returns:
1423
+ pseudocode string if found, None otherwise
1424
+ """
1425
+ try:
1426
+ parts = rel_path.split(os.sep)
1427
+ node = repo_structure
1428
+ for p in parts[:-1]:
1429
+ if p not in node:
1430
+ return None
1431
+ node = node[p]
1432
+ filename = parts[-1]
1433
+ if filename in node and isinstance(node[filename], dict):
1434
+ return node[filename].get("pseudocode")
1435
+ except Exception as e:
1436
+ self.logger.debug(f"Error getting pseudocode for {rel_path}: {e}")
1437
+ return None
1438
+
1439
+ def _get_main_files_sorted_by_score(self, mainfest: dict, k: int = 5) -> List[dict]:
1440
+ """
1441
+ Get main files sorted by main_score, returning top-k if needed.
1442
+
1443
+ Args:
1444
+ mainfest: the mainfest dict containing main_files info
1445
+ k: default number of main files to include (default 5)
1446
+
1447
+ Returns:
1448
+ list of main file entries sorted by main_score descending
1449
+ """
1450
+ repo_structure = mainfest["repo_info"]["repo_structure"]
1451
+
1452
+ # Get main files with their scores from main_files
1453
+ main_files_with_score = []
1454
+ for main_file in mainfest.get("main_files", []):
1455
+ rel_path = main_file["path"]
1456
+ # Find the corresponding entry in pseudocode_input to get main_scores
1457
+ pseudo_entry = next((p for p in mainfest.get("pseudocode_input", [])
1458
+ if p["path"] == rel_path), None)
1459
+ if pseudo_entry:
1460
+ main_scores = pseudo_entry.get("main_scores", [])
1461
+ max_main_score = max(main_scores) if main_scores else 0
1462
+ main_files_with_score.append({
1463
+ "path": rel_path,
1464
+ "main_score": max_main_score,
1465
+ "reason": main_file.get("reason", "")
1466
+ })
1467
+
1468
+ # Sort by main_score descending
1469
+ main_files_with_score.sort(key=lambda x: x["main_score"], reverse=True)
1470
+
1471
+ # Return all if count <= k, otherwise return top k
1472
+ if len(main_files_with_score) <= k:
1473
+ return main_files_with_score
1474
+ else:
1475
+ return main_files_with_score[:k]
1476
+
1477
+ def generate_project_pseudocode_from_main_files(
1478
+ self,
1479
+ mainfest: dict,
1480
+ k: int = 5,
1481
+ max_retry: int = 3
1482
+ ) -> str:
1483
+ """
1484
+ Function 1: Generate project pseudocode framework from main files.
1485
+
1486
+ If the number of main files is <= k (default 5), all are input.
1487
+ Otherwise, select the top-k main files with highest main_score.
1488
+
1489
+ Args:
1490
+ mainfest: the mainfest dict containing repo_info and main_files info
1491
+ k: default threshold for number of main files (default 5)
1492
+ max_retry: maximum number of retries for API calls
1493
+
1494
+ Returns:
1495
+ Initial project pseudocode string
1496
+ """
1497
+ self.logger.info("Generating project pseudocode from main files...")
1498
+
1499
+ # Extract necessary info from mainfest
1500
+ paper_title = mainfest["repo_info"]["paper_title"]
1501
+ paper_abstract = mainfest["repo_info"]["paper_abstract"]
1502
+ repo_structure = mainfest["repo_info"]["repo_structure"]
1503
+
1504
+ # Get main files sorted by main_score
1505
+ main_files_sorted = self._get_main_files_sorted_by_score(mainfest, k)
1506
+
1507
+ if not main_files_sorted:
1508
+ self.logger.warning("No main files found in mainfest")
1509
+ return ""
1510
+
1511
+ self.logger.info(f"Using {len(main_files_sorted)} main files to build project pseudocode")
1512
+
1513
+ # Build the main files pseudocode content
1514
+ main_files_pseudocode = []
1515
+ for i, main_file in enumerate(main_files_sorted):
1516
+ rel_path = main_file["path"]
1517
+ main_score = main_file["main_score"]
1518
+ reason = main_file.get("reason", "")
1519
+
1520
+ # Get pseudocode from repo_structure
1521
+ pseudocode = self._get_pseudocode_from_repo_structure(repo_structure, rel_path)
1522
+
1523
+ if pseudocode:
1524
+ entry = f"=== Main File {i+1}: {rel_path} (main_score: {main_score}/10) ===\n"
1525
+ entry += f"Reason: {reason}\n"
1526
+ entry += "Pseudocode:\n"
1527
+ entry += f"{pseudocode}"
1528
+ else:
1529
+ entry = f"=== Main File {i+1}: {rel_path} (main_score: {main_score}/10) ===\n"
1530
+ entry += f"Reason: {reason}\n"
1531
+ entry += "Pseudocode:\n"
1532
+ entry += f"[Not avaliable]"
1533
+ main_files_pseudocode.append(entry)
1534
+
1535
+ main_files_content = "\n".join(main_files_pseudocode)
1536
+
1537
+ # Build the prompt
1538
+ prompt = PROJECT_PSEUDOCODE_FROM_MAIN.format(
1539
+ paper_title=paper_title,
1540
+ paper_abstract=paper_abstract,
1541
+ repo_structure=self.format_repo_structure(repo_structure),
1542
+ main_files_pseudocode=main_files_content
1543
+ )
1544
+
1545
+ # Call the LLM to generate project pseudocode
1546
+ self.logger.info("Calling LLM to generate project pseudocode framework...")
1547
+ result = self.chat_agent.remote_chat(
1548
+ text_content=prompt,
1549
+ temperature=0
1550
+ )
1551
+
1552
+ return result
1553
+
1554
+ def refine_project_pseudocode_with_core_files(
1555
+ self,
1556
+ mainfest: dict,
1557
+ current_pseudocode: str,
1558
+ batch_size: int = 3,
1559
+ max_retry: int = 3
1560
+ ) -> str:
1561
+ """
1562
+ Function 2: Refine project pseudocode by incorporating core files in batches.
1563
+
1564
+ This function processes core files in batches and progressively refines
1565
+ the project pseudocode by incorporating each batch's pseudocode.
1566
+
1567
+ Args:
1568
+ mainfest: the mainfest dict containing repo_info and core_files info
1569
+ current_pseudocode: the current project pseudocode to refine
1570
+ batch_size: number of core files to process in each batch (default 3)
1571
+ max_retry: maximum number of retries for API calls
1572
+
1573
+ Returns:
1574
+ Final refined project pseudocode string
1575
+ """
1576
+ self.logger.info("Refining project pseudocode with core files (batch mode)...")
1577
+
1578
+ # Extract necessary info from mainfest
1579
+ paper_title = mainfest["repo_info"]["paper_title"]
1580
+ paper_abstract = mainfest["repo_info"]["paper_abstract"]
1581
+ repo_structure = mainfest["repo_info"]["repo_structure"]
1582
+
1583
+ # Get core files sorted by score
1584
+ core_files_sorted = []
1585
+ for core_file in mainfest.get("core_files", []):
1586
+ rel_path = core_file["path"]
1587
+ scores = core_file.get("scores", [])
1588
+ max_score = max(scores) if scores else 0
1589
+ core_files_sorted.append({
1590
+ "path": rel_path,
1591
+ "score": max_score,
1592
+ "reason": core_file.get("reason", "")
1593
+ })
1594
+
1595
+ # Sort by score descending
1596
+ core_files_sorted.sort(key=lambda x: x["score"], reverse=True)
1597
+
1598
+ if not core_files_sorted:
1599
+ self.logger.warning("No core files found in mainfest")
1600
+ return current_pseudocode
1601
+
1602
+ self.logger.info(f"Processing {len(core_files_sorted)} core files in batches of {batch_size}")
1603
+
1604
+ # Initialize the current pseudocode
1605
+ refined_pseudocode = current_pseudocode
1606
+
1607
+ # Process in batches
1608
+ for batch_idx in range(0, len(core_files_sorted), batch_size):
1609
+ batch = core_files_sorted[batch_idx:batch_idx + batch_size]
1610
+ self.logger.info(f"Processing batch {batch_idx // batch_size + 1}: files {batch_idx + 1} to {batch_idx + len(batch)}")
1611
+
1612
+ # Build core files pseudocode content for this batch
1613
+ core_files_pseudocode = []
1614
+ for i, core_file in enumerate(batch):
1615
+ rel_path = core_file["path"]
1616
+ score = core_file["score"]
1617
+ reason = core_file.get("reason", "")
1618
+
1619
+ # Get pseudocode from repo_structure
1620
+ pseudocode = self._get_pseudocode_from_repo_structure(repo_structure, rel_path)
1621
+
1622
+ if pseudocode:
1623
+ entry = f"--- Core File {i+1}: {rel_path} (core_score: {score}/10) ---"
1624
+ entry += f"Reason: {reason}\n"
1625
+ entry += "Pseudocode:\n"
1626
+ entry += f"{pseudocode}"
1627
+ else:
1628
+ entry = f"--- Core File {i+1}: {rel_path} (core_score: {score}/10) ---"
1629
+ entry += f"Reason: {reason}\n"
1630
+ entry += "Pseudocode:\n"
1631
+ entry += f"[Not Available]"
1632
+ core_files_pseudocode.append(entry)
1633
+
1634
+ core_files_content = "\n".join(core_files_pseudocode)
1635
+
1636
+ # Build the prompt for refinement
1637
+ prompt = REFINE_PROJECT_PSEUDOCODE_WITH_CORE.format(
1638
+ paper_title=paper_title,
1639
+ paper_abstract=paper_abstract,
1640
+ repo_structure=self.format_repo_structure(repo_structure),
1641
+ current_pseudocode=refined_pseudocode,
1642
+ core_files_pseudocode=core_files_content
1643
+ )
1644
+
1645
+ # Call the LLM to refine the pseudocode
1646
+ self.logger.info(f"Calling LLM to refine pseudocode for batch {batch_idx // batch_size + 1}...")
1647
+ result = self.chat_agent.remote_chat(
1648
+ text_content=prompt,
1649
+ temperature=0
1650
+ )
1651
+
1652
+ # Update the refined pseudocode
1653
+ refined_pseudocode = result
1654
+
1655
+ self.logger.info("Finished refining project pseudocode with all core files")
1656
+ return refined_pseudocode
1657
+
1658
+ def cache_repo_pseudocode(self, repo_name, pseudocode, concise = False):
1659
+ model_name = self.chat_agent.model_name
1660
+ if not concise:
1661
+ cache_path = os.path.join(self.repo_cache_path, repo_name, f"pseudocode_{model_name}.json")
1662
+ else:
1663
+ cache_path = os.path.join(self.repo_cache_path, repo_name, f"concise_pseudocode_{model_name}.json")
1664
+
1665
+ with open(cache_path, "w") as f:
1666
+ f.write(pseudocode)
1667
+
1668
+ def read_repo_pseudocode_cache(self, repo_name, include_concise = True):
1669
+ model_name = self.chat_agent.model_name
1670
+ cache_path = os.path.join(self.repo_cache_path, repo_name, f"pseudocode_{model_name}.json")
1671
+ concise_cache_path = os.path.join(self.repo_cache_path, repo_name, f"concise_pseudocode_{model_name}.json")
1672
+
1673
+ concise_pseudocode = None
1674
+ pseudocode = None
1675
+
1676
+ if os.path.exists(cache_path):
1677
+ with open(cache_path, "r") as f:
1678
+ pseudocode = f.read()
1679
+
1680
+ if include_concise and os.path.exists(concise_cache_path):
1681
+ with open(concise_cache_path, "r") as f:
1682
+ concise_pseudocode = f.read()
1683
+
1684
+ return pseudocode, concise_pseudocode
1685
+
1686
+ def _load_other_model_pseudocode_cache(self, repo_name: str, include_concise: bool = True) -> Tuple[Optional[str], Optional[str]]:
1687
+ """
1688
+ Load pseudocode cache from other models as fallback when current model's cache is not available.
1689
+
1690
+ Args:
1691
+ repo_name: the repository name
1692
+ include_concise: whether to also load concise pseudocode
1693
+
1694
+ Returns:
1695
+ Tuple of (pseudocode, concise_pseudocode) from other models, or (None, None) if not found
1696
+ """
1697
+ repo_path = os.path.join(self.repo_cache_path, repo_name)
1698
+
1699
+ if not os.path.exists(repo_path):
1700
+ self.logger.debug(f"Repo path does not exist: {repo_path}")
1701
+ return None, None
1702
+
1703
+ # List all pseudocode files in the repo directory
1704
+ try:
1705
+ files = os.listdir(repo_path)
1706
+ except Exception as e:
1707
+ self.logger.warning(f"Failed to list files in {repo_path}: {e}")
1708
+ return None, None
1709
+
1710
+ # Find pseudocode files from other models
1711
+ other_pseudocode = None
1712
+ other_concise_pseudocode = None
1713
+ current_model = self.chat_agent.model_name
1714
+
1715
+ for f in files:
1716
+ # Check if it's a pseudocode file (not from current model)
1717
+ if f.startswith("pseudocode_") and f.endswith(".json"):
1718
+ # Extract model name from filename
1719
+ model_name = f[len("pseudocode_"):-len(".json")]
1720
+ if model_name != current_model:
1721
+ cache_path = os.path.join(repo_path, f)
1722
+ try:
1723
+ with open(cache_path, "r") as pf:
1724
+ content = pf.read()
1725
+ if content and len(content) > 30: # Validate content
1726
+ other_pseudocode = content
1727
+ self.logger.info(f"Found fallback pseudocode from model: {model_name}")
1728
+ break
1729
+ except Exception as e:
1730
+ self.logger.debug(f"Failed to read fallback pseudocode from {cache_path}: {e}")
1731
+
1732
+ if include_concise:
1733
+ for f in files:
1734
+ if f.startswith("concise_pseudocode_") and f.endswith(".json"):
1735
+ model_name = f[len("concise_pseudocode_"):-len(".json")]
1736
+ if model_name != current_model:
1737
+ cache_path = os.path.join(repo_path, f)
1738
+ try:
1739
+ with open(cache_path, "r") as pf:
1740
+ content = pf.read()
1741
+ if content and len(content) > 30: # Validate content
1742
+ other_concise_pseudocode = content
1743
+ self.logger.info(f"Found fallback concise pseudocode from model: {model_name}")
1744
+ break
1745
+ except Exception as e:
1746
+ self.logger.debug(f"Failed to read fallback concise pseudocode from {cache_path}: {e}")
1747
+
1748
+ return other_pseudocode, other_concise_pseudocode
1749
+
1750
+ def refine_project_pseudocode_with_agent(
1751
+ self,
1752
+ mainfest: dict,
1753
+ current_pseudocode: str,
1754
+ max_steps: int = 20,
1755
+ hard_code_revise: bool = False,
1756
+ max_rounds_without_revise: int = 3,
1757
+ last_round_revise: bool = True,
1758
+ ) -> str:
1759
+ """
1760
+ Function 3 (Agent mode): Refine project pseudocode using agent-based approach.
1761
+
1762
+ A central agent decides operations:
1763
+ - get_pseudocode: Query pseudocode of a specific file
1764
+ - get_source_code: Query source code of a specific file
1765
+ - revise: Modify current pseudocode based on context
1766
+ - review: Call review skill to provide suggestions
1767
+ - finish: Complete the refinement
1768
+
1769
+ Args:
1770
+ mainfest: the mainfest dict containing repo_info and core_files info
1771
+ current_pseudocode: the current project pseudocode to refine
1772
+ max_steps: maximum number of agent steps (default 20)
1773
+ hard_code_revise: if True, force call revise after max_rounds_without_revise rounds without revise
1774
+ max_rounds_without_revise: maximum rounds without revise before forcing (default: 3)
1775
+
1776
+ Returns:
1777
+ Final refined project pseudocode string
1778
+ """
1779
+ self.logger.info(f"Refining project pseudocode with agent mode (hard_code_revise={hard_code_revise})...")
1780
+
1781
+ # Create PseudoReviser instance
1782
+ revisor = PseudoReviser(
1783
+ config=None, # Already have chat_agent
1784
+ chat_agent=self.chat_agent,
1785
+ repo_cache_path=self.repo_cache_path
1786
+ )
1787
+
1788
+ # Call the agent-based refinement
1789
+ final_pseudocode = revisor.refine_pseudocode_with_agent(
1790
+ mainfest=mainfest,
1791
+ current_pseudocode=current_pseudocode,
1792
+ max_steps=max_steps,
1793
+ hard_code_revise=hard_code_revise,
1794
+ max_rounds_without_revise=max_rounds_without_revise,
1795
+ last_round_revise = last_round_revise,
1796
+ )
1797
+
1798
+ self.logger.info("Agent-based pseudocode refinement completed")
1799
+ return final_pseudocode
1800
+
1801
+ def agentic_generate_project_pseudocode(
1802
+ self,
1803
+ mainfest: dict = None,
1804
+ max_steps: int = 20,
1805
+ hard_code_revise: bool = False,
1806
+ max_rounds_without_revise: int = 3,
1807
+ last_round_revise: bool = True,
1808
+ ) -> tuple:
1809
+ """
1810
+ Agentic method to generate pseudocode using PseudoWriter.
1811
+
1812
+ This method creates pseudocode from scratch based on repo_structure,
1813
+ paper title, and abstract - without relying on core_files and main_files.
1814
+
1815
+ Args:
1816
+ mainfest: mainfest dict containing repo_info (repo_name, paper_title, paper_abstract, repo_structure)
1817
+ max_steps: Maximum number of agent steps (default 20)
1818
+ hard_code_revise: If True, force call revise after max_rounds_without_revise rounds without revise
1819
+ max_rounds_without_revise: Maximum rounds without revise before forcing (default: 3)
1820
+ last_round_revise: If True, force revise in last round
1821
+
1822
+ Returns:
1823
+ Tuple of (final_pseudocode, initial_pseudocode) where initial_pseudocode is the first created pseudocode before any revision
1824
+ """
1825
+ repo_name = mainfest["repo_info"]["repo_name"]
1826
+ paper_title = mainfest["repo_info"]["paper_title"]
1827
+ paper_abstract = mainfest["repo_info"]["paper_abstract"]
1828
+ repo_structure = mainfest["repo_info"]["repo_structure"]
1829
+
1830
+ self.logger.info(f"Agentic pseudocode generation for {repo_name} (max_steps={max_steps})")
1831
+
1832
+ # Create PseudoWriter instance
1833
+ writer = PseudoWriter(
1834
+ config=self.config,
1835
+ chat_agent=self.chat_agent,
1836
+ repo_cache_path=self.repo_cache_path
1837
+ )
1838
+
1839
+ # Call the agent-based creation - returns tuple (final_pseudocode, initial_pseudocode)
1840
+ final_pseudocode, initial_pseudocode = writer.create_pseudocode_with_agent(
1841
+ repo_name=repo_name,
1842
+ paper_title=paper_title,
1843
+ paper_abstract=paper_abstract,
1844
+ repo_structure=repo_structure,
1845
+ max_steps=max_steps,
1846
+ hard_code_revise=hard_code_revise,
1847
+ max_rounds_without_revise=max_rounds_without_revise,
1848
+ last_round_revise=last_round_revise,
1849
+ )
1850
+
1851
+ self.logger.info(f"Agentic pseudocode generation completed for {repo_name}")
1852
+ return final_pseudocode, initial_pseudocode
1853
+
1854
+ def _batch_clone(self, paper_sites: List[str], download_parallel: int = 4) -> Tuple[List[str], List[str]]:
1855
+ """
1856
+ Clone multiple GitHub repositories in parallel.
1857
+
1858
+ Args:
1859
+ paper_sites: List of GitHub repository URLs to clone
1860
+ download_parallel: Number of parallel cloning workers (default: 4)
1861
+
1862
+ Returns:
1863
+ Tuple of (success_list, failed_list) containing repo names
1864
+ """
1865
+ self.logger.info(f"Starting batch clone for {len(paper_sites)} sites with {download_parallel} workers")
1866
+
1867
+ # Separate sites into cached and to-be-cloned
1868
+ sites_to_clone = []
1869
+ cached_sites = []
1870
+
1871
+ for site in paper_sites:
1872
+ repo_name = self.code_collector._extract_repo_name(site)
1873
+ repo_path = os.path.join(self.code_collector.repo_cache_path, repo_name)
1874
+ if os.path.isdir(repo_path):
1875
+ self.logger.info(f"Repo already cached: {repo_name}")
1876
+ cached_sites.append(repo_name)
1877
+ else:
1878
+ sites_to_clone.append((site, repo_name))
1879
+
1880
+ self.logger.info(f"Cached repos: {len(cached_sites)}, to clone: {len(sites_to_clone)}")
1881
+
1882
+ if not sites_to_clone:
1883
+ return cached_sites, []
1884
+
1885
+ # Parallel cloning with tqdm progress
1886
+ success_list = list(cached_sites)
1887
+ failed_list = []
1888
+
1889
+ def clone_single(site_repo_tuple):
1890
+ site, repo_name = site_repo_tuple
1891
+ try:
1892
+ target_path = self.code_collector._clone_repo(site, depth=1, max_retry=3)
1893
+ if target_path:
1894
+ return (repo_name, True, None)
1895
+ else:
1896
+ return (repo_name, False, "Clone returned None")
1897
+ except Exception as e:
1898
+ return (repo_name, False, str(e))
1899
+
1900
+ with ThreadPoolExecutor(max_workers=download_parallel) as executor:
1901
+ futures = {executor.submit(clone_single, site_repo): site_repo for site_repo in sites_to_clone}
1902
+
1903
+ with tqdm(total=len(sites_to_clone), desc="Cloning repos", unit="repo") as pbar:
1904
+ for future in as_completed(futures):
1905
+ repo_name, success, error_msg = future.result()
1906
+ if success:
1907
+ success_list.append(repo_name)
1908
+ self.logger.info(f"Successfully cloned: {repo_name}")
1909
+ else:
1910
+ failed_list.append(repo_name)
1911
+ self.logger.error(f"Failed to clone {repo_name}: {error_msg}")
1912
+ pbar.update(1)
1913
+
1914
+ self.logger.info(f"Batch clone completed: {len(success_list)} success, {len(failed_list)} failed")
1915
+ return success_list, failed_list
1916
+
1917
+ def _process_site_pseudocode(self, paper: str, site: str, main_fest: dict,
1918
+ batch_refine: bool, agent_refine: bool, batch_size: int) -> Optional[dict]:
1919
+ """
1920
+ Process pseudocode generation for a single site. Used by both sequential and parallel execution.
1921
+
1922
+ Returns:
1923
+ Dict with keys: pseudocode, initial_pseudocode, repo_name, main_fest, or None if failed
1924
+ """
1925
+ repo_name = self.code_collector._extract_repo_name(site)
1926
+ pseudocode = None
1927
+ initial_pseudocode = None
1928
+
1929
+ if not self.force_regenerate:
1930
+ pseudocode, initial_pseudocode = self.read_repo_pseudocode_cache(repo_name, True)
1931
+
1932
+ # Fallback: try to load pseudocode from other models if current model's cache is not available
1933
+ if (not pseudocode or not initial_pseudocode) and self.other_model_pseudocode_cache_enabled:
1934
+ self.logger.info(f"Current model cache not found for {repo_name}, trying to load from other models...")
1935
+ other_pseudocode, other_initial_pseudocode = self._load_other_model_pseudocode_cache(repo_name, True)
1936
+ if other_pseudocode:
1937
+ pseudocode = other_pseudocode
1938
+ self.logger.info(f"Loaded fallback pseudocode from other model for {repo_name}")
1939
+ if other_initial_pseudocode:
1940
+ initial_pseudocode = other_initial_pseudocode
1941
+ self.logger.info(f"Loaded fallback concise pseudocode from other model for {repo_name}")
1942
+
1943
+ if not pseudocode or not initial_pseudocode:
1944
+ if not self.agentic_repo_anlysis:
1945
+ initial_pseudocode = self.generate_project_pseudocode_from_main_files(main_fest)
1946
+
1947
+ pseudocode = initial_pseudocode
1948
+ # Refine
1949
+ if batch_refine:
1950
+ pseudocode = self.refine_project_pseudocode_with_core_files(mainfest = main_fest,
1951
+ current_pseudocode=pseudocode,
1952
+ batch_size = batch_size)
1953
+ if agent_refine:
1954
+ pseudocode = self.refine_project_pseudocode_with_agent(mainfest = main_fest,
1955
+ current_pseudocode=pseudocode,
1956
+ max_steps = 20,
1957
+ hard_code_revise = True,
1958
+ max_rounds_without_revise = 3,
1959
+ last_round_revise = True)
1960
+ else:
1961
+ try:
1962
+ pseudocode, initial_pseudocode = self.agentic_generate_project_pseudocode(
1963
+ mainfest=main_fest,
1964
+ max_steps=10,
1965
+ hard_code_revise=True,
1966
+ max_rounds_without_revise=5,
1967
+ last_round_revise=True
1968
+ )
1969
+ except Exception as e:
1970
+ self.logger.error(f"Fail to generate {site} pseudocode in agentic pseudocreater: {e}. Skip this repo.")
1971
+ return None
1972
+ pseudocode = f"[REPOSITORY NAME]:{repo_name}: \n" + pseudocode
1973
+ initial_pseudocode = f"[REPOSITORY NAME]:{repo_name}: \n" + initial_pseudocode
1974
+
1975
+ if main_fest and pseudocode and initial_pseudocode:
1976
+ self.cache_repo_pseudocode(repo_name, pseudocode)
1977
+ self.cache_repo_pseudocode(repo_name, initial_pseudocode, concise=True)
1978
+ return {
1979
+ "pseudocode": pseudocode,
1980
+ "initial_pseudocode": initial_pseudocode,
1981
+ "repo_name": repo_name,
1982
+ "main_fest": main_fest,
1983
+ }
1984
+ else:
1985
+ self.logger.error(f"Failed to generate valid pseudocode for {paper}/{repo_name}")
1986
+ return None
1987
+
1988
+ def execute(self, papers: List[str], batch_refine: bool = True, agent_refine: bool = True, batch_size: int = 3):
1989
+ papers_sites = []
1990
+ site_num = 0
1991
+ max_sites_per_paper = self.max_sites_per_paper
1992
+ max_papers = self.max_papers
1993
+ for paper in papers:
1994
+ try:
1995
+ paper_markdown = self.work_collector.get_paper_raw_markdown(paper)
1996
+ except Exception as e:
1997
+ papers_sites.append([])
1998
+ continue
1999
+ sites = self.code_collector.extract_code_links(paper_id = paper, paper_markdown=paper_markdown, valid_num=max_sites_per_paper)
2000
+ papers_sites.append(sites)
2001
+ site_num += len(sites)
2002
+ self.logger.info(f"[GENERAL] extracted site number: {site_num}")
2003
+
2004
+ all_sites = []
2005
+ papers_sites = papers_sites[:max_papers]
2006
+ for paper_sites in papers_sites:
2007
+ all_sites.extend(paper_sites)
2008
+ self._batch_clone(all_sites, self.github_clone_in_parallel)
2009
+
2010
+ # First pass: build mainfests for all sites
2011
+ site_tasks = [] # List of (paper, site, main_fest, repo_name)
2012
+ for paper, sites in zip(papers, papers_sites):
2013
+ if not sites:
2014
+ continue
2015
+ for site in sites:
2016
+ self.logger.info(f"processing {paper}: {site}")
2017
+ repo_name = self.code_collector._extract_repo_name(site)
2018
+ try:
2019
+ main_fest = self.build_mainfest(paper, site, 8)
2020
+ site_tasks.append((paper, site, main_fest, repo_name))
2021
+ except Exception as e:
2022
+ self.logger.error(f"Fail to process {site} in analyzer execution: {e}. Skip this repo.")
2023
+ continue
2024
+
2025
+ # Check cache for all tasks
2026
+ cached_results = {}
2027
+ tasks_need_generation = []
2028
+ site_tasks = site_tasks[:self.max_sites]
2029
+
2030
+ for paper, site, main_fest, repo_name in site_tasks:
2031
+ pseudocode = None
2032
+ initial_pseudocode = None
2033
+ if not self.force_regenerate:
2034
+ pseudocode, initial_pseudocode = self.read_repo_pseudocode_cache(repo_name, True)
2035
+
2036
+ # Try fallback from other models
2037
+ if (not pseudocode or not initial_pseudocode) and self.other_model_pseudocode_cache_enabled:
2038
+ other_pseudocode, other_initial_pseudocode = self._load_other_model_pseudocode_cache(repo_name, True)
2039
+ if other_pseudocode:
2040
+ pseudocode = other_pseudocode
2041
+ if other_initial_pseudocode:
2042
+ initial_pseudocode = other_initial_pseudocode
2043
+
2044
+ if pseudocode and initial_pseudocode:
2045
+ pseudocode = f"[REPOSITORY NAME]:{repo_name}: \n" + pseudocode
2046
+ initial_pseudocode = f"[REPOSITORY NAME]:{repo_name}: \n" + initial_pseudocode
2047
+ cached_results[(paper, site)] = {
2048
+ "pseudocode": pseudocode,
2049
+ "initial_pseudocode": initial_pseudocode,
2050
+ "repo_name": repo_name,
2051
+ "main_fest": main_fest,
2052
+ }
2053
+ else:
2054
+ tasks_need_generation.append((paper, site, main_fest, repo_name))
2055
+
2056
+ # Process remaining tasks - either sequentially or in parallel
2057
+ generated_results = {}
2058
+
2059
+ if self.parallel_generating_pseudocode and len(tasks_need_generation) > 1:
2060
+ self.logger.info(f"Parallel pseudocode generation enabled with {self.pseudocode_co_writer_num} workers")
2061
+ self.logger.info(f"Processing {len(tasks_need_generation)} tasks in parallel...")
2062
+
2063
+ def generate_single(task_tuple):
2064
+ paper, site, main_fest, repo_name = task_tuple
2065
+ return (paper, site), self._process_site_pseudocode(
2066
+ paper, site, main_fest, batch_refine, agent_refine, batch_size
2067
+ )
2068
+
2069
+ with ThreadPoolExecutor(max_workers=self.pseudocode_co_writer_num) as executor:
2070
+ futures = {executor.submit(generate_single, task): task for task in tasks_need_generation}
2071
+
2072
+ with tqdm(total=len(tasks_need_generation), desc="Generating pseudocode", unit="repo") as pbar:
2073
+ for future in as_completed(futures):
2074
+ key, result = future.result()
2075
+ if result is not None:
2076
+ generated_results[key] = result
2077
+ self.logger.info(f"Successfully generated pseudocode for {key[0]}/{key[1]}")
2078
+ else:
2079
+ self.logger.error(f"Failed to generate pseudocode for {key[0]}/{key[1]}")
2080
+ pbar.update(1)
2081
+ else:
2082
+ # Sequential processing
2083
+ self.logger.info(f"Processing {len(tasks_need_generation)} tasks sequentially...")
2084
+ for task in tqdm(tasks_need_generation, desc="Generating pseudocode", unit="repo"):
2085
+ paper, site, main_fest, repo_name = task
2086
+ result = self._process_site_pseudocode(
2087
+ paper, site, main_fest, batch_refine, agent_refine, batch_size
2088
+ )
2089
+ if result is not None:
2090
+ generated_results[(paper, site)] = result
2091
+
2092
+ # Combine cached and generated results
2093
+ all_results = {**cached_results, **generated_results}
2094
+
2095
+ # Organize results by paper
2096
+ paper_mainfests = []
2097
+ for paper, sites in zip(papers, papers_sites):
2098
+ if not sites:
2099
+ continue
2100
+
2101
+ paper_pseudocode = []
2102
+ paper_repo_names = []
2103
+ paper_repo_mainfests = []
2104
+ paper_concise_pseudocode = []
2105
+
2106
+ for site in sites:
2107
+ result = all_results.get((paper, site))
2108
+ if result is not None:
2109
+ paper_pseudocode.append(result["pseudocode"])
2110
+ paper_concise_pseudocode.append(result["initial_pseudocode"])
2111
+ paper_repo_names.append(result["repo_name"])
2112
+ paper_repo_mainfests.append(result["main_fest"])
2113
+ else:
2114
+ self.logger.error(f"No pseudocode result for {paper}/{site}")
2115
+
2116
+ if len(paper_pseudocode) > 0:
2117
+ paper_mainfests.append({
2118
+ "paper_id": paper,
2119
+ "paper_title": paper_repo_mainfests[0]["repo_info"].get("paper_title", "title loss"),
2120
+ "paper_abstract": paper_repo_mainfests[0]["repo_info"].get("paper_abstract", "abstract loss"),
2121
+ "pseudocodes": paper_pseudocode,
2122
+ "concise_pseudocodes": paper_concise_pseudocode,
2123
+ "repo_urls": sites,
2124
+ "repo_names": paper_repo_names,
2125
+ "mainfests": paper_repo_mainfests,
2126
+ })
2127
+ else:
2128
+ self.logger.error(f"Get no pseudocode for {paper}. Skip this paper's code repo")
2129
+
2130
+ self.logger.info(f"PAPER PSEUDOCODE SUCCESS NUM: {len(paper_mainfests)}")
2131
+ repo_num = 0
2132
+ for repo in paper_mainfests:
2133
+ repo_num += len(repo["repo_names"])
2134
+ self.logger.info(f"REPO PSEUDOCODE SUCCESS NUM: {repo_num}")
2135
+
2136
+ return paper_mainfests
2137
+
2138
+ def single_paper_test(config):
2139
+ # with open("/path/to/deep-survey/database/parsed_papers/2602.09372/auto/2602.09372.md", "r") as f:
2140
+ # md_text = f.read()
2141
+ code_collector = CodeCollector(config)
2142
+ work_collector = WorkCollector(config)
2143
+ code_analyzer = CodeAnalyzer(config, code_collector, work_collector)
2144
+
2145
+ md_text = work_collector.get_paper_raw_markdown("2406.10252")
2146
+ print(code_collector.extract_code_links("2406.10252" ,md_text))
2147
+ for site in code_collector.extract_code_links("2406.10252", md_text):
2148
+ code_collector._clone_repo(site)
2149
+ repo_name = code_collector._extract_repo_name(site)
2150
+ tree = code_analyzer.scan_repo_structure(repo_name)
2151
+ print(code_analyzer.format_repo_structure(tree))
2152
+
2153
+ mainfest = code_analyzer.build_mainfest("2406.10252", site, 8)
2154
+ print(code_analyzer.format_repo_structure(mainfest["repo_info"]["repo_structure"]))
2155
+ print(code_analyzer.format_mainfest(mainfest, True))
2156
+
2157
+ initial_pseudocode = code_analyzer.generate_project_pseudocode_from_main_files(mainfest)
2158
+ print(f"============= initial pseudocode ================")
2159
+ print(initial_pseudocode)
2160
+ final_pseudocode = code_analyzer.refine_project_pseudocode_with_core_files(mainfest = mainfest,
2161
+ current_pseudocode=initial_pseudocode,
2162
+ batch_size = 8)
2163
+ final_pseudocode = code_analyzer.refine_project_pseudocode_with_agent(mainfest = mainfest,
2164
+ current_pseudocode=initial_pseudocode,
2165
+ max_steps = 10)
2166
+ print(f"============= final pseudocode ================")
2167
+ print(final_pseudocode)
2168
+
2169
+ code_analyzer.cache_repo_pseudocode(repo_name, final_pseudocode)
2170
+
2171
+ @hydra.main(config_path="../config", config_name="deep_survey_batch_xiaomi", version_base=None)
2172
+ def main(config):
2173
+ config = merge_with_default_survey_config(config)
2174
+ code_collector = CodeCollector(config)
2175
+ work_collector = WorkCollector(config)
2176
+ code_analyzer = CodeAnalyzer(config, code_collector, work_collector)
2177
+ code_report_generator = CodeReportGenerator(config, work_collector=work_collector, code_collector=code_collector, code_analyzer=code_analyzer)
2178
+
2179
+ test_topics_paper = ["2406.10252", "2509.18661", "2510.07733", "2508.17647", "2503.04629", "2502.14776", "2504.05732v2"]
2180
+ topic = "LLM automated academic survey/review generation framework"
2181
+ paper_mainfests = code_analyzer.execute(test_topics_paper, batch_refine=True, agent_refine=True, batch_size=2)
2182
+ print("Genrating pseudocode report for test papers...")
2183
+ print("-------------------------------------------")
2184
+ print(code_report_generator.generate_report(papers = paper_mainfests, topic = topic, batch_size = 2))
2185
+ print("-------------------------------------------")
2186
+ print()
2187
+ print("-------------------------------------------")
2188
+ print("Genrating env report for test papers...")
2189
+ print(code_report_generator.generate_framework_env_report(paper_mainfests = paper_mainfests, topic = topic, batch_size = 2))
2190
+ print("-------------------------------------------")
2191
+
2192
+
2193
+ if __name__ == "__main__":
2194
+ main()