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.
- qwen_sci-0.2.2.dist-info/METADATA +492 -0
- qwen_sci-0.2.2.dist-info/RECORD +549 -0
- qwen_sci-0.2.2.dist-info/WHEEL +4 -0
- qwen_sci-0.2.2.dist-info/entry_points.txt +6 -0
- qwen_sci-0.2.2.dist-info/licenses/LICENSE +21 -0
- src/1.txt +1 -0
- src/__init__.py +1 -0
- src/__main__.py +5 -0
- src/agents/__init__.py +1 -0
- src/agents/experiment_design_agent/__init__.py +268 -0
- src/agents/experiment_design_agent/artifacts.py +747 -0
- src/agents/experiment_design_agent/cache.py +319 -0
- src/agents/experiment_design_agent/completeness.py +130 -0
- src/agents/experiment_design_agent/contracts.py +771 -0
- src/agents/experiment_design_agent/counterexample_analyzer.py +187 -0
- src/agents/experiment_design_agent/design_evidence_paper_screener.py +470 -0
- src/agents/experiment_design_agent/discipline_catalog.py +178 -0
- src/agents/experiment_design_agent/evidence_cards.py +338 -0
- src/agents/experiment_design_agent/evidence_planner.py +452 -0
- src/agents/experiment_design_agent/formal_reasoning_planner.py +884 -0
- src/agents/experiment_design_agent/fulltext_acquisition.py +863 -0
- src/agents/experiment_design_agent/idea_adapter.py +100 -0
- src/agents/experiment_design_agent/idea_intake.py +87 -0
- src/agents/experiment_design_agent/llm_json.py +328 -0
- src/agents/experiment_design_agent/orchestrator.py +1220 -0
- src/agents/experiment_design_agent/reasoning_context.py +183 -0
- src/agents/experiment_design_agent/reasoning_validation.py +546 -0
- src/agents/experiment_design_agent/run.py +481 -0
- src/agents/experiment_design_agent/run_logging.py +292 -0
- src/agents/experiment_design_agent/scope_gate.py +198 -0
- src/agents/experiment_design_agent/study_type_composer.py +1360 -0
- src/agents/experiment_design_agent/survey_evidence.py +1381 -0
- src/agents/experiment_design_agent/template_router.py +195 -0
- src/agents/experiment_design_agent/variable_claim_extractor.py +93 -0
- src/agents/idea_agent/__init__.py +1 -0
- src/agents/idea_agent/agent/__init__.py +6 -0
- src/agents/idea_agent/agent/artifacts.py +308 -0
- src/agents/idea_agent/agent/base.py +122 -0
- src/agents/idea_agent/agent/ligagent.py +488 -0
- src/agents/idea_agent/agent/mcts.py +2060 -0
- src/agents/idea_agent/agent/prompts/__init__.py +48 -0
- src/agents/idea_agent/agent/prompts/advanced_analysis.py +235 -0
- src/agents/idea_agent/agent/prompts/algorithm_alignment.py +55 -0
- src/agents/idea_agent/agent/prompts/algorithm_structuring.py +48 -0
- src/agents/idea_agent/agent/prompts/component_extraction.py +62 -0
- src/agents/idea_agent/agent/prompts/component_novelty_evaluation.py +54 -0
- src/agents/idea_agent/agent/prompts/experiment_findings_extraction.py +39 -0
- src/agents/idea_agent/agent/prompts/idea_fusion.py +295 -0
- src/agents/idea_agent/agent/prompts/idea_introduction.py +30 -0
- src/agents/idea_agent/agent/prompts/idea_result_alignment.py +39 -0
- src/agents/idea_agent/agent/prompts/input_interpreter.py +52 -0
- src/agents/idea_agent/agent/prompts/keynote_ops.py +78 -0
- src/agents/idea_agent/agent/prompts/mcts_evaluation.py +235 -0
- src/agents/idea_agent/agent/prompts/mcts_generation.py +140 -0
- src/agents/idea_agent/agent/prompts/mechanism_commit_query.py +99 -0
- src/agents/idea_agent/agent/prompts/memory_selection.py +0 -0
- src/agents/idea_agent/agent/prompts/prompt_modes.py +16 -0
- src/agents/idea_agent/agent/prompts/rag_query.py +34 -0
- src/agents/idea_agent/agent/prompts/re_analysis_replan.py +48 -0
- src/agents/idea_agent/agent/prompts/reference_grounding.py +34 -0
- src/agents/idea_agent/agent/prompts/root_domain_classification.py +32 -0
- src/agents/idea_agent/agent/prompts/scientific_debate.py +88 -0
- src/agents/idea_agent/agent/prompts/scientific_materialization.py +45 -0
- src/agents/idea_agent/agent/prompts/skill_instantiation.py +193 -0
- src/agents/idea_agent/agent/prompts/theory_transfer_query.py +93 -0
- src/agents/idea_agent/agent/prompts/topic_background.py +15 -0
- src/agents/idea_agent/agent/skills/DEFAULT_SKILL_TEMPLATES.json +263 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/SKILL.md +40 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/alternative-path-contrast/references/contrast_patterns.md +22 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/SKILL.md +40 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/feedback-closed-loop/references/mechanism_cards.md +24 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/SKILL.md +39 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/hierarchical-decomposition/references/hierarchy_patterns.md +22 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/SKILL.md +39 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/mechanism-commit-innovation/references/mechanism_commit_patterns.md +24 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/SKILL.md +39 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/multi-scale-coordinator/references/coordination_patterns.md +22 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/SKILL.md +41 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/speculative-execution-with-repair/references/speculation_patterns.md +22 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/SKILL.md +39 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/surgical-modularity/references/modularity_patterns.md +22 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/SKILL.md +40 -0
- src/agents/idea_agent/agent/skills/edit_operator_skills/theory-transfer-injection/references/transfer_patterns.md +24 -0
- src/agents/idea_agent/example/idea_result.json +900 -0
- src/agents/idea_agent/run.py +276 -0
- src/agents/idea_agent/tests/test_advanced_analysis_contract.py +90 -0
- src/agents/idea_agent/tests/test_gap_hypothesis_bridge.py +166 -0
- src/agents/idea_agent/tests/test_idea_debate.py +480 -0
- src/agents/idea_agent/tests/test_idea_result_v5.py +99 -0
- src/agents/idea_agent/tests/test_idea_synthesis.py +92 -0
- src/agents/idea_agent/tests/test_mature_idea_sources.py +27 -0
- src/agents/idea_agent/tests/test_multimodal_idea_integration.py +386 -0
- src/agents/idea_agent/tests/test_profile_materialization_and_scoring.py +219 -0
- src/agents/idea_agent/tests/test_profile_native_skill_rendering.py +122 -0
- src/agents/idea_agent/tests/test_result_persistence.py +183 -0
- src/agents/idea_agent/tests/test_scientific_intervention_profiles.py +241 -0
- src/agents/idea_agent/tests/test_scientific_rubric_and_prompts.py +559 -0
- src/agents/idea_agent/tests/test_strict_json_object_responses.py +48 -0
- src/agents/idea_agent/utils/core/__init__.py +1 -0
- src/agents/idea_agent/utils/core/ablation_inputs.py +60 -0
- src/agents/idea_agent/utils/core/chat_errors.py +95 -0
- src/agents/idea_agent/utils/core/chat_router.py +130 -0
- src/agents/idea_agent/utils/core/chat_transport.py +188 -0
- src/agents/idea_agent/utils/core/config_loader.py +166 -0
- src/agents/idea_agent/utils/core/json_utils.py +28 -0
- src/agents/idea_agent/utils/core/logger.py +287 -0
- src/agents/idea_agent/utils/core/progress.py +58 -0
- src/agents/idea_agent/utils/core/response_parsing.py +57 -0
- src/agents/idea_agent/utils/core/run_inputs.py +198 -0
- src/agents/idea_agent/utils/mcts/__init__.py +1 -0
- src/agents/idea_agent/utils/mcts/component_novelty.py +342 -0
- src/agents/idea_agent/utils/mcts/defect_registry.py +302 -0
- src/agents/idea_agent/utils/mcts/idea_routes.py +61 -0
- src/agents/idea_agent/utils/mcts/idea_taste_presets.py +312 -0
- src/agents/idea_agent/utils/mcts/mcts_helpers.py +738 -0
- src/agents/idea_agent/utils/mcts/mcts_runtime.py +3000 -0
- src/agents/idea_agent/utils/mcts/scientific_intervention_ontology.py +1561 -0
- src/agents/idea_agent/utils/mcts/scientific_rubric.py +211 -0
- src/agents/idea_agent/utils/mcts/skill_parsing.py +92 -0
- src/agents/idea_agent/utils/papers/__init__.py +1 -0
- src/agents/idea_agent/utils/papers/paper_graph_vector_store.py +499 -0
- src/agents/idea_agent/utils/papers/paper_repository.py +372 -0
- src/agents/idea_agent/utils/prompting/__init__.py +1 -0
- src/agents/idea_agent/utils/prompting/prompt_views.py +637 -0
- src/agents/idea_agent/utils/workflow/__init__.py +1 -0
- src/agents/idea_agent/utils/workflow/advanced_analysis_contract.py +244 -0
- src/agents/idea_agent/utils/workflow/gap_hypothesis_bridge.py +461 -0
- src/agents/idea_agent/utils/workflow/idea_contract.py +426 -0
- src/agents/idea_agent/utils/workflow/idea_debate.py +1238 -0
- src/agents/idea_agent/utils/workflow/idea_diversity.py +163 -0
- src/agents/idea_agent/utils/workflow/idea_fusion.py +889 -0
- src/agents/idea_agent/utils/workflow/idea_helpers.py +478 -0
- src/agents/idea_agent/utils/workflow/idea_portfolio.py +422 -0
- src/agents/idea_agent/utils/workflow/idea_synthesis.py +482 -0
- src/agents/idea_agent/utils/workflow/ligagent_flow.py +581 -0
- src/agents/idea_agent/utils/workflow/ligagent_handlers.py +1836 -0
- src/agents/idea_agent/utils/workflow/ligagent_helpers.py +1331 -0
- src/agents/idea_agent/utils/workflow/ligagent_utils.py +322 -0
- src/agents/idea_agent/utils/workflow/mature_idea_sources.py +648 -0
- src/agents/idea_agent/utils/workflow/multimodal_data_anchoring.py +578 -0
- src/agents/idea_agent/utils/workflow/result_persistence.py +774 -0
- src/agents/idea_agent/utils/workflow/stage_contract.py +76 -0
- src/agents/idea_agent/utils/workflow/workflow_runtime.py +288 -0
- src/agents/research_plan_author/__init__.py +50 -0
- src/agents/research_plan_author/artifacts.py +270 -0
- src/agents/research_plan_author/authoring_blueprint.py +1246 -0
- src/agents/research_plan_author/bibtex_renderer.py +252 -0
- src/agents/research_plan_author/contract_repair.py +131 -0
- src/agents/research_plan_author/contracts.py +745 -0
- src/agents/research_plan_author/cross_section_editor.py +232 -0
- src/agents/research_plan_author/document_quality.py +987 -0
- src/agents/research_plan_author/idea_evolution.py +223 -0
- src/agents/research_plan_author/input_loader.py +76 -0
- src/agents/research_plan_author/latex_compiler.py +260 -0
- src/agents/research_plan_author/latex_safety.py +159 -0
- src/agents/research_plan_author/llm_json.py +74 -0
- src/agents/research_plan_author/markdown_renderer.py +275 -0
- src/agents/research_plan_author/pdf_validator.py +149 -0
- src/agents/research_plan_author/render.py +166 -0
- src/agents/research_plan_author/render_artifacts.py +187 -0
- src/agents/research_plan_author/run.py +765 -0
- src/agents/research_plan_author/run_logging.py +209 -0
- src/agents/research_plan_author/section_cache.py +184 -0
- src/agents/research_plan_author/section_composer.py +1764 -0
- src/agents/research_plan_author/section_router.py +209 -0
- src/agents/research_plan_author/semantic_validator.py +254 -0
- src/agents/research_plan_author/source_bundle.py +46 -0
- src/agents/research_plan_author/source_registry.py +208 -0
- src/agents/research_plan_author/survey_source_loader.py +137 -0
- src/agents/research_plan_author/template_adapter.py +173 -0
- src/agents/research_plan_author/template_profile.py +204 -0
- src/agents/research_plan_author/tex_renderer.py +496 -0
- src/agents/research_plan_author/theory_presentation.py +134 -0
- src/agents/research_plan_author/theory_spine.py +703 -0
- src/agents/survey_agent/Dockerfile +54 -0
- src/agents/survey_agent/README.md +350 -0
- src/agents/survey_agent/config/deep_survey.yaml +217 -0
- src/agents/survey_agent/config/deep_survey_batch.yaml +176 -0
- src/agents/survey_agent/config/deep_survey_fast.yaml +183 -0
- src/agents/survey_agent/config/evaluate_baseline.yaml +197 -0
- src/agents/survey_agent/config/outcomeRAG.yaml +159 -0
- src/agents/survey_agent/env_report.md +348 -0
- src/agents/survey_agent/modules/__init__.py +0 -0
- src/agents/survey_agent/modules/code_collector.py +2194 -0
- src/agents/survey_agent/modules/code_report_generator.py +1282 -0
- src/agents/survey_agent/modules/data_manager.py +1713 -0
- src/agents/survey_agent/modules/database.py +167 -0
- src/agents/survey_agent/modules/fulltext_download_cache.py +462 -0
- src/agents/survey_agent/modules/fulltext_http.py +134 -0
- src/agents/survey_agent/modules/fulltext_resolution.py +749 -0
- src/agents/survey_agent/modules/judge.py +434 -0
- src/agents/survey_agent/modules/outcome_RAG.py +236 -0
- src/agents/survey_agent/modules/paper_graph_retriever.py +1075 -0
- src/agents/survey_agent/modules/pe.py +1729 -0
- src/agents/survey_agent/modules/pseudo_creator.py +782 -0
- src/agents/survey_agent/modules/pseudo_reviser.py +537 -0
- src/agents/survey_agent/modules/refine_agent.py +1952 -0
- src/agents/survey_agent/modules/survey_generator.py +5691 -0
- src/agents/survey_agent/modules/survey_visualizer.py +1817 -0
- src/agents/survey_agent/modules/visual_prompts.py +327 -0
- src/agents/survey_agent/modules/work_analyzer.py +2750 -0
- src/agents/survey_agent/modules/work_collector.py +5649 -0
- src/agents/survey_agent/scripts/baseline_evaluation.py +411 -0
- src/agents/survey_agent/scripts/run_deep_survey.py +224 -0
- src/agents/survey_agent/scripts/run_deep_survey_adapter.py +407 -0
- src/agents/survey_agent/scripts/run_deep_survey_adapter_with_input.py +409 -0
- src/agents/survey_agent/scripts/run_deep_survey_batch.py +228 -0
- src/agents/survey_agent/scripts/run_deep_survey_batch_arg.py +243 -0
- src/agents/survey_agent/scripts/run_deep_survey_batch_settings.py +218 -0
- src/agents/survey_agent/scripts/run_keynotes_gen.py +128 -0
- src/agents/survey_agent/scripts/run_work_clusters.py +140 -0
- src/agents/survey_agent/utils/__init__.py +0 -0
- src/agents/survey_agent/utils/ablation_utils.py +317 -0
- src/agents/survey_agent/utils/api_call.py +2518 -0
- src/agents/survey_agent/utils/chat_utils.py +82 -0
- src/agents/survey_agent/utils/config_utils.py +70 -0
- src/agents/survey_agent/utils/convert_to_md.py +29 -0
- src/agents/survey_agent/utils/err_info.py +24 -0
- src/agents/survey_agent/utils/file_utils.py +176 -0
- src/agents/survey_agent/utils/gpu_utils.py +145 -0
- src/agents/survey_agent/utils/graph_visualizer.py +279 -0
- src/agents/survey_agent/utils/html_utils.py +181 -0
- src/agents/survey_agent/utils/latex_generator_intra_cluster.py +529 -0
- src/agents/survey_agent/utils/latex_generator_relation_table.py +193 -0
- src/agents/survey_agent/utils/mineru_section_packer.py +334 -0
- src/agents/survey_agent/utils/mineru_utils.py +396 -0
- src/agents/survey_agent/utils/paper.py +144 -0
- src/agents/survey_agent/utils/read_yaml.py +15 -0
- src/agents/survey_agent/utils/repo_utils.py +167 -0
- src/agents/survey_agent/utils/rich_logger.py +77 -0
- src/agents/survey_agent/utils/step2v2.py +874 -0
- src/agents/survey_agent/utils/step2v2_extractor.py +856 -0
- src/agents/survey_agent/utils/topic_survey_storage.py +309 -0
- src/agents/survey_agent/utils/utils.py +118 -0
- src/agents/survey_agent/utils/work.py +2 -0
- src/cli.py +1611 -0
- src/config/__init__.py +213 -0
- src/config/default.yaml +1122 -0
- src/harness/LICENSE +21 -0
- src/harness/VENDORED.md +14 -0
- src/harness/src/__init__.py +0 -0
- src/harness/src/openharness/__init__.py +0 -0
- src/harness/src/openharness/__main__.py +6 -0
- src/harness/src/openharness/api/__init__.py +21 -0
- src/harness/src/openharness/api/client.py +281 -0
- src/harness/src/openharness/api/codex_client.py +407 -0
- src/harness/src/openharness/api/copilot_auth.py +240 -0
- src/harness/src/openharness/api/copilot_client.py +134 -0
- src/harness/src/openharness/api/errors.py +19 -0
- src/harness/src/openharness/api/openai_client.py +577 -0
- src/harness/src/openharness/api/provider.py +186 -0
- src/harness/src/openharness/api/registry.py +437 -0
- src/harness/src/openharness/api/usage.py +17 -0
- src/harness/src/openharness/auth/__init__.py +29 -0
- src/harness/src/openharness/auth/external.py +610 -0
- src/harness/src/openharness/auth/flows.py +189 -0
- src/harness/src/openharness/auth/manager.py +482 -0
- src/harness/src/openharness/auth/storage.py +269 -0
- src/harness/src/openharness/autopilot/__init__.py +23 -0
- src/harness/src/openharness/autopilot/service.py +2239 -0
- src/harness/src/openharness/autopilot/types.py +91 -0
- src/harness/src/openharness/bridge/__init__.py +20 -0
- src/harness/src/openharness/bridge/manager.py +106 -0
- src/harness/src/openharness/bridge/session_runner.py +46 -0
- src/harness/src/openharness/bridge/types.py +37 -0
- src/harness/src/openharness/bridge/work_secret.py +41 -0
- src/harness/src/openharness/channels/UPSTREAM +6 -0
- src/harness/src/openharness/channels/__init__.py +22 -0
- src/harness/src/openharness/channels/adapter.py +131 -0
- src/harness/src/openharness/channels/bus/__init__.py +6 -0
- src/harness/src/openharness/channels/bus/events.py +38 -0
- src/harness/src/openharness/channels/bus/queue.py +44 -0
- src/harness/src/openharness/channels/impl/__init__.py +6 -0
- src/harness/src/openharness/channels/impl/base.py +141 -0
- src/harness/src/openharness/channels/impl/dingtalk.py +445 -0
- src/harness/src/openharness/channels/impl/discord.py +311 -0
- src/harness/src/openharness/channels/impl/email.py +410 -0
- src/harness/src/openharness/channels/impl/feishu.py +1342 -0
- src/harness/src/openharness/channels/impl/manager.py +257 -0
- src/harness/src/openharness/channels/impl/matrix.py +700 -0
- src/harness/src/openharness/channels/impl/mochat.py +897 -0
- src/harness/src/openharness/channels/impl/qq.py +141 -0
- src/harness/src/openharness/channels/impl/slack.py +284 -0
- src/harness/src/openharness/channels/impl/telegram.py +525 -0
- src/harness/src/openharness/channels/impl/whatsapp.py +159 -0
- src/harness/src/openharness/cli.py +2551 -0
- src/harness/src/openharness/commands/__init__.py +21 -0
- src/harness/src/openharness/commands/registry.py +2783 -0
- src/harness/src/openharness/config/__init__.py +34 -0
- src/harness/src/openharness/config/paths.py +160 -0
- src/harness/src/openharness/config/schema.py +119 -0
- src/harness/src/openharness/config/settings.py +1104 -0
- src/harness/src/openharness/coordinator/__init__.py +12 -0
- src/harness/src/openharness/coordinator/agent_definitions.py +975 -0
- src/harness/src/openharness/coordinator/coordinator_mode.py +520 -0
- src/harness/src/openharness/engine/__init__.py +80 -0
- src/harness/src/openharness/engine/cost_tracker.py +24 -0
- src/harness/src/openharness/engine/messages.py +222 -0
- src/harness/src/openharness/engine/query.py +1188 -0
- src/harness/src/openharness/engine/query_engine.py +313 -0
- src/harness/src/openharness/engine/stream_events.py +90 -0
- src/harness/src/openharness/hooks/__init__.py +50 -0
- src/harness/src/openharness/hooks/events.py +20 -0
- src/harness/src/openharness/hooks/executor.py +242 -0
- src/harness/src/openharness/hooks/hot_reload.py +31 -0
- src/harness/src/openharness/hooks/loader.py +68 -0
- src/harness/src/openharness/hooks/schemas.py +66 -0
- src/harness/src/openharness/hooks/types.py +38 -0
- src/harness/src/openharness/keybindings/__init__.py +14 -0
- src/harness/src/openharness/keybindings/default_bindings.py +11 -0
- src/harness/src/openharness/keybindings/loader.py +22 -0
- src/harness/src/openharness/keybindings/parser.py +18 -0
- src/harness/src/openharness/keybindings/resolver.py +13 -0
- src/harness/src/openharness/mcp/__init__.py +79 -0
- src/harness/src/openharness/mcp/client.py +298 -0
- src/harness/src/openharness/mcp/config.py +16 -0
- src/harness/src/openharness/mcp/types.py +76 -0
- src/harness/src/openharness/memory/__init__.py +25 -0
- src/harness/src/openharness/memory/agent.py +91 -0
- src/harness/src/openharness/memory/manager.py +183 -0
- src/harness/src/openharness/memory/memdir.py +52 -0
- src/harness/src/openharness/memory/migrate.py +137 -0
- src/harness/src/openharness/memory/paths.py +22 -0
- src/harness/src/openharness/memory/relevance.py +145 -0
- src/harness/src/openharness/memory/scan.py +116 -0
- src/harness/src/openharness/memory/schema.py +443 -0
- src/harness/src/openharness/memory/search.py +71 -0
- src/harness/src/openharness/memory/team.py +90 -0
- src/harness/src/openharness/memory/types.py +33 -0
- src/harness/src/openharness/memory/usage.py +153 -0
- src/harness/src/openharness/output_styles/__init__.py +5 -0
- src/harness/src/openharness/output_styles/loader.py +42 -0
- src/harness/src/openharness/permissions/__init__.py +26 -0
- src/harness/src/openharness/permissions/checker.py +200 -0
- src/harness/src/openharness/permissions/modes.py +13 -0
- src/harness/src/openharness/personalization/__init__.py +6 -0
- src/harness/src/openharness/personalization/extractor.py +138 -0
- src/harness/src/openharness/personalization/rules.py +64 -0
- src/harness/src/openharness/personalization/session_hook.py +65 -0
- src/harness/src/openharness/platforms.py +86 -0
- src/harness/src/openharness/plugins/__init__.py +53 -0
- src/harness/src/openharness/plugins/bundled/__init__.py +0 -0
- src/harness/src/openharness/plugins/installer.py +39 -0
- src/harness/src/openharness/plugins/loader.py +730 -0
- src/harness/src/openharness/plugins/schemas.py +24 -0
- src/harness/src/openharness/plugins/types.py +59 -0
- src/harness/src/openharness/prompts/__init__.py +14 -0
- src/harness/src/openharness/prompts/claudemd.py +48 -0
- src/harness/src/openharness/prompts/context.py +187 -0
- src/harness/src/openharness/prompts/environment.py +135 -0
- src/harness/src/openharness/prompts/system_prompt.py +109 -0
- src/harness/src/openharness/sandbox/Dockerfile +6 -0
- src/harness/src/openharness/sandbox/__init__.py +33 -0
- src/harness/src/openharness/sandbox/adapter.py +148 -0
- src/harness/src/openharness/sandbox/docker_backend.py +232 -0
- src/harness/src/openharness/sandbox/docker_image.py +103 -0
- src/harness/src/openharness/sandbox/path_validator.py +37 -0
- src/harness/src/openharness/sandbox/session.py +63 -0
- src/harness/src/openharness/services/__init__.py +30 -0
- src/harness/src/openharness/services/autodream/__init__.py +34 -0
- src/harness/src/openharness/services/autodream/backup.py +104 -0
- src/harness/src/openharness/services/autodream/lock.py +138 -0
- src/harness/src/openharness/services/autodream/prompt.py +128 -0
- src/harness/src/openharness/services/autodream/service.py +313 -0
- src/harness/src/openharness/services/compact/__init__.py +1725 -0
- src/harness/src/openharness/services/cron.py +137 -0
- src/harness/src/openharness/services/cron_scheduler.py +595 -0
- src/harness/src/openharness/services/lsp/__init__.py +216 -0
- src/harness/src/openharness/services/memory_extract/__init__.py +261 -0
- src/harness/src/openharness/services/oauth/__init__.py +0 -0
- src/harness/src/openharness/services/session_backend.py +97 -0
- src/harness/src/openharness/services/session_memory/__init__.py +139 -0
- src/harness/src/openharness/services/session_storage.py +230 -0
- src/harness/src/openharness/services/token_estimation.py +15 -0
- src/harness/src/openharness/services/tool_outputs.py +55 -0
- src/harness/src/openharness/skills/__init__.py +44 -0
- src/harness/src/openharness/skills/_frontmatter.py +97 -0
- src/harness/src/openharness/skills/bundled/__init__.py +75 -0
- src/harness/src/openharness/skills/bundled/content/commit.md +25 -0
- src/harness/src/openharness/skills/bundled/content/debug.md +25 -0
- src/harness/src/openharness/skills/bundled/content/diagnose.md +35 -0
- src/harness/src/openharness/skills/bundled/content/plan.md +39 -0
- src/harness/src/openharness/skills/bundled/content/review.md +33 -0
- src/harness/src/openharness/skills/bundled/content/simplify.md +26 -0
- src/harness/src/openharness/skills/bundled/content/skill-creator.md +175 -0
- src/harness/src/openharness/skills/bundled/content/test.md +38 -0
- src/harness/src/openharness/skills/loader.py +229 -0
- src/harness/src/openharness/skills/registry.py +29 -0
- src/harness/src/openharness/skills/types.py +24 -0
- src/harness/src/openharness/state/__init__.py +6 -0
- src/harness/src/openharness/state/app_state.py +30 -0
- src/harness/src/openharness/state/store.py +40 -0
- src/harness/src/openharness/swarm/__init__.py +70 -0
- src/harness/src/openharness/swarm/in_process.py +693 -0
- src/harness/src/openharness/swarm/lockfile.py +24 -0
- src/harness/src/openharness/swarm/mailbox.py +522 -0
- src/harness/src/openharness/swarm/permission_sync.py +1168 -0
- src/harness/src/openharness/swarm/registry.py +410 -0
- src/harness/src/openharness/swarm/spawn_utils.py +229 -0
- src/harness/src/openharness/swarm/subprocess_backend.py +171 -0
- src/harness/src/openharness/swarm/team_lifecycle.py +910 -0
- src/harness/src/openharness/swarm/types.py +398 -0
- src/harness/src/openharness/swarm/worktree.py +315 -0
- src/harness/src/openharness/tasks/__init__.py +18 -0
- src/harness/src/openharness/tasks/local_agent_task.py +28 -0
- src/harness/src/openharness/tasks/local_shell_task.py +17 -0
- src/harness/src/openharness/tasks/manager.py +475 -0
- src/harness/src/openharness/tasks/stop_task.py +11 -0
- src/harness/src/openharness/tasks/types.py +32 -0
- src/harness/src/openharness/themes/__init__.py +21 -0
- src/harness/src/openharness/themes/builtin.py +89 -0
- src/harness/src/openharness/themes/loader.py +55 -0
- src/harness/src/openharness/themes/schema.py +54 -0
- src/harness/src/openharness/tools/__init__.py +107 -0
- src/harness/src/openharness/tools/agent_tool.py +141 -0
- src/harness/src/openharness/tools/ask_user_question_tool.py +46 -0
- src/harness/src/openharness/tools/base.py +80 -0
- src/harness/src/openharness/tools/bash_tool.py +259 -0
- src/harness/src/openharness/tools/brief_tool.py +33 -0
- src/harness/src/openharness/tools/config_tool.py +54 -0
- src/harness/src/openharness/tools/cron_create_tool.py +105 -0
- src/harness/src/openharness/tools/cron_delete_tool.py +32 -0
- src/harness/src/openharness/tools/cron_list_tool.py +69 -0
- src/harness/src/openharness/tools/cron_toggle_tool.py +37 -0
- src/harness/src/openharness/tools/enter_plan_mode_tool.py +28 -0
- src/harness/src/openharness/tools/enter_worktree_tool.py +80 -0
- src/harness/src/openharness/tools/exit_plan_mode_tool.py +28 -0
- src/harness/src/openharness/tools/exit_worktree_tool.py +42 -0
- src/harness/src/openharness/tools/file_edit_tool.py +95 -0
- src/harness/src/openharness/tools/file_read_tool.py +72 -0
- src/harness/src/openharness/tools/file_write_tool.py +87 -0
- src/harness/src/openharness/tools/glob_tool.py +175 -0
- src/harness/src/openharness/tools/grep_tool.py +374 -0
- src/harness/src/openharness/tools/image_generation_tool.py +447 -0
- src/harness/src/openharness/tools/image_to_text_tool.py +237 -0
- src/harness/src/openharness/tools/list_mcp_resources_tool.py +36 -0
- src/harness/src/openharness/tools/lsp_tool.py +154 -0
- src/harness/src/openharness/tools/mcp_auth_tool.py +71 -0
- src/harness/src/openharness/tools/mcp_tool.py +72 -0
- src/harness/src/openharness/tools/notebook_edit_tool.py +97 -0
- src/harness/src/openharness/tools/read_mcp_resource_tool.py +38 -0
- src/harness/src/openharness/tools/remote_trigger_tool.py +74 -0
- src/harness/src/openharness/tools/send_message_tool.py +58 -0
- src/harness/src/openharness/tools/skill_tool.py +43 -0
- src/harness/src/openharness/tools/sleep_tool.py +32 -0
- src/harness/src/openharness/tools/task_create_tool.py +56 -0
- src/harness/src/openharness/tools/task_get_tool.py +33 -0
- src/harness/src/openharness/tools/task_list_tool.py +35 -0
- src/harness/src/openharness/tools/task_output_tool.py +35 -0
- src/harness/src/openharness/tools/task_stop_tool.py +30 -0
- src/harness/src/openharness/tools/task_update_tool.py +50 -0
- src/harness/src/openharness/tools/team_create_tool.py +31 -0
- src/harness/src/openharness/tools/team_delete_tool.py +30 -0
- src/harness/src/openharness/tools/todo_write_tool.py +46 -0
- src/harness/src/openharness/tools/tool_search_tool.py +38 -0
- src/harness/src/openharness/tools/web_fetch_tool.py +117 -0
- src/harness/src/openharness/tools/web_search_tool.py +119 -0
- src/harness/src/openharness/ui/__init__.py +5 -0
- src/harness/src/openharness/ui/app.py +320 -0
- src/harness/src/openharness/ui/backend_host.py +941 -0
- src/harness/src/openharness/ui/coordinator_drain.py +198 -0
- src/harness/src/openharness/ui/input.py +32 -0
- src/harness/src/openharness/ui/output.py +265 -0
- src/harness/src/openharness/ui/permission_dialog.py +14 -0
- src/harness/src/openharness/ui/protocol.py +247 -0
- src/harness/src/openharness/ui/react_launcher.py +179 -0
- src/harness/src/openharness/ui/runtime.py +799 -0
- src/harness/src/openharness/ui/textual_app.py +495 -0
- src/harness/src/openharness/utils/__init__.py +0 -0
- src/harness/src/openharness/utils/file_lock.py +86 -0
- src/harness/src/openharness/utils/fs.py +98 -0
- src/harness/src/openharness/utils/helpers.py +77 -0
- src/harness/src/openharness/utils/network_guard.py +340 -0
- src/harness/src/openharness/utils/shell.py +147 -0
- src/harness/src/openharness/vim/__init__.py +5 -0
- src/harness/src/openharness/vim/transitions.py +8 -0
- src/harness/src/openharness/voice/__init__.py +7 -0
- src/harness/src/openharness/voice/keyterms.py +10 -0
- src/harness/src/openharness/voice/stream_stt.py +8 -0
- src/harness/src/openharness/voice/voice_mode.py +44 -0
- src/llm/__init__.py +29 -0
- src/llm/image_generation.py +402 -0
- src/llm/provider_registry.py +419 -0
- src/llm/runtime_env.py +64 -0
- src/llm/structured_output.py +131 -0
- src/llm/vision.py +198 -0
- src/memory/__init__.py +2 -0
- src/memory/api/__init__.py +1 -0
- src/memory/api/base_memory_system_api.py +98 -0
- src/memory/api/base_symbolic_memory_system_api.py +190 -0
- src/memory/api/base_vector_memory_system_api.py +116 -0
- src/memory/api/faiss_memory_system_api.py +228 -0
- src/memory/api/slot_process_api.py +827 -0
- src/memory/api/symbolic_memory_system_api.py +572 -0
- src/memory/memory_system/__init__.py +48 -0
- src/memory/memory_system/component_taxonomy.py +430 -0
- src/memory/memory_system/llm.py +437 -0
- src/memory/memory_system/models.py +168 -0
- src/memory/memory_system/user_prompt.py +418 -0
- src/memory/memory_system/utils.py +282 -0
- src/memory/memory_system/vectorstore.py +311 -0
- src/memory/memory_system/working_slot.py +99 -0
- src/pipeline/__init__.py +129 -0
- src/pipeline/contracts.py +531 -0
- src/pipeline/discipline_taxonomy.py +916 -0
- src/pipeline/evidence_coverage_ledger.py +1150 -0
- src/pipeline/evidence_refinement.py +620 -0
- src/pipeline/experiment_to_symbolic.py +137 -0
- src/pipeline/multimodal_evidence/__init__.py +28 -0
- src/pipeline/multimodal_evidence/capabilities.py +107 -0
- src/pipeline/multimodal_evidence/claims.py +135 -0
- src/pipeline/multimodal_evidence/contract.py +291 -0
- src/pipeline/multimodal_evidence/data_sh_compiler.py +505 -0
- src/pipeline/multimodal_evidence/handoff_binding.py +135 -0
- src/pipeline/multimodal_evidence/manifest.py +254 -0
- src/pipeline/multimodal_evidence/native_analysis.py +479 -0
- src/pipeline/multimodal_evidence/perception.py +277 -0
- src/pipeline/multimodal_evidence/query_binding.py +65 -0
- src/pipeline/multimodal_evidence/rendering.py +120 -0
- src/pipeline/multimodal_evidence/safety.py +42 -0
- src/pipeline/multimodal_evidence/sampling.py +69 -0
- src/pipeline/multimodal_evidence/service.py +148 -0
- src/pipeline/multimodal_evidence/survey_integration.py +460 -0
- src/pipeline/paper_identity.py +52 -0
- src/pipeline/research_design_inventory.py +338 -0
- src/pipeline/research_domain_resolution.py +669 -0
- src/pipeline/research_identity.py +548 -0
- src/pipeline/research_question_contract.py +646 -0
- src/pipeline/retrieval_lanes.py +889 -0
- src/pipeline/run_loop.py +862 -0
- src/pipeline/science_events.py +30 -0
- src/pipeline/science_manifests.py +602 -0
- src/pipeline/science_run.py +918 -0
- src/pipeline/science_stages.py +709 -0
- src/pipeline/science_workflow.py +807 -0
- src/pipeline/sh_cluster_projection.py +306 -0
- src/pipeline/sh_graph_provenance.py +648 -0
- src/pipeline/subhypothesis_decomposition.py +392 -0
- src/pipeline/subhypothesis_evidence.py +856 -0
- src/pipeline/survey_evidence_plan.py +858 -0
- src/pipeline/survey_gap_adjudication.py +647 -0
- src/pipeline/survey_gap_candidates.py +840 -0
- src/pipeline/survey_gap_ledger.py +683 -0
- src/pipeline/survey_gap_triage.py +401 -0
- src/pipeline/survey_handoff_persistence.py +481 -0
- src/pipeline/survey_handoff_projection.py +457 -0
- src/pipeline/survey_idea_handoff.py +1112 -0
- src/pipeline/survey_idea_loader.py +289 -0
- 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()
|