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,987 @@
|
|
|
1
|
+
"""Whole-document quality scoring and bounded revision for Research Plan Author."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from src.agents.survey_agent.modules.pe import EVAL_CRITERIA
|
|
13
|
+
|
|
14
|
+
from .latex_safety import LatexSafetyError, split_equation_content
|
|
15
|
+
from .llm_json import call_required_json
|
|
16
|
+
from .markdown_renderer import render_quality_review_markdown, render_research_plan_markdown
|
|
17
|
+
from .semantic_validator import validate_composed_research_plan
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
AUTHOR_DOCUMENT_QUALITY_SCHEMA_VERSION = "research_plan_author_document_quality_v1"
|
|
21
|
+
AUTHOR_DOCUMENT_REVISION_SCHEMA_VERSION = "research_plan_author_document_revision_v1"
|
|
22
|
+
_RUBRIC_DIMENSIONS = (
|
|
23
|
+
"Synthesis Quality",
|
|
24
|
+
"Organization",
|
|
25
|
+
"Readability",
|
|
26
|
+
"Academic Rigor",
|
|
27
|
+
"Clarity",
|
|
28
|
+
"Coherence",
|
|
29
|
+
"Comprehensiveness",
|
|
30
|
+
"Critical Analysis",
|
|
31
|
+
"Novelty and Insights",
|
|
32
|
+
"Future Directions",
|
|
33
|
+
)
|
|
34
|
+
_SPECIAL_RUBRICS = {
|
|
35
|
+
"mathematics_theory": (
|
|
36
|
+
(
|
|
37
|
+
"Theory Auditability",
|
|
38
|
+
"Assess whether the manuscript turns its central proposal into an auditable chain of labeled definitions, assumptions, lemmas, equations, proof obligations, and explicit dependency links. Reward a reader being able to identify what must be checked next; do not require a completed proof.",
|
|
39
|
+
0.35,
|
|
40
|
+
),
|
|
41
|
+
(
|
|
42
|
+
"Boundary and Status Discipline",
|
|
43
|
+
"Assess whether candidate, unverified, expected-not-observed, no-information, and review-required statuses are precise and consistently attached to the relevant mathematical claims. Reward disciplined scope management that still advances the argument rather than repeatedly restating uncertainty.",
|
|
44
|
+
0.25,
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"Falsifiability and Decision Completeness",
|
|
48
|
+
"Assess whether falsifiers, counterexample criteria, proof-obligation outcomes, no-information branches, and next actions close the proposal's decision logic. Reward concrete pre-registered responses to each meaningful branch.",
|
|
49
|
+
0.25,
|
|
50
|
+
),
|
|
51
|
+
(
|
|
52
|
+
"Energy-Condition Defense",
|
|
53
|
+
"Assess whether the manuscript carefully distinguishes NEC, ANEC/AANEC, null convergence or Ricci contraction, SEC, and independently assumed focusing conditions, including the limits of their implications. Reward accurate boundary defense rather than decorative terminology.",
|
|
54
|
+
0.15,
|
|
55
|
+
),
|
|
56
|
+
),
|
|
57
|
+
"computational_digital": (
|
|
58
|
+
(
|
|
59
|
+
"Computational Methodological Strength",
|
|
60
|
+
"Assess task definition, data split, baselines, ablations, robustness, metrics, and reproducibility as one coherent computational plan.",
|
|
61
|
+
1.0,
|
|
62
|
+
),
|
|
63
|
+
),
|
|
64
|
+
"materials_chemical": (
|
|
65
|
+
(
|
|
66
|
+
"Mechanistic and Characterization Strength",
|
|
67
|
+
"Assess the connection among mechanism, material variables, characterization, controls, design of experiments, and reproducibility.",
|
|
68
|
+
1.0,
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
"engineering_energy": (
|
|
72
|
+
(
|
|
73
|
+
"System-Boundary and Validation Strength",
|
|
74
|
+
"Assess system boundaries, constraints, metrics, failure modes, layered validation, and engineering implementability.",
|
|
75
|
+
1.0,
|
|
76
|
+
),
|
|
77
|
+
),
|
|
78
|
+
"earth_environment_agro": (
|
|
79
|
+
(
|
|
80
|
+
"Spatiotemporal and Causal Design Strength",
|
|
81
|
+
"Assess spatial and temporal units, sampling frame, covariates, confounding, causal interpretation, and extrapolation limits.",
|
|
82
|
+
1.0,
|
|
83
|
+
),
|
|
84
|
+
),
|
|
85
|
+
"life_veterinary": (
|
|
86
|
+
(
|
|
87
|
+
"Experimental Controls and Biological Validity",
|
|
88
|
+
"Assess model choice, controls, repeats, assays, bias control, and biological interpretation boundaries.",
|
|
89
|
+
1.0,
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
"clinical_health": (
|
|
93
|
+
(
|
|
94
|
+
"Clinical Validity, Safety, and Governance Strength",
|
|
95
|
+
"Assess PICO, endpoints, bias control, clinical relevance, safety, ethics, governance, and implementability.",
|
|
96
|
+
1.0,
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
}
|
|
100
|
+
_VISIBLE_CITATION_TOKEN = re.compile(r"\[@[^\]]+\]")
|
|
101
|
+
_EVALUATION_MARKER = re.compile(r"<!--\s*author:section=[^>]+-->", flags=re.IGNORECASE)
|
|
102
|
+
_REVIEW_PRESENTATION_PREFIX = re.compile(
|
|
103
|
+
r"\*\*(?:"
|
|
104
|
+
r"Definition\.|"
|
|
105
|
+
r"Proposition \((?:proposed|Candidate)\)\.|"
|
|
106
|
+
r"Planned protocol\.|"
|
|
107
|
+
r"Conditional outcome branch\.|"
|
|
108
|
+
r"Human-review checklist\.|"
|
|
109
|
+
r"Lemma(?: [^*]+)?|"
|
|
110
|
+
r"Proof Obligation(?: [^*]+)?|"
|
|
111
|
+
r"Equation \[[^*]+\]\.|"
|
|
112
|
+
r"Pre-registered Branch \([^*]+\)\.|"
|
|
113
|
+
r"Decision Status: No-information\."
|
|
114
|
+
r")\*\*\s*",
|
|
115
|
+
flags=re.IGNORECASE,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _mapping(value: object) -> dict[str, Any]:
|
|
120
|
+
return dict(value) if isinstance(value, Mapping) else {}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _text(value: object) -> str:
|
|
124
|
+
return str(value or "").strip()
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _as_bool(value: object, default: bool) -> bool:
|
|
128
|
+
if isinstance(value, bool):
|
|
129
|
+
return value
|
|
130
|
+
if value is None:
|
|
131
|
+
return default
|
|
132
|
+
return str(value).strip().casefold() in {"1", "true", "yes", "on"}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _setting_or_default(settings: Mapping[str, Any], key: str, default: Any) -> Any:
|
|
136
|
+
"""Preserve explicit falsy settings such as zero iterations or zero weight."""
|
|
137
|
+
|
|
138
|
+
value = settings.get(key)
|
|
139
|
+
return default if value is None else value
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _scorecard(
|
|
143
|
+
scores: Mapping[str, int],
|
|
144
|
+
special_scores: Mapping[str, int],
|
|
145
|
+
special_weight: float,
|
|
146
|
+
*,
|
|
147
|
+
special_dimension_weights: Mapping[str, float] | None = None,
|
|
148
|
+
) -> dict[str, Any]:
|
|
149
|
+
core = (scores["Synthesis Quality"] + scores["Organization"]) / 2
|
|
150
|
+
writing = (scores["Readability"] + scores["Academic Rigor"] + scores["Clarity"] + scores["Coherence"]) / 4
|
|
151
|
+
depth = (scores["Comprehensiveness"] + scores["Critical Analysis"] + scores["Novelty and Insights"] + scores["Future Directions"]) / 4
|
|
152
|
+
total = 0.33 * core + 0.33 * writing + 0.34 * depth
|
|
153
|
+
normalized_special_weights = {
|
|
154
|
+
dimension: max(0.0, float((special_dimension_weights or {}).get(dimension, 1.0)))
|
|
155
|
+
for dimension in special_scores
|
|
156
|
+
}
|
|
157
|
+
special_weight_total = sum(normalized_special_weights.values())
|
|
158
|
+
special = (
|
|
159
|
+
sum(special_scores[dimension] * normalized_special_weights[dimension] for dimension in special_scores)
|
|
160
|
+
/ special_weight_total
|
|
161
|
+
if special_scores and special_weight_total > 0
|
|
162
|
+
else total
|
|
163
|
+
)
|
|
164
|
+
weight = min(1.0, max(0.0, float(special_weight))) if special_scores else 0.0
|
|
165
|
+
return {
|
|
166
|
+
"dimension_scores": dict(scores),
|
|
167
|
+
"special_dimension_scores": dict(special_scores),
|
|
168
|
+
"special_dimension_weights": normalized_special_weights,
|
|
169
|
+
"core_quality": round(core, 4),
|
|
170
|
+
"writing_quality": round(writing, 4),
|
|
171
|
+
"content_depth": round(depth, 4),
|
|
172
|
+
"total_score": round(total, 4),
|
|
173
|
+
"final_100": round(total * 10, 2),
|
|
174
|
+
"special_score": round(special, 4),
|
|
175
|
+
"selection_score": round((1 - weight) * total + weight * special, 4),
|
|
176
|
+
"special_score_weight": weight,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _block_lookup(document: Mapping[str, Any]) -> dict[tuple[str, str], str]:
|
|
181
|
+
result: dict[tuple[str, str], str] = {}
|
|
182
|
+
for section in [*(document.get("sections") or []), *(document.get("appendices") or [])]:
|
|
183
|
+
if not isinstance(section, Mapping):
|
|
184
|
+
continue
|
|
185
|
+
section_id = _text(section.get("section_id"))
|
|
186
|
+
for block in section.get("blocks") or []:
|
|
187
|
+
if isinstance(block, Mapping):
|
|
188
|
+
result[(section_id, _text(block.get("block_id")))] = _text(block.get("text"))
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _claim_lookup(document: Mapping[str, Any]) -> dict[str, dict[str, Any]]:
|
|
193
|
+
return {
|
|
194
|
+
_text(claim.get("claim_id")): dict(claim)
|
|
195
|
+
for claim in document.get("claim_provenance") or []
|
|
196
|
+
if isinstance(claim, Mapping) and _text(claim.get("claim_id"))
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _normalise_evidence_text(value: object) -> str:
|
|
201
|
+
"""Compare Judge excerpts with the raw block after removing review-only decoration."""
|
|
202
|
+
|
|
203
|
+
text = _EVALUATION_MARKER.sub("", _text(value))
|
|
204
|
+
text = _VISIBLE_CITATION_TOKEN.sub("", text)
|
|
205
|
+
text = _REVIEW_PRESENTATION_PREFIX.sub("", text)
|
|
206
|
+
return " ".join(text.replace("$$", " ").split())
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _valid_evidence(
|
|
210
|
+
items: object,
|
|
211
|
+
*,
|
|
212
|
+
blocks: Mapping[tuple[str, str], str],
|
|
213
|
+
) -> tuple[list[dict[str, Any]] | None, list[str]]:
|
|
214
|
+
if not isinstance(items, list) or not items:
|
|
215
|
+
return None, ["evidence must be a non-empty list"]
|
|
216
|
+
normalized: list[dict[str, Any]] = []
|
|
217
|
+
warnings: list[str] = []
|
|
218
|
+
for index, item in enumerate(items, start=1):
|
|
219
|
+
if not isinstance(item, Mapping):
|
|
220
|
+
warnings.append(f"evidence[{index}] must be an object and was discarded")
|
|
221
|
+
continue
|
|
222
|
+
section_id = _text(item.get("section_id"))
|
|
223
|
+
block_id = _text(item.get("block_id"))
|
|
224
|
+
excerpt = _text(item.get("excerpt"))
|
|
225
|
+
assessment = _text(item.get("assessment"))
|
|
226
|
+
block_text = blocks.get((section_id, block_id), "")
|
|
227
|
+
normalized_excerpt = _normalise_evidence_text(excerpt)
|
|
228
|
+
normalized_block = _normalise_evidence_text(block_text)
|
|
229
|
+
missing = [
|
|
230
|
+
field
|
|
231
|
+
for field, value in {
|
|
232
|
+
"section_id": section_id,
|
|
233
|
+
"block_id": block_id,
|
|
234
|
+
"excerpt": excerpt,
|
|
235
|
+
"assessment": assessment,
|
|
236
|
+
}.items()
|
|
237
|
+
if not value
|
|
238
|
+
]
|
|
239
|
+
if missing:
|
|
240
|
+
warnings.append(f"evidence[{index}] is missing {', '.join(missing)} and was discarded")
|
|
241
|
+
continue
|
|
242
|
+
if (section_id, block_id) not in blocks:
|
|
243
|
+
warnings.append(f"evidence[{index}] references an unknown section_id/block_id and was discarded")
|
|
244
|
+
continue
|
|
245
|
+
if not normalized_excerpt:
|
|
246
|
+
warnings.append(f"evidence[{index}].excerpt has no retained manuscript text and was discarded")
|
|
247
|
+
continue
|
|
248
|
+
if normalized_excerpt not in normalized_block:
|
|
249
|
+
warnings.append(f"evidence[{index}].excerpt is not found in its referenced block and was discarded")
|
|
250
|
+
continue
|
|
251
|
+
if len(normalized) < 3:
|
|
252
|
+
normalized.append({"section_id": section_id, "block_id": block_id, "excerpt": excerpt, "assessment": assessment})
|
|
253
|
+
if not normalized:
|
|
254
|
+
return None, [*warnings, "no evidence entry could be reliably located in the manuscript"]
|
|
255
|
+
return normalized, warnings
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _valid_references(
|
|
259
|
+
items: object,
|
|
260
|
+
*,
|
|
261
|
+
blocks: Mapping[tuple[str, str], str],
|
|
262
|
+
field_name: str,
|
|
263
|
+
) -> tuple[list[dict[str, str]], list[str]]:
|
|
264
|
+
if not isinstance(items, list):
|
|
265
|
+
return [], [f"{field_name} must be a list; location is pending review"]
|
|
266
|
+
normalized: list[dict[str, str]] = []
|
|
267
|
+
warnings: list[str] = []
|
|
268
|
+
for index, item in enumerate(items, start=1):
|
|
269
|
+
if not isinstance(item, Mapping):
|
|
270
|
+
warnings.append(f"{field_name}[{index}] must be an object and was discarded")
|
|
271
|
+
continue
|
|
272
|
+
section_id = _text(item.get("section_id"))
|
|
273
|
+
block_id = _text(item.get("block_id"))
|
|
274
|
+
if (section_id, block_id) not in blocks:
|
|
275
|
+
warnings.append(f"{field_name}[{index}] references an unknown section_id/block_id and was discarded")
|
|
276
|
+
continue
|
|
277
|
+
if len(normalized) < 3:
|
|
278
|
+
normalized.append({"section_id": section_id, "block_id": block_id})
|
|
279
|
+
return normalized, warnings
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _response_summary(response: object) -> dict[str, Any]:
|
|
283
|
+
"""Keep structural Judge diagnostics without persisting model prose or prompts."""
|
|
284
|
+
|
|
285
|
+
if not isinstance(response, Mapping):
|
|
286
|
+
return {"response_type": type(response).__name__}
|
|
287
|
+
strength = _mapping(response.get("maximum_strength"))
|
|
288
|
+
return {
|
|
289
|
+
"response_type": "object",
|
|
290
|
+
"top_level_keys": sorted(_text(key) for key in response.keys())[:20],
|
|
291
|
+
"dimension": _text(response.get("dimension")),
|
|
292
|
+
"score": response.get("score") if isinstance(response.get("score"), (str, int, float, bool)) else type(response.get("score")).__name__,
|
|
293
|
+
"rationale_present": bool(_text(response.get("rationale"))),
|
|
294
|
+
"evidence_count": len(response.get("evidence")) if isinstance(response.get("evidence"), list) else None,
|
|
295
|
+
"maximum_strength_present": strength.get("present") if isinstance(strength.get("present"), bool) else None,
|
|
296
|
+
"major_weakness_count": len(response.get("major_weaknesses")) if isinstance(response.get("major_weaknesses"), list) else None,
|
|
297
|
+
"polish_direction_count": len(response.get("polish_directions")) if isinstance(response.get("polish_directions"), list) else None,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _invalid_report(category: str, *errors: str) -> tuple[None, dict[str, Any]]:
|
|
302
|
+
return None, {
|
|
303
|
+
"failure_category": category,
|
|
304
|
+
"validation_errors": [error for error in errors if error],
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _parse_dimension_report(
|
|
309
|
+
response: object,
|
|
310
|
+
*,
|
|
311
|
+
dimension: str,
|
|
312
|
+
document: Mapping[str, Any],
|
|
313
|
+
) -> tuple[dict[str, Any] | None, dict[str, Any]]:
|
|
314
|
+
if not isinstance(response, Mapping):
|
|
315
|
+
return _invalid_report("response_shape", "Judge response must be a JSON object")
|
|
316
|
+
returned_dimension = _text(response.get("dimension"))
|
|
317
|
+
if returned_dimension != dimension:
|
|
318
|
+
return _invalid_report(
|
|
319
|
+
"dimension_mismatch",
|
|
320
|
+
f"response.dimension must equal the requested dimension '{dimension}'",
|
|
321
|
+
)
|
|
322
|
+
score = response.get("score")
|
|
323
|
+
if isinstance(score, bool) or not isinstance(score, int) or not 1 <= score <= 10:
|
|
324
|
+
return _invalid_report("score_contract", "response.score must be an integer from 1 through 10")
|
|
325
|
+
rationale = _text(response.get("rationale"))
|
|
326
|
+
if not rationale:
|
|
327
|
+
return _invalid_report("rationale_contract", "response.rationale must be non-empty")
|
|
328
|
+
blocks = _block_lookup(document)
|
|
329
|
+
evidence, evidence_errors = _valid_evidence(response.get("evidence"), blocks=blocks)
|
|
330
|
+
if evidence is None:
|
|
331
|
+
return _invalid_report("grounding_contract", *evidence_errors)
|
|
332
|
+
grounding_warnings = list(evidence_errors)
|
|
333
|
+
strength = _mapping(response.get("maximum_strength"))
|
|
334
|
+
if bool(strength.get("present")):
|
|
335
|
+
strength_refs, strength_errors = _valid_references(
|
|
336
|
+
strength.get("evidence_refs"),
|
|
337
|
+
blocks=blocks,
|
|
338
|
+
field_name="maximum_strength.evidence_refs",
|
|
339
|
+
)
|
|
340
|
+
if not _text(strength.get("description")):
|
|
341
|
+
return _invalid_report("strength_contract", "maximum_strength.description must be non-empty when present=true")
|
|
342
|
+
grounding_warnings.extend(strength_errors)
|
|
343
|
+
strength = {
|
|
344
|
+
"present": True,
|
|
345
|
+
"description": _text(strength.get("description")),
|
|
346
|
+
"evidence_refs": strength_refs,
|
|
347
|
+
"location_status": "located" if strength_refs else "pending_review",
|
|
348
|
+
}
|
|
349
|
+
else:
|
|
350
|
+
strength = {"present": False, "description": "", "evidence_refs": [], "location_status": "not_applicable"}
|
|
351
|
+
weaknesses: list[dict[str, Any]] = []
|
|
352
|
+
raw_weaknesses = response.get("major_weaknesses")
|
|
353
|
+
if not isinstance(raw_weaknesses, list):
|
|
354
|
+
return _invalid_report("weakness_contract", "major_weaknesses must be a list")
|
|
355
|
+
for index, weakness in enumerate(raw_weaknesses[:3], start=1):
|
|
356
|
+
if not isinstance(weakness, Mapping):
|
|
357
|
+
return _invalid_report("weakness_contract", f"major_weaknesses[{index}] must be an object")
|
|
358
|
+
refs, reference_errors = _valid_references(
|
|
359
|
+
weakness.get("evidence_refs"),
|
|
360
|
+
blocks=blocks,
|
|
361
|
+
field_name=f"major_weaknesses[{index}].evidence_refs",
|
|
362
|
+
)
|
|
363
|
+
missing = [
|
|
364
|
+
field
|
|
365
|
+
for field, value in {
|
|
366
|
+
"description": _text(weakness.get("description")),
|
|
367
|
+
"impact": _text(weakness.get("impact")),
|
|
368
|
+
"repair_direction": _text(weakness.get("repair_direction")),
|
|
369
|
+
}.items()
|
|
370
|
+
if not value
|
|
371
|
+
]
|
|
372
|
+
if missing:
|
|
373
|
+
return _invalid_report("weakness_contract", f"major_weaknesses[{index}] is missing {', '.join(missing)}")
|
|
374
|
+
grounding_warnings.extend(reference_errors)
|
|
375
|
+
weaknesses.append({
|
|
376
|
+
"severity": _text(weakness.get("severity")) or "major",
|
|
377
|
+
"description": _text(weakness.get("description")),
|
|
378
|
+
"impact": _text(weakness.get("impact")),
|
|
379
|
+
"repair_direction": _text(weakness.get("repair_direction")),
|
|
380
|
+
"evidence_refs": refs,
|
|
381
|
+
"location_status": "located" if refs else "pending_review",
|
|
382
|
+
})
|
|
383
|
+
directions: list[dict[str, Any]] = []
|
|
384
|
+
raw_directions = response.get("polish_directions")
|
|
385
|
+
if not isinstance(raw_directions, list) or not raw_directions:
|
|
386
|
+
return _invalid_report("polish_contract", "polish_directions must be a non-empty list")
|
|
387
|
+
for index, direction in enumerate(raw_directions[:3], start=1):
|
|
388
|
+
if not isinstance(direction, Mapping) or not _text(direction.get("direction")):
|
|
389
|
+
return _invalid_report("polish_contract", f"polish_directions[{index}].direction must be non-empty")
|
|
390
|
+
try:
|
|
391
|
+
priority = int(direction.get("priority") or len(directions) + 1)
|
|
392
|
+
except (TypeError, ValueError):
|
|
393
|
+
return _invalid_report("polish_contract", f"polish_directions[{index}].priority must be an integer")
|
|
394
|
+
directions.append({
|
|
395
|
+
"priority": priority,
|
|
396
|
+
"direction": _text(direction.get("direction")),
|
|
397
|
+
"expected_gain": _text(direction.get("expected_gain")),
|
|
398
|
+
})
|
|
399
|
+
return {
|
|
400
|
+
"dimension": dimension,
|
|
401
|
+
"score": score,
|
|
402
|
+
"rationale": rationale,
|
|
403
|
+
"evidence": evidence,
|
|
404
|
+
"maximum_strength": strength,
|
|
405
|
+
"major_weaknesses": weaknesses,
|
|
406
|
+
"polish_directions": directions,
|
|
407
|
+
"grounding_warnings": grounding_warnings,
|
|
408
|
+
}, {"grounding_warnings": grounding_warnings}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _judge_prompt(*, dimension: str, description: str, manuscript: str) -> str:
|
|
412
|
+
payload = {
|
|
413
|
+
"operation": "research_plan_document_quality_dimension",
|
|
414
|
+
"dimension": dimension,
|
|
415
|
+
"criterion_description": description,
|
|
416
|
+
"manuscript_markdown": manuscript,
|
|
417
|
+
"output_contract": {
|
|
418
|
+
"dimension": dimension,
|
|
419
|
+
"score": "integer 1..10",
|
|
420
|
+
"rationale": "why this score is warranted",
|
|
421
|
+
"evidence": [{"section_id": "existing marker", "block_id": "existing marker", "excerpt": "short phrase copied from the raw block text only", "assessment": "why it matters"}],
|
|
422
|
+
"maximum_strength": {"present": "boolean", "description": "if present", "evidence_refs": [{"section_id": "", "block_id": ""}]},
|
|
423
|
+
"major_weaknesses": [{"severity": "major or moderate", "description": "", "impact": "", "repair_direction": "", "evidence_refs": [{"section_id": "", "block_id": ""}]}],
|
|
424
|
+
"polish_directions": [{"priority": 1, "direction": "specific revision", "expected_gain": "affected quality"}],
|
|
425
|
+
},
|
|
426
|
+
}
|
|
427
|
+
return """You are a senior academic reviewer scoring one dimension of a complete research-plan manuscript. Read the full manuscript and score the requested dimension from 1 to 10 using the supplied criterion. Give a concrete, constructive assessment grounded in the manuscript itself. Identify its strongest contribution when one is present, identify major weaknesses when present, and give practical directions for a stronger next draft. The manuscript is a proposal: reward clear conditional reasoning, honest scope, and useful proof or validation obligations rather than demanding completed experiments or completed proofs. Use the invisible author markers only to locate evidence; never treat them as visible prose. For every evidence excerpt, copy a short phrase from the raw block text, excluding Markdown wrappers, generated citation suffixes, and marker comments. Return exactly one JSON object.\n\nINPUT_JSON:\n""" + json.dumps(payload, ensure_ascii=False)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _revision_context(
|
|
431
|
+
preparation: Mapping[str, Any],
|
|
432
|
+
source_registry: Mapping[str, Any],
|
|
433
|
+
) -> dict[str, Any]:
|
|
434
|
+
"""Expose the complete frozen upstream knowledge base to the reviser.
|
|
435
|
+
|
|
436
|
+
This is an authoring context, not a section-local source permission list.
|
|
437
|
+
The original Author preparation already owns its provenance; retaining the
|
|
438
|
+
full context lets the reviser make substantive cross-agent connections.
|
|
439
|
+
"""
|
|
440
|
+
|
|
441
|
+
knowledge_base = _mapping(source_registry.get("authoring_knowledge_base"))
|
|
442
|
+
if knowledge_base:
|
|
443
|
+
return deepcopy(knowledge_base)
|
|
444
|
+
context = _mapping(_mapping(preparation.get("source_bundle")).get("author_context"))
|
|
445
|
+
return {
|
|
446
|
+
"source_catalog": {
|
|
447
|
+
"citation_registry": deepcopy(list(source_registry.get("citation_registry") or [])),
|
|
448
|
+
"evidence_cards_by_id": deepcopy(_mapping(source_registry.get("evidence_cards_by_id"))),
|
|
449
|
+
},
|
|
450
|
+
"upstream_artifacts": {
|
|
451
|
+
"selected_direction": context.get("selected_direction"),
|
|
452
|
+
"research_design": context.get("research_design"),
|
|
453
|
+
"hypothesis_mapping": context.get("hypothesis_mapping"),
|
|
454
|
+
"formal_reasoning": context.get("formal_reasoning"),
|
|
455
|
+
"counterexample_analysis": context.get("counterexample_analysis"),
|
|
456
|
+
"outcome_branches": context.get("outcome_branches"),
|
|
457
|
+
"reasoning_context": context.get("reasoning_context"),
|
|
458
|
+
"variables_and_operationalization": context.get("variables_and_operationalization"),
|
|
459
|
+
"idea_evolution": _mapping(_mapping(preparation.get("source_bundle")).get("idea_evolution")),
|
|
460
|
+
"survey_binding": _mapping(_mapping(preparation.get("source_bundle")).get("survey_binding")),
|
|
461
|
+
},
|
|
462
|
+
"unknown_items": deepcopy(list(source_registry.get("unknown_items") or [])),
|
|
463
|
+
"review_items": deepcopy(list(source_registry.get("review_items") or [])),
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def _editable_block_context(document: Mapping[str, Any]) -> list[dict[str, Any]]:
|
|
468
|
+
claims = _claim_lookup(document)
|
|
469
|
+
editable: list[dict[str, Any]] = []
|
|
470
|
+
for section in [*(document.get("sections") or []), *(document.get("appendices") or [])]:
|
|
471
|
+
if not isinstance(section, Mapping):
|
|
472
|
+
continue
|
|
473
|
+
section_id = _text(section.get("section_id"))
|
|
474
|
+
for block in section.get("blocks") or []:
|
|
475
|
+
if not isinstance(block, Mapping):
|
|
476
|
+
continue
|
|
477
|
+
claim_ids = [_text(claim_id) for claim_id in block.get("claim_ids") or [] if _text(claim_id)]
|
|
478
|
+
editable.append(
|
|
479
|
+
{
|
|
480
|
+
"section_id": section_id,
|
|
481
|
+
"block_id": _text(block.get("block_id")),
|
|
482
|
+
"claim_ids": claim_ids,
|
|
483
|
+
"supported_claims": [
|
|
484
|
+
{
|
|
485
|
+
"claim_id": claim_id,
|
|
486
|
+
"claim_kind": _text(claim.get("claim_kind")),
|
|
487
|
+
"statement": _text(claim.get("statement")),
|
|
488
|
+
"qualification": _text(claim.get("qualification")),
|
|
489
|
+
}
|
|
490
|
+
for claim_id in claim_ids
|
|
491
|
+
if isinstance((claim := claims.get(claim_id)), Mapping)
|
|
492
|
+
],
|
|
493
|
+
}
|
|
494
|
+
)
|
|
495
|
+
return editable
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _revision_prompt(
|
|
499
|
+
*,
|
|
500
|
+
document: Mapping[str, Any],
|
|
501
|
+
reports: list[Mapping[str, Any]],
|
|
502
|
+
preparation: Mapping[str, Any],
|
|
503
|
+
source_registry: Mapping[str, Any],
|
|
504
|
+
max_edits: int,
|
|
505
|
+
partial_score_recovery: bool = False,
|
|
506
|
+
) -> str:
|
|
507
|
+
editable_blocks = _editable_block_context(document)
|
|
508
|
+
abstract = _mapping(document.get("abstract"))
|
|
509
|
+
payload = {
|
|
510
|
+
"operation": "research_plan_document_quality_revision",
|
|
511
|
+
"manuscript_markdown": render_quality_review_markdown(document),
|
|
512
|
+
"quality_reports": reports,
|
|
513
|
+
"available_research_context": _revision_context(preparation, source_registry),
|
|
514
|
+
"editable_blocks": editable_blocks,
|
|
515
|
+
"abstract_claim_ids": [_text(claim_id) for claim_id in abstract.get("claim_ids") or [] if _text(claim_id)],
|
|
516
|
+
"max_block_edits": max_edits,
|
|
517
|
+
"output_contract": {
|
|
518
|
+
"schema_version": AUTHOR_DOCUMENT_REVISION_SCHEMA_VERSION,
|
|
519
|
+
"revision_summary": "short summary",
|
|
520
|
+
"abstract_text": "optional complete replacement",
|
|
521
|
+
"abstract_supporting_claim_ids": ["required only when abstract_text is supplied; choose existing abstract_claim_ids"],
|
|
522
|
+
"block_edits": [{"section_id": "existing", "block_id": "existing", "text": "complete replacement", "supporting_claim_ids": ["one or more claim_ids already attached to this block"], "heading": "optional replacement"}],
|
|
523
|
+
},
|
|
524
|
+
}
|
|
525
|
+
recovery_instruction = (
|
|
526
|
+
"A subset of judge reports was unavailable for this draft. Use the available reports to make one focused recovery revision; "
|
|
527
|
+
"the revised draft must earn a complete scorecard before it can compete for selection. "
|
|
528
|
+
if partial_score_recovery
|
|
529
|
+
else ""
|
|
530
|
+
)
|
|
531
|
+
return """You are revising a complete academic research plan after a detailed quality review. Strengthen the argument, depth, structure, mathematical explanation, transitions, and precision where the reports identify useful opportunities. Preserve the manuscript's strongest contributions. Work freely with the supplied research context, but keep the plan honest about what is proposed, conditional, or unresolved. For a mathematics-theory manuscript, make its reasoning auditable: use supplied candidates, proof obligations, falsifiers, no-information branches, and next actions to close the decision logic. Prefer a specific lemma, criterion, implication, or response over repeating that a dependency is undefined or needs confirmation. Do not invent missing definitions or completed proofs merely to improve a score. Return a compact set of high-value complete block replacements; do not rewrite blocks that do not benefit from revision. Existing citations, claim identifiers, and section structure are maintained by the Author backend, so focus on stronger scholarly prose and mathematical or methodological exposition. Every replacement must identify the already attached claims that support it; this preserves the proposal status of the underlying material while allowing deeper synthesis. Do not state a proposed or unverified result as completed, proved, observed, or universal. """ + recovery_instruction + """Return exactly one JSON object.\n\nINPUT_JSON:\n""" + json.dumps(payload, ensure_ascii=False)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _apply_patch(document: Mapping[str, Any], patch: object, *, max_edits: int) -> tuple[dict[str, Any] | None, str]:
|
|
535
|
+
if not isinstance(patch, Mapping) or _text(patch.get("schema_version")) != AUTHOR_DOCUMENT_REVISION_SCHEMA_VERSION:
|
|
536
|
+
return None, "revision response does not match the document revision contract"
|
|
537
|
+
edits = patch.get("block_edits")
|
|
538
|
+
if not isinstance(edits, list) or len(edits) > max_edits:
|
|
539
|
+
return None, "revision has an invalid number of block edits"
|
|
540
|
+
candidate = deepcopy(dict(document))
|
|
541
|
+
block_lookup = {
|
|
542
|
+
(_text(section.get("section_id")), _text(block.get("block_id"))): block
|
|
543
|
+
for section in [*(candidate.get("sections") or []), *(candidate.get("appendices") or [])]
|
|
544
|
+
if isinstance(section, dict)
|
|
545
|
+
for block in section.get("blocks") or []
|
|
546
|
+
if isinstance(block, dict)
|
|
547
|
+
}
|
|
548
|
+
seen: set[tuple[str, str]] = set()
|
|
549
|
+
for edit in edits:
|
|
550
|
+
if not isinstance(edit, Mapping):
|
|
551
|
+
return None, "revision edit must be an object"
|
|
552
|
+
key = (_text(edit.get("section_id")), _text(edit.get("block_id")))
|
|
553
|
+
text = _text(edit.get("text"))
|
|
554
|
+
if key not in block_lookup or key in seen or not text:
|
|
555
|
+
return None, "revision edit targets an unknown, repeated, or empty block"
|
|
556
|
+
if _VISIBLE_CITATION_TOKEN.search(text):
|
|
557
|
+
return None, "revision may not add inline citation tokens; citations are derived by the Author backend"
|
|
558
|
+
seen.add(key)
|
|
559
|
+
block = block_lookup[key]
|
|
560
|
+
supporting_claim_ids = [_text(claim_id) for claim_id in edit.get("supporting_claim_ids") or [] if _text(claim_id)]
|
|
561
|
+
existing_claim_ids = {_text(claim_id) for claim_id in block.get("claim_ids") or [] if _text(claim_id)}
|
|
562
|
+
if not supporting_claim_ids or set(supporting_claim_ids) - existing_claim_ids:
|
|
563
|
+
return None, "revision edit must identify existing block claim_ids that support its replacement"
|
|
564
|
+
block["text"] = text
|
|
565
|
+
if "heading" in edit:
|
|
566
|
+
block["heading"] = _text(edit.get("heading"))
|
|
567
|
+
abstract_text = _text(patch.get("abstract_text"))
|
|
568
|
+
if abstract_text:
|
|
569
|
+
abstract = _mapping(candidate.get("abstract"))
|
|
570
|
+
supporting_claim_ids = [_text(claim_id) for claim_id in patch.get("abstract_supporting_claim_ids") or [] if _text(claim_id)]
|
|
571
|
+
abstract_claim_ids = {_text(claim_id) for claim_id in abstract.get("claim_ids") or [] if _text(claim_id)}
|
|
572
|
+
if not supporting_claim_ids or set(supporting_claim_ids) - abstract_claim_ids:
|
|
573
|
+
return None, "abstract revision must identify existing abstract claim_ids that support its replacement"
|
|
574
|
+
abstract["text"] = abstract_text
|
|
575
|
+
candidate["abstract"] = abstract
|
|
576
|
+
if not edits and not abstract_text:
|
|
577
|
+
return None, "revision contains no changes"
|
|
578
|
+
for block in block_lookup.values():
|
|
579
|
+
if _text(block.get("kind")) == "equation":
|
|
580
|
+
try:
|
|
581
|
+
fragments = split_equation_content(
|
|
582
|
+
block.get("text"),
|
|
583
|
+
label=f"quality revision block {_text(block.get('block_id'))}",
|
|
584
|
+
)
|
|
585
|
+
if not any(kind == "equation" for kind, _ in fragments):
|
|
586
|
+
raise LatexSafetyError(
|
|
587
|
+
f"quality revision block {_text(block.get('block_id'))} contains no valid mathematical expression"
|
|
588
|
+
)
|
|
589
|
+
except LatexSafetyError as error:
|
|
590
|
+
return None, str(error)
|
|
591
|
+
return candidate, _text(patch.get("revision_summary"))
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def optimize_research_plan_document(
|
|
595
|
+
document: Mapping[str, Any],
|
|
596
|
+
*,
|
|
597
|
+
preparation: Mapping[str, Any],
|
|
598
|
+
routing: Mapping[str, Any],
|
|
599
|
+
source_registry: Mapping[str, Any],
|
|
600
|
+
quality_config: Mapping[str, Any] | None,
|
|
601
|
+
judge_llm_call: Callable[..., object] | None,
|
|
602
|
+
revision_llm_call: Callable[..., object] | None,
|
|
603
|
+
logger: Any | None = None,
|
|
604
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
605
|
+
"""Score bounded document candidates and retain the best valid version."""
|
|
606
|
+
|
|
607
|
+
settings = _mapping(quality_config)
|
|
608
|
+
enabled = _as_bool(settings.get("enabled"), True)
|
|
609
|
+
special_weight = float(_setting_or_default(settings, "special_score_weight", 0.25))
|
|
610
|
+
partial_score_min_valid_dimensions = max(
|
|
611
|
+
1,
|
|
612
|
+
int(_setting_or_default(settings, "partial_score_min_valid_dimensions", 8)),
|
|
613
|
+
)
|
|
614
|
+
max_iterations = max(0, int(_setting_or_default(settings, "max_iterations", 2)))
|
|
615
|
+
max_edits = max(1, int(_setting_or_default(settings, "max_revision_block_edits", 14)))
|
|
616
|
+
concurrency = max(1, int(_setting_or_default(settings, "score_concurrency", 3)))
|
|
617
|
+
judge_retries = max(1, int(_setting_or_default(settings, "judge_max_retries", 3)))
|
|
618
|
+
template_family = _text(routing.get("template_family"))
|
|
619
|
+
special = _SPECIAL_RUBRICS.get(template_family)
|
|
620
|
+
effective_special_weight = special_weight if special is not None else 0.0
|
|
621
|
+
special_dimension_weights = {
|
|
622
|
+
dimension: weight
|
|
623
|
+
for dimension, _description, weight in special or ()
|
|
624
|
+
}
|
|
625
|
+
audit: dict[str, Any] = {
|
|
626
|
+
"schema_version": AUTHOR_DOCUMENT_QUALITY_SCHEMA_VERSION,
|
|
627
|
+
"enabled": enabled,
|
|
628
|
+
"model": _text(settings.get("model")),
|
|
629
|
+
"template_family": template_family,
|
|
630
|
+
"general_score_weight": round(1 - effective_special_weight, 4),
|
|
631
|
+
"special_score_weight": round(effective_special_weight, 4),
|
|
632
|
+
"special_dimensions": [
|
|
633
|
+
{"dimension": dimension, "weight": weight}
|
|
634
|
+
for dimension, _description, weight in special or ()
|
|
635
|
+
],
|
|
636
|
+
"partial_score_min_valid_dimensions": partial_score_min_valid_dimensions,
|
|
637
|
+
"candidates": [],
|
|
638
|
+
"selected_candidate_index": 0,
|
|
639
|
+
"winning_candidate_index": None,
|
|
640
|
+
"selection_status": "UNRESOLVED",
|
|
641
|
+
"warnings": [],
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
def score(candidate: Mapping[str, Any], index: int, parent_index: int | None, summary: str) -> dict[str, Any]:
|
|
645
|
+
record: dict[str, Any] = {
|
|
646
|
+
"candidate_index": index,
|
|
647
|
+
"parent_index": parent_index,
|
|
648
|
+
"revision_summary": summary,
|
|
649
|
+
"markdown": render_research_plan_markdown(candidate),
|
|
650
|
+
"status": "UNSCORED",
|
|
651
|
+
"dimension_reports": [],
|
|
652
|
+
"judge_attempts": [],
|
|
653
|
+
"failed_dimensions": [],
|
|
654
|
+
}
|
|
655
|
+
if not enabled or judge_llm_call is None:
|
|
656
|
+
record["status"] = "SKIPPED"
|
|
657
|
+
return record
|
|
658
|
+
manuscript = render_quality_review_markdown(candidate)
|
|
659
|
+
requested = [(dimension, _text(_mapping(EVAL_CRITERIA.get(dimension)).get("description"))) for dimension in _RUBRIC_DIMENSIONS]
|
|
660
|
+
if special is not None:
|
|
661
|
+
requested.extend(
|
|
662
|
+
(dimension, description)
|
|
663
|
+
for dimension, description, _weight in special
|
|
664
|
+
)
|
|
665
|
+
reports: dict[str, dict[str, Any]] = {}
|
|
666
|
+
|
|
667
|
+
def emit_attempt(attempt_record: Mapping[str, Any]) -> None:
|
|
668
|
+
if logger is None:
|
|
669
|
+
return
|
|
670
|
+
level = (
|
|
671
|
+
"WARNING"
|
|
672
|
+
if attempt_record.get("grounding_warnings")
|
|
673
|
+
else "INFO" if attempt_record.get("status") == "VALID" else "WARNING"
|
|
674
|
+
)
|
|
675
|
+
logger.emit(
|
|
676
|
+
"document_quality",
|
|
677
|
+
"dimension_score_attempt",
|
|
678
|
+
level=level,
|
|
679
|
+
status=str(attempt_record.get("status") or "WARNING"),
|
|
680
|
+
candidate_index=index,
|
|
681
|
+
dimension=attempt_record.get("dimension"),
|
|
682
|
+
attempt=attempt_record.get("attempt"),
|
|
683
|
+
failure_category=attempt_record.get("failure_category"),
|
|
684
|
+
validation_errors=attempt_record.get("validation_errors") or [],
|
|
685
|
+
grounding_warnings=attempt_record.get("grounding_warnings") or [],
|
|
686
|
+
response_summary=attempt_record.get("response_summary") or {},
|
|
687
|
+
error_type=attempt_record.get("error_type"),
|
|
688
|
+
failure_detail=attempt_record.get("failure_detail"),
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
def judge_dimension(dimension: str, description: str) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
|
|
692
|
+
attempts: list[dict[str, Any]] = []
|
|
693
|
+
for attempt in range(judge_retries):
|
|
694
|
+
attempt_number = attempt + 1
|
|
695
|
+
try:
|
|
696
|
+
response = call_required_json(
|
|
697
|
+
judge_llm_call,
|
|
698
|
+
_judge_prompt(dimension=dimension, description=description, manuscript=manuscript),
|
|
699
|
+
stage=f"author_document_quality:{dimension}",
|
|
700
|
+
)
|
|
701
|
+
parsed, diagnostic = _parse_dimension_report(response, dimension=dimension, document=candidate)
|
|
702
|
+
attempt_record: dict[str, Any] = {
|
|
703
|
+
"dimension": dimension,
|
|
704
|
+
"attempt": attempt_number,
|
|
705
|
+
"response_summary": _response_summary(response),
|
|
706
|
+
}
|
|
707
|
+
if parsed is not None:
|
|
708
|
+
attempt_record["status"] = "VALID"
|
|
709
|
+
grounding_warnings = list(diagnostic.get("grounding_warnings") or [])
|
|
710
|
+
if grounding_warnings:
|
|
711
|
+
attempt_record["warning_category"] = "grounding_warning"
|
|
712
|
+
attempt_record["grounding_warnings"] = grounding_warnings
|
|
713
|
+
attempts.append(attempt_record)
|
|
714
|
+
emit_attempt(attempt_record)
|
|
715
|
+
return parsed, attempts
|
|
716
|
+
attempt_record.update(
|
|
717
|
+
{
|
|
718
|
+
"status": "INVALID",
|
|
719
|
+
"failure_category": diagnostic.get("failure_category") or "report_contract",
|
|
720
|
+
"validation_errors": list(diagnostic.get("validation_errors") or []),
|
|
721
|
+
}
|
|
722
|
+
)
|
|
723
|
+
except Exception as error:
|
|
724
|
+
attempt_record = {
|
|
725
|
+
"dimension": dimension,
|
|
726
|
+
"attempt": attempt_number,
|
|
727
|
+
"status": "ERROR",
|
|
728
|
+
"failure_category": "judge_call_error",
|
|
729
|
+
"error_type": type(error).__name__,
|
|
730
|
+
"failure_detail": _text(error)[:400],
|
|
731
|
+
}
|
|
732
|
+
attempts.append(attempt_record)
|
|
733
|
+
emit_attempt(attempt_record)
|
|
734
|
+
return None, attempts
|
|
735
|
+
|
|
736
|
+
judge_attempts: list[dict[str, Any]] = []
|
|
737
|
+
with ThreadPoolExecutor(max_workers=min(concurrency, len(requested))) as executor:
|
|
738
|
+
futures = {
|
|
739
|
+
executor.submit(judge_dimension, dimension, description): dimension
|
|
740
|
+
for dimension, description in requested
|
|
741
|
+
}
|
|
742
|
+
for future in as_completed(futures):
|
|
743
|
+
dimension = futures[future]
|
|
744
|
+
try:
|
|
745
|
+
parsed, dimension_attempts = future.result()
|
|
746
|
+
except Exception as error:
|
|
747
|
+
parsed = None
|
|
748
|
+
dimension_attempts = [{
|
|
749
|
+
"dimension": dimension,
|
|
750
|
+
"attempt": 0,
|
|
751
|
+
"status": "ERROR",
|
|
752
|
+
"failure_category": "judge_worker_error",
|
|
753
|
+
"error_type": type(error).__name__,
|
|
754
|
+
"failure_detail": _text(error)[:400],
|
|
755
|
+
}]
|
|
756
|
+
emit_attempt(dimension_attempts[0])
|
|
757
|
+
judge_attempts.extend(dimension_attempts)
|
|
758
|
+
if parsed is None:
|
|
759
|
+
record["failed_dimensions"].append(dimension)
|
|
760
|
+
last_attempt = dimension_attempts[-1] if dimension_attempts else {}
|
|
761
|
+
category = _text(last_attempt.get("failure_category")) or "unknown_failure"
|
|
762
|
+
audit["warnings"].append(
|
|
763
|
+
f"candidate {index} judge report for {dimension} was unavailable after "
|
|
764
|
+
f"{len(dimension_attempts)} attempt(s): {category}"
|
|
765
|
+
)
|
|
766
|
+
else:
|
|
767
|
+
reports[dimension] = parsed
|
|
768
|
+
requested_index = {dimension: position for position, (dimension, _description) in enumerate(requested)}
|
|
769
|
+
record["judge_attempts"] = sorted(
|
|
770
|
+
judge_attempts,
|
|
771
|
+
key=lambda attempt: (requested_index.get(_text(attempt.get("dimension")), len(requested)), int(attempt.get("attempt") or 0)),
|
|
772
|
+
)
|
|
773
|
+
record["failed_dimensions"] = [
|
|
774
|
+
dimension for dimension, _description in requested if dimension in set(record["failed_dimensions"])
|
|
775
|
+
]
|
|
776
|
+
record["dimension_reports"] = [reports[key] for key, _description in requested if key in reports]
|
|
777
|
+
if len(reports) != len(requested):
|
|
778
|
+
record["partial_scorecard"] = {
|
|
779
|
+
"available_dimension_scores": {
|
|
780
|
+
dimension: reports[dimension]["score"]
|
|
781
|
+
for dimension, _description in requested
|
|
782
|
+
if dimension in reports
|
|
783
|
+
},
|
|
784
|
+
"missing_dimensions": list(record["failed_dimensions"]),
|
|
785
|
+
"valid_dimension_count": len(reports),
|
|
786
|
+
"requested_dimension_count": len(requested),
|
|
787
|
+
"complete": False,
|
|
788
|
+
}
|
|
789
|
+
record["status"] = (
|
|
790
|
+
"PARTIALLY_SCORED"
|
|
791
|
+
if len(reports) >= partial_score_min_valid_dimensions
|
|
792
|
+
else "JUDGE_FAILED"
|
|
793
|
+
)
|
|
794
|
+
return record
|
|
795
|
+
ordinary = {dimension: reports[dimension]["score"] for dimension in _RUBRIC_DIMENSIONS}
|
|
796
|
+
special_scores = {
|
|
797
|
+
dimension: reports[dimension]["score"]
|
|
798
|
+
for dimension, _description, _weight in special or ()
|
|
799
|
+
}
|
|
800
|
+
record["scorecard"] = _scorecard(
|
|
801
|
+
ordinary,
|
|
802
|
+
special_scores,
|
|
803
|
+
effective_special_weight,
|
|
804
|
+
special_dimension_weights=special_dimension_weights,
|
|
805
|
+
)
|
|
806
|
+
record["status"] = "SCORED"
|
|
807
|
+
return record
|
|
808
|
+
|
|
809
|
+
baseline = deepcopy(dict(document))
|
|
810
|
+
candidates = [score(baseline, 0, None, "Initial composed manuscript")]
|
|
811
|
+
candidate_documents = [baseline]
|
|
812
|
+
if logger is not None:
|
|
813
|
+
logger.emit(
|
|
814
|
+
"document_quality",
|
|
815
|
+
"candidate_scored",
|
|
816
|
+
status=candidates[0]["status"],
|
|
817
|
+
candidate_index=0,
|
|
818
|
+
total_score=_mapping(candidates[0].get("scorecard")).get("total_score"),
|
|
819
|
+
failed_dimension_count=len(candidates[0].get("failed_dimensions") or []),
|
|
820
|
+
failed_dimensions=candidates[0].get("failed_dimensions") or [],
|
|
821
|
+
)
|
|
822
|
+
current = baseline
|
|
823
|
+
current_index = 0
|
|
824
|
+
partial_score_recovery_used = False
|
|
825
|
+
for iteration in range(1, max_iterations + 1):
|
|
826
|
+
current_record = candidates[current_index]
|
|
827
|
+
current_status = _text(current_record.get("status"))
|
|
828
|
+
is_partial_recovery = current_status == "PARTIALLY_SCORED"
|
|
829
|
+
if current_status not in {"SCORED", "PARTIALLY_SCORED"} or revision_llm_call is None:
|
|
830
|
+
break
|
|
831
|
+
if is_partial_recovery and partial_score_recovery_used:
|
|
832
|
+
break
|
|
833
|
+
if is_partial_recovery:
|
|
834
|
+
current_record["partial_score_recovery_attempted"] = True
|
|
835
|
+
try:
|
|
836
|
+
patch = call_required_json(
|
|
837
|
+
revision_llm_call,
|
|
838
|
+
_revision_prompt(
|
|
839
|
+
document=current,
|
|
840
|
+
reports=list(current_record["dimension_reports"]),
|
|
841
|
+
preparation=preparation,
|
|
842
|
+
source_registry=source_registry,
|
|
843
|
+
max_edits=max_edits,
|
|
844
|
+
partial_score_recovery=is_partial_recovery,
|
|
845
|
+
),
|
|
846
|
+
stage="author_document_quality_revision",
|
|
847
|
+
)
|
|
848
|
+
revised, summary = _apply_patch(current, patch, max_edits=max_edits)
|
|
849
|
+
except Exception as error:
|
|
850
|
+
revised, summary = None, f"revision call failed: {type(error).__name__}"
|
|
851
|
+
if revised is None:
|
|
852
|
+
audit["warnings"].append(f"quality iteration {iteration} discarded: {summary}")
|
|
853
|
+
break
|
|
854
|
+
validation_errors = validate_composed_research_plan(revised, preparation=preparation, routing=routing, source_registry=source_registry)
|
|
855
|
+
if validation_errors:
|
|
856
|
+
audit["warnings"].append(f"quality iteration {iteration} discarded by final validation: {'; '.join(validation_errors)}")
|
|
857
|
+
break
|
|
858
|
+
candidate_index = len(candidates)
|
|
859
|
+
candidate = score(revised, candidate_index, current_index, summary)
|
|
860
|
+
candidates.append(candidate)
|
|
861
|
+
candidate_documents.append(revised)
|
|
862
|
+
if logger is not None:
|
|
863
|
+
logger.emit(
|
|
864
|
+
"document_quality",
|
|
865
|
+
"candidate_scored",
|
|
866
|
+
status=candidate["status"],
|
|
867
|
+
candidate_index=candidate_index,
|
|
868
|
+
parent_index=current_index,
|
|
869
|
+
total_score=_mapping(candidate.get("scorecard")).get("total_score"),
|
|
870
|
+
failed_dimension_count=len(candidate.get("failed_dimensions") or []),
|
|
871
|
+
failed_dimensions=candidate.get("failed_dimensions") or [],
|
|
872
|
+
)
|
|
873
|
+
current = revised
|
|
874
|
+
current_index = candidate_index
|
|
875
|
+
if is_partial_recovery:
|
|
876
|
+
partial_score_recovery_used = True
|
|
877
|
+
break
|
|
878
|
+
scored = [candidate for candidate in candidates if candidate.get("status") == "SCORED"]
|
|
879
|
+
if scored:
|
|
880
|
+
selected = max(scored, key=lambda candidate: (_mapping(candidate.get("scorecard")).get("selection_score", -1), -int(candidate["candidate_index"])))
|
|
881
|
+
selected_index = int(selected["candidate_index"])
|
|
882
|
+
selected_document = candidate_documents[selected_index]
|
|
883
|
+
audit["winning_candidate_index"] = selected_index
|
|
884
|
+
audit["selection_status"] = "COMPLETE_SCORECARD_SELECTED"
|
|
885
|
+
else:
|
|
886
|
+
selected = candidates[0]
|
|
887
|
+
selected_index = 0
|
|
888
|
+
selected_document = baseline
|
|
889
|
+
audit["selection_status"] = (
|
|
890
|
+
"PARTIAL_SCORECARD_RETAINED_AS_FALLBACK"
|
|
891
|
+
if any(candidate.get("status") == "PARTIALLY_SCORED" for candidate in candidates)
|
|
892
|
+
else "NO_COMPLETE_SCORECARD_RETAINED_AS_FALLBACK"
|
|
893
|
+
)
|
|
894
|
+
audit["warnings"].append("no complete quality scorecard was available; retained the initial manuscript without selecting a quality winner")
|
|
895
|
+
audit["candidates"] = candidates
|
|
896
|
+
audit["selected_candidate_index"] = selected_index
|
|
897
|
+
return deepcopy(dict(selected_document)), audit
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
def render_document_quality_report(audit: Mapping[str, Any]) -> str:
|
|
901
|
+
"""Render a human-readable score report without exposing prompts or sources."""
|
|
902
|
+
|
|
903
|
+
lines = ["# Research Plan Quality Report"]
|
|
904
|
+
winning_index = audit.get("winning_candidate_index")
|
|
905
|
+
if winning_index is None:
|
|
906
|
+
lines.append(f"- Retained manuscript candidate: {audit.get('selected_candidate_index', 0)} (no quality winner selected)")
|
|
907
|
+
else:
|
|
908
|
+
lines.append(f"- Selected candidate: {winning_index}")
|
|
909
|
+
for candidate in audit.get("candidates") or []:
|
|
910
|
+
if not isinstance(candidate, Mapping):
|
|
911
|
+
continue
|
|
912
|
+
lines.append(f"## Candidate {candidate.get('candidate_index')} — {candidate.get('status')}")
|
|
913
|
+
scorecard = _mapping(candidate.get("scorecard"))
|
|
914
|
+
if scorecard:
|
|
915
|
+
lines.append(f"- Total Score: {scorecard.get('total_score')} / 10")
|
|
916
|
+
lines.append(f"- Selection Score: {scorecard.get('selection_score')} / 10")
|
|
917
|
+
special_weights = _mapping(scorecard.get("special_dimension_weights"))
|
|
918
|
+
if special_weights:
|
|
919
|
+
lines.append(
|
|
920
|
+
"- Theory/domain score weights: "
|
|
921
|
+
+ ", ".join(f"{dimension}={weight}" for dimension, weight in special_weights.items())
|
|
922
|
+
)
|
|
923
|
+
partial_scorecard = _mapping(candidate.get("partial_scorecard"))
|
|
924
|
+
if partial_scorecard:
|
|
925
|
+
lines.append(
|
|
926
|
+
"- Partial scorecard: "
|
|
927
|
+
+ f"{partial_scorecard.get('valid_dimension_count')} of "
|
|
928
|
+
+ f"{partial_scorecard.get('requested_dimension_count')} reports were valid; "
|
|
929
|
+
+ "this candidate cannot be selected."
|
|
930
|
+
)
|
|
931
|
+
missing_dimensions = partial_scorecard.get("missing_dimensions") or []
|
|
932
|
+
if missing_dimensions:
|
|
933
|
+
lines.append("- Unavailable dimensions: " + ", ".join(_text(item) for item in missing_dimensions))
|
|
934
|
+
failed_attempts = [
|
|
935
|
+
attempt
|
|
936
|
+
for attempt in candidate.get("judge_attempts") or []
|
|
937
|
+
if isinstance(attempt, Mapping) and _text(attempt.get("status")) != "VALID"
|
|
938
|
+
]
|
|
939
|
+
if failed_attempts:
|
|
940
|
+
lines.append("#### Judge Diagnostics")
|
|
941
|
+
for attempt in failed_attempts:
|
|
942
|
+
detail = "; ".join(_text(error) for error in attempt.get("validation_errors") or [] if _text(error))
|
|
943
|
+
if not detail:
|
|
944
|
+
detail = _text(attempt.get("failure_detail")) or _text(attempt.get("error_type"))
|
|
945
|
+
lines.append(
|
|
946
|
+
f"- {attempt.get('dimension')} attempt {attempt.get('attempt')}: "
|
|
947
|
+
f"{attempt.get('failure_category') or 'unknown_failure'}"
|
|
948
|
+
+ (f" — {detail}" if detail else "")
|
|
949
|
+
)
|
|
950
|
+
for report in candidate.get("dimension_reports") or []:
|
|
951
|
+
if not isinstance(report, Mapping):
|
|
952
|
+
continue
|
|
953
|
+
lines.append(f"### {report.get('dimension')} — {report.get('score')} / 10")
|
|
954
|
+
lines.append(_text(report.get("rationale")))
|
|
955
|
+
for warning in report.get("grounding_warnings") or []:
|
|
956
|
+
if _text(warning):
|
|
957
|
+
lines.append("**Grounding warning:** " + _text(warning))
|
|
958
|
+
for evidence in report.get("evidence") or []:
|
|
959
|
+
if isinstance(evidence, Mapping):
|
|
960
|
+
lines.append(
|
|
961
|
+
"**Grounding:** "
|
|
962
|
+
+ f"{_text(evidence.get('section_id'))}/{_text(evidence.get('block_id'))} — "
|
|
963
|
+
+ _text(evidence.get("assessment"))
|
|
964
|
+
)
|
|
965
|
+
strength = _mapping(report.get("maximum_strength"))
|
|
966
|
+
if strength.get("present"):
|
|
967
|
+
lines.append("**Maximum strength:** " + _text(strength.get("description")))
|
|
968
|
+
if _text(strength.get("location_status")) == "pending_review":
|
|
969
|
+
lines.append("**Location:** pending review")
|
|
970
|
+
for weakness in report.get("major_weaknesses") or []:
|
|
971
|
+
if isinstance(weakness, Mapping):
|
|
972
|
+
lines.append("**Major weakness:** " + _text(weakness.get("description")))
|
|
973
|
+
if _text(weakness.get("location_status")) == "pending_review":
|
|
974
|
+
lines.append("**Location:** pending review")
|
|
975
|
+
lines.append("**Direction:** " + _text(weakness.get("repair_direction")))
|
|
976
|
+
for direction in report.get("polish_directions") or []:
|
|
977
|
+
if isinstance(direction, Mapping):
|
|
978
|
+
lines.append("**Polish direction:** " + _text(direction.get("direction")))
|
|
979
|
+
return "\n\n".join(line for line in lines if line.strip()) + "\n"
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
__all__ = [
|
|
983
|
+
"AUTHOR_DOCUMENT_QUALITY_SCHEMA_VERSION",
|
|
984
|
+
"AUTHOR_DOCUMENT_REVISION_SCHEMA_VERSION",
|
|
985
|
+
"optimize_research_plan_document",
|
|
986
|
+
"render_document_quality_report",
|
|
987
|
+
]
|