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,159 @@
|
|
|
1
|
+
"""Small, shared safety primitives for generated LaTeX and BibTeX."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LatexSafetyError(ValueError):
|
|
9
|
+
"""Raised when document-controlled text is unsafe for the renderer."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_OBSERVED_RESULT_LANGUAGE = re.compile(
|
|
13
|
+
r"\b(?:we|this\s+(?:study|research|work|investigation|analysis)|"
|
|
14
|
+
r"the\s+(?:study|experiment|analysis|evaluation))\s+(?:have\s+)?"
|
|
15
|
+
r"(?:observed|found|demonstrated|showed|measured|established|confirmed|revealed)\b"
|
|
16
|
+
r"|\b(?:the\s+)?(?:results|experiments|analyses|data|evaluation)\s+(?:have\s+)?"
|
|
17
|
+
r"(?:observed|found|demonstrated|showed|measured|established|confirmed|revealed)\b",
|
|
18
|
+
re.IGNORECASE,
|
|
19
|
+
)
|
|
20
|
+
_FORBIDDEN_TEX_COMMANDS = re.compile(
|
|
21
|
+
r"\\(?:input|include|write|openout|read|catcode|newcommand|renewcommand|def|gdef|"
|
|
22
|
+
r"usepackage|documentclass|immediate|write18|csname|expandafter|every|loop|directlua)\b",
|
|
23
|
+
re.IGNORECASE,
|
|
24
|
+
)
|
|
25
|
+
_MATH_COMMAND = re.compile(r"\\([A-Za-z]+)")
|
|
26
|
+
_ALLOWED_MATH_COMMANDS = {
|
|
27
|
+
"alpha", "beta", "gamma", "delta", "epsilon", "varepsilon", "zeta", "eta", "theta",
|
|
28
|
+
"vartheta", "iota", "kappa", "lambda", "mu", "nu", "xi", "pi", "rho", "sigma",
|
|
29
|
+
"tau", "upsilon", "phi", "varphi", "chi", "psi", "omega", "Gamma", "Delta", "Theta",
|
|
30
|
+
"Lambda", "Xi", "Pi", "Sigma", "Phi", "Psi", "Omega", "frac", "sqrt", "sum", "prod",
|
|
31
|
+
"int", "lim", "log", "ln", "exp", "sin", "cos", "tan", "min", "max", "sup", "inf",
|
|
32
|
+
"forall", "exists", "in", "notin", "subset", "subseteq", "supset", "supseteq", "cup", "cap",
|
|
33
|
+
"to", "rightarrow", "Rightarrow", "Leftarrow", "Leftrightarrow", "leq", "geq", "neq", "approx",
|
|
34
|
+
"equiv", "times", "cdot", "pm", "mp", "partial", "nabla", "infty", "ldots", "dots", "mathrm",
|
|
35
|
+
"mathcal", "mathbb", "mathbf", "operatorname", "left", "right", "langle", "rangle", "vert", "mid",
|
|
36
|
+
}
|
|
37
|
+
_ALLOWED_MATH_CHARS = re.compile(r"^[A-Za-z0-9\s+\-*/=<>(),.;:{}\[\]_\\^|!?'`]+$")
|
|
38
|
+
_MATH_STRUCTURE = re.compile(
|
|
39
|
+
r"(?:[=<>]|[_^]|\\(?:frac|sqrt|sum|prod|int|lim|forall|exists|leq|geq|neq|approx|equiv|to|rightarrow|Rightarrow|Leftarrow|Leftrightarrow)\b)"
|
|
40
|
+
)
|
|
41
|
+
_MATH_IDENTIFIER_COMMAND = re.compile(
|
|
42
|
+
r"\\(?:operatorname|mathrm|mathcal|mathbb|mathbf)\s*\{[A-Za-z]+\}"
|
|
43
|
+
)
|
|
44
|
+
_LOWERCASE_PROSE_WORD = re.compile(r"\b[a-z]{3,}\b")
|
|
45
|
+
_EQUATION_FRAGMENT_BOUNDARY = re.compile(r"\n[ \t]*\n+")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def contains_observed_result_language(value: object) -> bool:
|
|
49
|
+
"""Detect proposal-incompatible statements that present work as completed."""
|
|
50
|
+
|
|
51
|
+
return bool(_OBSERVED_RESULT_LANGUAGE.search(str(value or "")))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def normalize_visible_text(value: object, *, label: str) -> str:
|
|
55
|
+
"""Normalize visible text while rejecting unsupported control characters.
|
|
56
|
+
|
|
57
|
+
Author content may use any Unicode script. Text safety is enforced by
|
|
58
|
+
escaping and raw-TeX restrictions rather than by a language gate.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
text = str(value or "")
|
|
62
|
+
if any(ord(character) < 32 and character not in "\n\r\t" for character in text):
|
|
63
|
+
raise LatexSafetyError(f"{label} contains an unsupported control character")
|
|
64
|
+
return text.replace("\r\n", "\n").replace("\r", "\n")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def escape_latex_text(
|
|
68
|
+
value: object,
|
|
69
|
+
*,
|
|
70
|
+
label: str = "text",
|
|
71
|
+
) -> str:
|
|
72
|
+
"""Escape plain visible text; this intentionally never preserves raw TeX."""
|
|
73
|
+
|
|
74
|
+
text = normalize_visible_text(value, label=label)
|
|
75
|
+
replacements = {
|
|
76
|
+
"&": r"\&",
|
|
77
|
+
"%": r"\%",
|
|
78
|
+
"$": r"\$",
|
|
79
|
+
"#": r"\#",
|
|
80
|
+
"_": r"\_",
|
|
81
|
+
"{": r"\{",
|
|
82
|
+
"}": r"\}",
|
|
83
|
+
"~": r"\textasciitilde{}",
|
|
84
|
+
"^": r"\textasciicircum{}",
|
|
85
|
+
"\\": r"\textbackslash{}",
|
|
86
|
+
}
|
|
87
|
+
return "".join(replacements.get(character, character) for character in text)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def safe_math_expression(value: object, *, label: str = "equation") -> str:
|
|
91
|
+
"""Allow a deliberately small math-only subset and reject executable TeX."""
|
|
92
|
+
|
|
93
|
+
text = normalize_visible_text(value, label=label).strip()
|
|
94
|
+
if not text:
|
|
95
|
+
raise LatexSafetyError(f"{label} is empty")
|
|
96
|
+
if "$" in text or "\\begin" in text.casefold() or "\\end" in text.casefold():
|
|
97
|
+
raise LatexSafetyError(f"{label} must be a math expression, not a TeX environment")
|
|
98
|
+
if _FORBIDDEN_TEX_COMMANDS.search(text):
|
|
99
|
+
raise LatexSafetyError(f"{label} contains a forbidden TeX command")
|
|
100
|
+
if not _ALLOWED_MATH_CHARS.fullmatch(text):
|
|
101
|
+
raise LatexSafetyError(f"{label} contains unsupported math characters")
|
|
102
|
+
commands = _MATH_COMMAND.findall(text.replace(r"\\", " "))
|
|
103
|
+
unsupported = sorted({command for command in commands if command not in _ALLOWED_MATH_COMMANDS})
|
|
104
|
+
if unsupported:
|
|
105
|
+
raise LatexSafetyError(f"{label} contains unsupported math commands: {', '.join(unsupported)}")
|
|
106
|
+
if not _MATH_STRUCTURE.search(text):
|
|
107
|
+
raise LatexSafetyError(f"{label} must contain a mathematical relation or structure")
|
|
108
|
+
residual_text = _MATH_COMMAND.sub("", _MATH_IDENTIFIER_COMMAND.sub("", text))
|
|
109
|
+
if _LOWERCASE_PROSE_WORD.search(residual_text):
|
|
110
|
+
raise LatexSafetyError(f"{label} contains explanatory prose and must be split from its mathematics")
|
|
111
|
+
return text
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def split_equation_content(value: object, *, label: str = "equation") -> list[tuple[str, str]]:
|
|
115
|
+
"""Partition an equation block into safe math expressions and visible prose.
|
|
116
|
+
|
|
117
|
+
A model may put sentence-level explanation around otherwise valid formulae.
|
|
118
|
+
Keeping these fragments distinct lets the renderer preserve the explanation
|
|
119
|
+
without sending it into a display-math environment.
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
text = normalize_visible_text(value, label=label).strip()
|
|
123
|
+
if not text:
|
|
124
|
+
raise LatexSafetyError(f"{label} is empty")
|
|
125
|
+
if "$" in text or "\\begin" in text.casefold() or "\\end" in text.casefold():
|
|
126
|
+
raise LatexSafetyError(f"{label} must be a math expression, not a TeX environment")
|
|
127
|
+
if _FORBIDDEN_TEX_COMMANDS.search(text):
|
|
128
|
+
raise LatexSafetyError(f"{label} contains a forbidden TeX command")
|
|
129
|
+
fragments = [
|
|
130
|
+
fragment.strip()
|
|
131
|
+
for fragment in _EQUATION_FRAGMENT_BOUNDARY.split(text)
|
|
132
|
+
if fragment.strip()
|
|
133
|
+
]
|
|
134
|
+
result: list[tuple[str, str]] = []
|
|
135
|
+
for index, fragment in enumerate(fragments, start=1):
|
|
136
|
+
fragment_label = label if len(fragments) == 1 else f"{label} fragment {index}"
|
|
137
|
+
try:
|
|
138
|
+
result.append(("equation", safe_math_expression(fragment, label=fragment_label)))
|
|
139
|
+
except LatexSafetyError:
|
|
140
|
+
result.append(("prose", fragment))
|
|
141
|
+
return result
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def validate_citation_key(value: object) -> str:
|
|
145
|
+
key = str(value or "").strip()
|
|
146
|
+
if not re.fullmatch(r"[A-Za-z0-9._:+-]+", key):
|
|
147
|
+
raise LatexSafetyError(f"citation key is unsafe for BibTeX: {key!r}")
|
|
148
|
+
return key
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
__all__ = [
|
|
152
|
+
"LatexSafetyError",
|
|
153
|
+
"contains_observed_result_language",
|
|
154
|
+
"escape_latex_text",
|
|
155
|
+
"normalize_visible_text",
|
|
156
|
+
"safe_math_expression",
|
|
157
|
+
"split_equation_content",
|
|
158
|
+
"validate_citation_key",
|
|
159
|
+
]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Required JSON-only LLM invocation for Research Plan Author stages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from threading import local
|
|
7
|
+
from typing import Any, Optional
|
|
8
|
+
|
|
9
|
+
from src.agents.experiment_design_agent.llm_json import (
|
|
10
|
+
JSON_OBJECT_RESPONSE_FORMAT,
|
|
11
|
+
RequiredJsonLLMError,
|
|
12
|
+
call_required_json,
|
|
13
|
+
)
|
|
14
|
+
from src.llm.provider_registry import resolve_model
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_author_json_llm_call(
|
|
18
|
+
*,
|
|
19
|
+
config: Any = None,
|
|
20
|
+
model: Optional[str] = None,
|
|
21
|
+
temperature: float | None = None,
|
|
22
|
+
) -> Callable[..., object]:
|
|
23
|
+
"""Build the configured Author callback while preserving strict JSON mode."""
|
|
24
|
+
|
|
25
|
+
holder = local()
|
|
26
|
+
|
|
27
|
+
def setting(value: Any, key: str, default: Any = "") -> Any:
|
|
28
|
+
if isinstance(value, Mapping):
|
|
29
|
+
return value.get(key, default)
|
|
30
|
+
return getattr(value, key, default)
|
|
31
|
+
|
|
32
|
+
def _call(prompt: str, **kwargs: Any) -> object:
|
|
33
|
+
if kwargs.get("response_format", JSON_OBJECT_RESPONSE_FORMAT) != JSON_OBJECT_RESPONSE_FORMAT:
|
|
34
|
+
raise RequiredJsonLLMError("research_plan_author: only JSON object response format is permitted")
|
|
35
|
+
runtime_config = config
|
|
36
|
+
if runtime_config is None:
|
|
37
|
+
from src.config import get_config
|
|
38
|
+
|
|
39
|
+
runtime_config = get_config()
|
|
40
|
+
author_config = setting(runtime_config, "research_plan_author", {})
|
|
41
|
+
authoring_config = setting(author_config, "authoring", {})
|
|
42
|
+
resolved_model = str(model or setting(author_config, "model") or "").strip()
|
|
43
|
+
provider_name = str(setting(author_config, "provider") or "").strip()
|
|
44
|
+
resolved_temperature = float(
|
|
45
|
+
setting(authoring_config, "temperature", 0.5) if temperature is None else temperature
|
|
46
|
+
)
|
|
47
|
+
if model:
|
|
48
|
+
provider_name = resolve_model(runtime_config, resolved_model).provider
|
|
49
|
+
agent = getattr(holder, "agent", None)
|
|
50
|
+
if agent is None:
|
|
51
|
+
from src.agents.idea_agent.agent.base import AgentBase
|
|
52
|
+
|
|
53
|
+
agent = AgentBase(config=runtime_config, provider_name=provider_name or None)
|
|
54
|
+
holder.agent = agent
|
|
55
|
+
if not resolved_model:
|
|
56
|
+
resolved_model = str(agent.provider.default_models.get("author") or "").strip()
|
|
57
|
+
if not resolved_model:
|
|
58
|
+
raise RequiredJsonLLMError("research_plan_author: no model is configured for the author LLM role")
|
|
59
|
+
return agent.chat(
|
|
60
|
+
prompt,
|
|
61
|
+
model=resolved_model,
|
|
62
|
+
response_format=JSON_OBJECT_RESPONSE_FORMAT,
|
|
63
|
+
temperature=resolved_temperature,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return _call
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = [
|
|
70
|
+
"JSON_OBJECT_RESPONSE_FORMAT",
|
|
71
|
+
"RequiredJsonLLMError",
|
|
72
|
+
"build_author_json_llm_call",
|
|
73
|
+
"call_required_json",
|
|
74
|
+
]
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
"""Deterministic Markdown views of structured Research Plan documents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
import re
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .theory_presentation import theory_block_presentation, theory_unit_registry, visible_theory_text
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_PRIVATE_PROVENANCE = re.compile(
|
|
13
|
+
r"\[?(?:survey:survey_markdown(?:#section-[A-Za-z0-9._:+-]+)?|anchor:[A-Za-z0-9_:+-]+(?:\.[A-Za-z0-9_:+-]+)*)\]?",
|
|
14
|
+
flags=re.IGNORECASE,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _mapping(value: object) -> dict[str, Any]:
|
|
19
|
+
return dict(value) if isinstance(value, Mapping) else {}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _text(value: object) -> str:
|
|
23
|
+
return str(value or "").strip()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _visible(value: object) -> str:
|
|
27
|
+
return _PRIVATE_PROVENANCE.sub("", _text(value)).strip()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _visible_for_document(document: Mapping[str, Any], value: object) -> str:
|
|
31
|
+
return _visible(visible_theory_text(document, value))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _label_component(value: object) -> str:
|
|
35
|
+
component = re.sub(r"[^A-Za-z0-9:-]+", "-", _text(value)).strip("-:")
|
|
36
|
+
return component or "unnamed"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _equation_label(section_id: object, block_id: object) -> str:
|
|
40
|
+
return f"eq:{_label_component(section_id)}-{_label_component(block_id)}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _document_equation_labels(document: Mapping[str, Any]) -> dict[str, str]:
|
|
44
|
+
labels: dict[str, str] = {}
|
|
45
|
+
for section in [*(document.get("sections") or []), *(document.get("appendices") or [])]:
|
|
46
|
+
if not isinstance(section, Mapping):
|
|
47
|
+
continue
|
|
48
|
+
section_id = _text(section.get("section_id"))
|
|
49
|
+
for block in section.get("blocks") or []:
|
|
50
|
+
if isinstance(block, Mapping) and _text(block.get("kind")) == "equation":
|
|
51
|
+
block_id = _text(block.get("block_id"))
|
|
52
|
+
if section_id and block_id:
|
|
53
|
+
labels[f"{section_id}:{block_id}"] = _equation_label(section_id, block_id)
|
|
54
|
+
return labels
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _equation_cross_references(block: Mapping[str, Any], *, equation_labels: Mapping[str, str]) -> str:
|
|
58
|
+
labels = [
|
|
59
|
+
equation_labels[reference_id]
|
|
60
|
+
for raw_reference_id in block.get("reference_block_ids") or []
|
|
61
|
+
if (reference_id := _text(raw_reference_id)) in equation_labels
|
|
62
|
+
]
|
|
63
|
+
return "" if not labels else " See Eq. (" + ", ".join(labels) + ")."
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _claim_map(document: Mapping[str, Any]) -> dict[str, Mapping[str, Any]]:
|
|
67
|
+
return {
|
|
68
|
+
_text(claim.get("claim_id")): claim
|
|
69
|
+
for claim in document.get("claim_provenance") or []
|
|
70
|
+
if isinstance(claim, Mapping) and _text(claim.get("claim_id"))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _citation_suffix(block: Mapping[str, Any], claims: Mapping[str, Mapping[str, Any]]) -> str:
|
|
75
|
+
keys: list[str] = []
|
|
76
|
+
for claim_id in block.get("claim_ids") or []:
|
|
77
|
+
claim = claims.get(_text(claim_id))
|
|
78
|
+
if not isinstance(claim, Mapping):
|
|
79
|
+
continue
|
|
80
|
+
for citation_key in claim.get("citation_keys") or []:
|
|
81
|
+
normalized = _text(citation_key)
|
|
82
|
+
if normalized and normalized not in keys:
|
|
83
|
+
keys.append(normalized)
|
|
84
|
+
return " " + " ".join(f"[@{key}]" for key in keys) if keys else ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _block_markdown(
|
|
88
|
+
block: Mapping[str, Any],
|
|
89
|
+
*,
|
|
90
|
+
claims: Mapping[str, Mapping[str, Any]],
|
|
91
|
+
evaluation_markers: bool,
|
|
92
|
+
section_id: str,
|
|
93
|
+
document: Mapping[str, Any],
|
|
94
|
+
theory_registry: Mapping[str, Mapping[str, Any]],
|
|
95
|
+
equation_labels: Mapping[str, str],
|
|
96
|
+
) -> str:
|
|
97
|
+
block_id = _text(block.get("block_id"))
|
|
98
|
+
marker = f"<!-- author:section={section_id} block={block_id} -->\n" if evaluation_markers else ""
|
|
99
|
+
heading = _visible_for_document(document, block.get("heading"))
|
|
100
|
+
kind = _text(block.get("kind"))
|
|
101
|
+
text = _visible_for_document(document, block.get("text"))
|
|
102
|
+
if not text:
|
|
103
|
+
return ""
|
|
104
|
+
prefix = f"### {heading}\n\n" if heading else ""
|
|
105
|
+
suffix = _citation_suffix(block, claims)
|
|
106
|
+
theory_prefix, _status = theory_block_presentation(
|
|
107
|
+
block,
|
|
108
|
+
claims=claims,
|
|
109
|
+
registry=theory_registry,
|
|
110
|
+
)
|
|
111
|
+
cross_references = _equation_cross_references(block, equation_labels=equation_labels)
|
|
112
|
+
if kind == "equation":
|
|
113
|
+
equation_label = equation_labels.get(_text(block.get("block_id")), "")
|
|
114
|
+
equation_prefix = f"**Equation [{equation_label}].**\n\n" if equation_label else ""
|
|
115
|
+
return marker + prefix + equation_prefix + "$$\n" + text + "\n$$" + suffix
|
|
116
|
+
if kind == "table" and theory_prefix:
|
|
117
|
+
return marker + prefix + f"**{theory_prefix}**\n\n" + text + cross_references + suffix
|
|
118
|
+
if theory_prefix:
|
|
119
|
+
return marker + prefix + f"**{theory_prefix}** " + text + cross_references + suffix
|
|
120
|
+
if kind == "definition":
|
|
121
|
+
return marker + prefix + "**Definition.** " + text + cross_references + suffix
|
|
122
|
+
if kind == "proposition":
|
|
123
|
+
return marker + prefix + "**Proposition (Candidate).** " + text + cross_references + suffix
|
|
124
|
+
if kind == "protocol":
|
|
125
|
+
return marker + prefix + "**Planned protocol.** " + text + cross_references + suffix
|
|
126
|
+
if kind == "outcome_branch":
|
|
127
|
+
return marker + prefix + "**Pre-registered Branch (Expected---Not Observed).** " + text + cross_references + suffix
|
|
128
|
+
if kind == "review_checklist":
|
|
129
|
+
return marker + prefix + "**Human-review checklist (Review-required).** " + text + cross_references + suffix
|
|
130
|
+
return marker + prefix + text + cross_references + suffix
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _section_markdown(
|
|
134
|
+
section: Mapping[str, Any],
|
|
135
|
+
*,
|
|
136
|
+
claims: Mapping[str, Mapping[str, Any]],
|
|
137
|
+
evaluation_markers: bool,
|
|
138
|
+
document: Mapping[str, Any],
|
|
139
|
+
theory_registry: Mapping[str, Mapping[str, Any]],
|
|
140
|
+
document_equation_labels: Mapping[str, str],
|
|
141
|
+
) -> str:
|
|
142
|
+
section_id = _text(section.get("section_id"))
|
|
143
|
+
title = _visible_for_document(document, section.get("title"))
|
|
144
|
+
equation_labels = {
|
|
145
|
+
**document_equation_labels,
|
|
146
|
+
**{
|
|
147
|
+
_text(block.get("block_id")): _equation_label(section_id, block.get("block_id"))
|
|
148
|
+
for block in section.get("blocks") or []
|
|
149
|
+
if isinstance(block, Mapping) and _text(block.get("kind")) == "equation"
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
lines = [f"## {title}"]
|
|
153
|
+
for block in section.get("blocks") or []:
|
|
154
|
+
if isinstance(block, Mapping):
|
|
155
|
+
rendered = _block_markdown(
|
|
156
|
+
block,
|
|
157
|
+
claims=claims,
|
|
158
|
+
evaluation_markers=evaluation_markers,
|
|
159
|
+
section_id=section_id,
|
|
160
|
+
document=document,
|
|
161
|
+
theory_registry=theory_registry,
|
|
162
|
+
equation_labels=equation_labels,
|
|
163
|
+
)
|
|
164
|
+
if rendered:
|
|
165
|
+
lines.append(rendered)
|
|
166
|
+
return "\n\n".join(lines)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _bibliography_markdown(document: Mapping[str, Any], claims: Mapping[str, Mapping[str, Any]]) -> str:
|
|
170
|
+
cited_keys = {
|
|
171
|
+
_text(key)
|
|
172
|
+
for claim in claims.values()
|
|
173
|
+
for key in claim.get("citation_keys") or []
|
|
174
|
+
if _text(key)
|
|
175
|
+
}
|
|
176
|
+
records = [
|
|
177
|
+
dict(record)
|
|
178
|
+
for record in document.get("citation_registry") or []
|
|
179
|
+
if isinstance(record, Mapping) and _text(record.get("citation_key")) in cited_keys
|
|
180
|
+
]
|
|
181
|
+
lines = ["## References"]
|
|
182
|
+
if not records:
|
|
183
|
+
lines.append("No registered sources are cited in the visible manuscript.")
|
|
184
|
+
return "\n\n".join(lines)
|
|
185
|
+
for record in sorted(records, key=lambda item: _text(item.get("citation_key"))):
|
|
186
|
+
metadata = _mapping(record.get("bibliographic_metadata"))
|
|
187
|
+
authors = [_text(author) for author in metadata.get("authors") or [] if _text(author)]
|
|
188
|
+
author_text = authors[0] + (" et al." if len(authors) > 1 else "") if authors else "Registered source"
|
|
189
|
+
title = _visible(metadata.get("title")) or "Untitled work"
|
|
190
|
+
venue = _visible(metadata.get("venue"))
|
|
191
|
+
year = _visible(metadata.get("year"))
|
|
192
|
+
tail = ", ".join(item for item in (venue, year) if item)
|
|
193
|
+
entry = f"- [@{_text(record.get('citation_key'))}] {author_text}. *{title}*."
|
|
194
|
+
lines.append(entry + (f" {tail}." if tail else ""))
|
|
195
|
+
return "\n".join(lines)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def render_research_plan_markdown(
|
|
199
|
+
document: Mapping[str, Any],
|
|
200
|
+
*,
|
|
201
|
+
evaluation_markers: bool = False,
|
|
202
|
+
compact_references: bool = False,
|
|
203
|
+
) -> str:
|
|
204
|
+
"""Render a complete, source-safe Markdown manuscript from one document."""
|
|
205
|
+
|
|
206
|
+
claims = _claim_map(document)
|
|
207
|
+
theory_registry = theory_unit_registry(document)
|
|
208
|
+
document_equation_labels = _document_equation_labels(document)
|
|
209
|
+
metadata = _mapping(document.get("document_metadata"))
|
|
210
|
+
title = _visible_for_document(document, metadata.get("title")) or "Research Plan"
|
|
211
|
+
lines = [f"# {title}"]
|
|
212
|
+
keywords = [
|
|
213
|
+
_visible_for_document(document, keyword)
|
|
214
|
+
for keyword in document.get("keywords") or []
|
|
215
|
+
if _visible_for_document(document, keyword)
|
|
216
|
+
]
|
|
217
|
+
if keywords:
|
|
218
|
+
lines.append("**Keywords:** " + ", ".join(keywords))
|
|
219
|
+
abstract = _mapping(document.get("abstract"))
|
|
220
|
+
abstract_text = _visible_for_document(document, abstract.get("text"))
|
|
221
|
+
if abstract_text:
|
|
222
|
+
lines.append("## Abstract\n\n" + abstract_text)
|
|
223
|
+
for section in document.get("sections") or []:
|
|
224
|
+
if not isinstance(section, Mapping) or _text(section.get("section_id")) == "references":
|
|
225
|
+
continue
|
|
226
|
+
lines.append(
|
|
227
|
+
_section_markdown(
|
|
228
|
+
section,
|
|
229
|
+
claims=claims,
|
|
230
|
+
evaluation_markers=evaluation_markers,
|
|
231
|
+
document=document,
|
|
232
|
+
theory_registry=theory_registry,
|
|
233
|
+
document_equation_labels=document_equation_labels,
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
appendices = [section for section in document.get("appendices") or [] if isinstance(section, Mapping)]
|
|
237
|
+
if appendices:
|
|
238
|
+
lines.append("# Appendices")
|
|
239
|
+
lines.extend(
|
|
240
|
+
_section_markdown(
|
|
241
|
+
section,
|
|
242
|
+
claims=claims,
|
|
243
|
+
evaluation_markers=evaluation_markers,
|
|
244
|
+
document=document,
|
|
245
|
+
theory_registry=theory_registry,
|
|
246
|
+
document_equation_labels=document_equation_labels,
|
|
247
|
+
)
|
|
248
|
+
for section in appendices
|
|
249
|
+
)
|
|
250
|
+
if compact_references:
|
|
251
|
+
cited_count = len(
|
|
252
|
+
{
|
|
253
|
+
_text(key)
|
|
254
|
+
for claim in claims.values()
|
|
255
|
+
for key in claim.get("citation_keys") or []
|
|
256
|
+
if _text(key)
|
|
257
|
+
}
|
|
258
|
+
)
|
|
259
|
+
lines.append(f"## References\n\nThe manuscript uses {cited_count} registered citation key(s).")
|
|
260
|
+
else:
|
|
261
|
+
lines.append(_bibliography_markdown(document, claims))
|
|
262
|
+
return "\n\n".join(part for part in lines if part.strip()).strip() + "\n"
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def render_quality_review_markdown(document: Mapping[str, Any]) -> str:
|
|
266
|
+
"""Render the full prose with private stable markers for quality review only."""
|
|
267
|
+
|
|
268
|
+
return render_research_plan_markdown(
|
|
269
|
+
document,
|
|
270
|
+
evaluation_markers=True,
|
|
271
|
+
compact_references=True,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
__all__ = ["render_quality_review_markdown", "render_research_plan_markdown"]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Structural and first-page render validation for generated Author PDFs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import subprocess
|
|
9
|
+
import tempfile
|
|
10
|
+
from time import perf_counter
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
PDF_VALIDATION_SCHEMA_VERSION = "research_plan_author_pdf_validation_v1"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PdfValidationError(RuntimeError):
|
|
18
|
+
"""Raised when a generated PDF cannot be parsed or rendered safely."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, message: str, *, report: dict[str, Any] | None = None) -> None:
|
|
21
|
+
super().__init__(message)
|
|
22
|
+
self.report = dict(report) if isinstance(report, dict) else None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class PdfValidationResult:
|
|
27
|
+
valid: bool
|
|
28
|
+
report: dict[str, Any]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_pdf(
|
|
32
|
+
pdf_path: str | Path,
|
|
33
|
+
*,
|
|
34
|
+
renderer: str | Path,
|
|
35
|
+
timeout_seconds: int = 60,
|
|
36
|
+
logger: Any | None = None,
|
|
37
|
+
) -> PdfValidationResult:
|
|
38
|
+
"""Require a parseable PDF with at least one renderable first page."""
|
|
39
|
+
|
|
40
|
+
source = Path(pdf_path).expanduser().resolve()
|
|
41
|
+
renderer_path = Path(renderer).expanduser().resolve()
|
|
42
|
+
if not source.is_file() or source.stat().st_size == 0:
|
|
43
|
+
raise PdfValidationError(f"generated PDF does not exist or is empty: {source}")
|
|
44
|
+
if not renderer_path.is_file():
|
|
45
|
+
raise PdfValidationError(f"PDF renderer is not an executable file: {renderer_path}")
|
|
46
|
+
parser_report: dict[str, Any] = {}
|
|
47
|
+
try:
|
|
48
|
+
from pypdf import PdfReader
|
|
49
|
+
|
|
50
|
+
reader = PdfReader(str(source))
|
|
51
|
+
page_count = len(reader.pages)
|
|
52
|
+
if page_count < 1:
|
|
53
|
+
raise PdfValidationError("generated PDF has no pages")
|
|
54
|
+
first_page = reader.pages[0]
|
|
55
|
+
box = first_page.mediabox
|
|
56
|
+
width = float(box.width)
|
|
57
|
+
height = float(box.height)
|
|
58
|
+
if width <= 0 or height <= 0:
|
|
59
|
+
raise PdfValidationError("generated PDF has invalid first-page dimensions")
|
|
60
|
+
extracted = ""
|
|
61
|
+
for page in reader.pages[: min(page_count, 3)]:
|
|
62
|
+
try:
|
|
63
|
+
extracted += page.extract_text() or ""
|
|
64
|
+
except Exception:
|
|
65
|
+
continue
|
|
66
|
+
parser_report = {
|
|
67
|
+
"backend": "pypdf",
|
|
68
|
+
"status": "parsed",
|
|
69
|
+
"page_count": page_count,
|
|
70
|
+
"first_page_width": width,
|
|
71
|
+
"first_page_height": height,
|
|
72
|
+
"extractable_text": bool(extracted.strip()),
|
|
73
|
+
"warning": "non_text_pdf" if not extracted.strip() else "",
|
|
74
|
+
}
|
|
75
|
+
except PdfValidationError:
|
|
76
|
+
raise
|
|
77
|
+
except Exception as error:
|
|
78
|
+
raise PdfValidationError(
|
|
79
|
+
f"pypdf could not parse generated PDF: {type(error).__name__}: {error}",
|
|
80
|
+
report={
|
|
81
|
+
"schema_version": PDF_VALIDATION_SCHEMA_VERSION,
|
|
82
|
+
"generated_at": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
|
|
83
|
+
"valid": False,
|
|
84
|
+
"pdf_path": str(source),
|
|
85
|
+
"file_size_bytes": source.stat().st_size,
|
|
86
|
+
"parser": {"backend": "pypdf", "status": "parse_failed", "error": type(error).__name__},
|
|
87
|
+
"renderer": {"backend": "pdftoppm", "status": "not_started"},
|
|
88
|
+
},
|
|
89
|
+
) from error
|
|
90
|
+
with tempfile.TemporaryDirectory(prefix="research-plan-author-pdf-") as temporary_root:
|
|
91
|
+
prefix = Path(temporary_root) / "page"
|
|
92
|
+
command = [str(renderer_path), "-f", "1", "-l", "1", "-png", str(source), str(prefix)]
|
|
93
|
+
started_at = perf_counter()
|
|
94
|
+
if logger is not None:
|
|
95
|
+
logger.emit("pdf_validation", "render_started", status="RUNNING", renderer=str(renderer_path))
|
|
96
|
+
try:
|
|
97
|
+
completed = subprocess.run(
|
|
98
|
+
command,
|
|
99
|
+
shell=False,
|
|
100
|
+
capture_output=True,
|
|
101
|
+
text=True,
|
|
102
|
+
encoding="utf-8",
|
|
103
|
+
errors="replace",
|
|
104
|
+
timeout=max(1, int(timeout_seconds)),
|
|
105
|
+
check=False,
|
|
106
|
+
)
|
|
107
|
+
except subprocess.TimeoutExpired as error:
|
|
108
|
+
raise PdfValidationError(f"PDF first-page renderer timed out after {timeout_seconds} seconds") from error
|
|
109
|
+
except OSError as error:
|
|
110
|
+
raise PdfValidationError(f"PDF first-page renderer could not start: {error}") from error
|
|
111
|
+
images = sorted(Path(temporary_root).glob("page-*.png"))
|
|
112
|
+
render_success = completed.returncode == 0 and bool(images) and images[0].stat().st_size > 0
|
|
113
|
+
render_report = {
|
|
114
|
+
"backend": "pdftoppm",
|
|
115
|
+
"status": "rendered" if render_success else "render_failed",
|
|
116
|
+
"command": [str(value) for value in command],
|
|
117
|
+
"return_code": completed.returncode,
|
|
118
|
+
"elapsed_ms": round((perf_counter() - started_at) * 1000, 3),
|
|
119
|
+
"rendered_first_page_bytes": images[0].stat().st_size if images else 0,
|
|
120
|
+
"stderr_tail": (completed.stderr or "")[-2000:],
|
|
121
|
+
}
|
|
122
|
+
if logger is not None:
|
|
123
|
+
logger.emit(
|
|
124
|
+
"pdf_validation",
|
|
125
|
+
"render_completed" if render_success else "render_failed",
|
|
126
|
+
level="INFO" if render_success else "ERROR",
|
|
127
|
+
status="COMPLETED" if render_success else "FAILED",
|
|
128
|
+
return_code=completed.returncode,
|
|
129
|
+
elapsed_ms=render_report["elapsed_ms"],
|
|
130
|
+
)
|
|
131
|
+
report = {
|
|
132
|
+
"schema_version": PDF_VALIDATION_SCHEMA_VERSION,
|
|
133
|
+
"generated_at": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
|
|
134
|
+
"valid": render_success,
|
|
135
|
+
"pdf_path": str(source),
|
|
136
|
+
"file_size_bytes": source.stat().st_size,
|
|
137
|
+
"parser": parser_report,
|
|
138
|
+
"renderer": render_report,
|
|
139
|
+
}
|
|
140
|
+
if not render_success:
|
|
141
|
+
raise PdfValidationError(
|
|
142
|
+
"PDF first-page render validation failed: "
|
|
143
|
+
+ (completed.stderr or completed.stdout or "renderer produced no PNG")[-1000:],
|
|
144
|
+
report=report,
|
|
145
|
+
)
|
|
146
|
+
return PdfValidationResult(valid=True, report=report)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
__all__ = ["PDF_VALIDATION_SCHEMA_VERSION", "PdfValidationError", "PdfValidationResult", "validate_pdf"]
|