alysis-code 0.13.0__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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import mimetypes
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import tempfile
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from contextlib import suppress
|
|
14
|
+
from dataclasses import replace
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from ..atomic_io import atomic_write_text
|
|
19
|
+
from ..config import (
|
|
20
|
+
AppConfig,
|
|
21
|
+
ConfigError,
|
|
22
|
+
get_api_key,
|
|
23
|
+
resolve_llm_enable_thinking,
|
|
24
|
+
resolve_llm_timeout_s,
|
|
25
|
+
resolve_model_access_api_key,
|
|
26
|
+
resolve_profile_api_key,
|
|
27
|
+
resolve_prompt_cache_key,
|
|
28
|
+
resolve_prompt_cache_retention,
|
|
29
|
+
resolve_role_temperature,
|
|
30
|
+
)
|
|
31
|
+
from ..forge import RunPaths, now_iso
|
|
32
|
+
from ..llm.factory import make_llm_client
|
|
33
|
+
from ..llm.types import LLMError, LLMResponse
|
|
34
|
+
from ..model_registry import ModelRegistry
|
|
35
|
+
from ..model_router import ROLE_COMPREHENSION, resolve_model_for_role
|
|
36
|
+
from ..profiles import ProfileSpec, get_profile
|
|
37
|
+
from .index import AssetIndex
|
|
38
|
+
from .models import (
|
|
39
|
+
AssetError,
|
|
40
|
+
AssetRecord,
|
|
41
|
+
ComprehensionData,
|
|
42
|
+
ComprehensionRecord,
|
|
43
|
+
)
|
|
44
|
+
from .ocr import OcrProvider, OcrResult
|
|
45
|
+
from .paths import asset_extracted_text_path, asset_preview_path, repo_rel
|
|
46
|
+
from .prompts import (
|
|
47
|
+
build_retry_prompt,
|
|
48
|
+
build_text_comprehension_prompt,
|
|
49
|
+
build_vision_comprehension_prompt,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
_JSON_OBJECT_RE = re.compile(r"\{.*\}", re.DOTALL)
|
|
53
|
+
_MAX_FAILURE_ERROR_CHARS = 500
|
|
54
|
+
_TOP_LEVEL_KEYS = {"detected_language", "language_confidence", "data"}
|
|
55
|
+
_DATA_KEYS = {
|
|
56
|
+
"semantic_summary",
|
|
57
|
+
"classification",
|
|
58
|
+
"key_entities",
|
|
59
|
+
"stated_facts",
|
|
60
|
+
"stated_decisions",
|
|
61
|
+
"stated_constraints",
|
|
62
|
+
"actionable_signals",
|
|
63
|
+
"open_questions",
|
|
64
|
+
"relations_hint",
|
|
65
|
+
"confidence",
|
|
66
|
+
}
|
|
67
|
+
_CLASSIFICATION_KEYS = {"kind", "subkind", "domain"}
|
|
68
|
+
_COMPREHENSION_TOOL_NAME = "record_asset_comprehension"
|
|
69
|
+
_COMPREHENSION_TOOL_CHOICE = {
|
|
70
|
+
"type": "function",
|
|
71
|
+
"function": {"name": _COMPREHENSION_TOOL_NAME},
|
|
72
|
+
}
|
|
73
|
+
_COMPREHENSION_TOOL: dict[str, Any] = {
|
|
74
|
+
"type": "function",
|
|
75
|
+
"function": {
|
|
76
|
+
"name": _COMPREHENSION_TOOL_NAME,
|
|
77
|
+
"description": "Persist the structured comprehension for a user-provided asset.",
|
|
78
|
+
"strict": True,
|
|
79
|
+
"parameters": {
|
|
80
|
+
"type": "object",
|
|
81
|
+
"additionalProperties": False,
|
|
82
|
+
"required": ["detected_language", "language_confidence", "data"],
|
|
83
|
+
"properties": {
|
|
84
|
+
"detected_language": {
|
|
85
|
+
"anyOf": [{"type": "string"}, {"type": "null"}],
|
|
86
|
+
"description": "ISO 639-1 primary language code, or null if unknown.",
|
|
87
|
+
},
|
|
88
|
+
"language_confidence": {
|
|
89
|
+
"anyOf": [{"type": "number"}, {"type": "null"}],
|
|
90
|
+
"description": "Confidence from 0.0 to 1.0, or null if unknown.",
|
|
91
|
+
},
|
|
92
|
+
"data": {
|
|
93
|
+
"type": "object",
|
|
94
|
+
"additionalProperties": False,
|
|
95
|
+
"required": [
|
|
96
|
+
"semantic_summary",
|
|
97
|
+
"classification",
|
|
98
|
+
"key_entities",
|
|
99
|
+
"stated_facts",
|
|
100
|
+
"stated_decisions",
|
|
101
|
+
"stated_constraints",
|
|
102
|
+
"actionable_signals",
|
|
103
|
+
"open_questions",
|
|
104
|
+
"relations_hint",
|
|
105
|
+
"confidence",
|
|
106
|
+
],
|
|
107
|
+
"properties": {
|
|
108
|
+
"semantic_summary": {"type": "string"},
|
|
109
|
+
"classification": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"additionalProperties": False,
|
|
112
|
+
"required": ["kind", "subkind", "domain"],
|
|
113
|
+
"properties": {
|
|
114
|
+
"kind": {"type": "string"},
|
|
115
|
+
"subkind": {"type": "string"},
|
|
116
|
+
"domain": {"type": "string"},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
"key_entities": {
|
|
120
|
+
"type": "array",
|
|
121
|
+
"items": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"additionalProperties": False,
|
|
124
|
+
"required": ["type", "value"],
|
|
125
|
+
"properties": {
|
|
126
|
+
"type": {"type": "string"},
|
|
127
|
+
"value": {"type": "string"},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
"stated_facts": {"type": "array", "items": {"type": "string"}},
|
|
132
|
+
"stated_decisions": {"type": "array", "items": {"type": "string"}},
|
|
133
|
+
"stated_constraints": {"type": "array", "items": {"type": "string"}},
|
|
134
|
+
"actionable_signals": {"type": "array", "items": {"type": "string"}},
|
|
135
|
+
"open_questions": {"type": "array", "items": {"type": "string"}},
|
|
136
|
+
"relations_hint": {"type": "string"},
|
|
137
|
+
"confidence": {
|
|
138
|
+
"type": "object",
|
|
139
|
+
"additionalProperties": {"type": "number"},
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
LOGGER = logging.getLogger(__name__)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class AssetComprehender:
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
*,
|
|
154
|
+
cfg: AppConfig,
|
|
155
|
+
model_registry: ModelRegistry,
|
|
156
|
+
ocr_provider: OcrProvider | None,
|
|
157
|
+
run_paths: RunPaths,
|
|
158
|
+
) -> None:
|
|
159
|
+
self.cfg = cfg
|
|
160
|
+
self.model_registry = model_registry
|
|
161
|
+
self.ocr_provider = ocr_provider
|
|
162
|
+
self.run_paths = run_paths
|
|
163
|
+
self.index = AssetIndex(run_paths)
|
|
164
|
+
|
|
165
|
+
def comprehend(
|
|
166
|
+
self,
|
|
167
|
+
asset: AssetRecord,
|
|
168
|
+
*,
|
|
169
|
+
previous: ComprehensionRecord | None = None,
|
|
170
|
+
angle: str | None = None,
|
|
171
|
+
) -> ComprehensionRecord:
|
|
172
|
+
_ = previous
|
|
173
|
+
started = time.monotonic()
|
|
174
|
+
try:
|
|
175
|
+
if asset.kind == "text":
|
|
176
|
+
return self._comprehend_text_asset(asset=asset, started=started, angle=angle)
|
|
177
|
+
if asset.kind == "image":
|
|
178
|
+
return self._comprehend_image_asset(asset=asset, started=started, angle=angle)
|
|
179
|
+
raise AssetError(f"Unsupported asset kind: {asset.kind}")
|
|
180
|
+
except Exception as exc: # noqa: BLE001 - failures must become durable records
|
|
181
|
+
failed = self._failed_record(asset=asset, started=started, error=str(exc))
|
|
182
|
+
return self.index.write_comprehension(failed)
|
|
183
|
+
|
|
184
|
+
def comprehend_async_threadable(
|
|
185
|
+
self,
|
|
186
|
+
asset: AssetRecord,
|
|
187
|
+
callback: Callable[[ComprehensionRecord], None],
|
|
188
|
+
) -> threading.Thread:
|
|
189
|
+
def _target() -> None:
|
|
190
|
+
callback(self.comprehend(asset))
|
|
191
|
+
|
|
192
|
+
thread = threading.Thread(target=_target, daemon=True)
|
|
193
|
+
thread.start()
|
|
194
|
+
return thread
|
|
195
|
+
|
|
196
|
+
def _comprehend_text_asset(
|
|
197
|
+
self,
|
|
198
|
+
*,
|
|
199
|
+
asset: AssetRecord,
|
|
200
|
+
started: float,
|
|
201
|
+
angle: str | None,
|
|
202
|
+
) -> ComprehensionRecord:
|
|
203
|
+
role, model, profile = self._primary_model()
|
|
204
|
+
if not _has_inspection_angle(angle):
|
|
205
|
+
cached = self._cache_hit(asset=asset, model=model, role=role, started=started)
|
|
206
|
+
if cached is not None:
|
|
207
|
+
return cached
|
|
208
|
+
text = self._load_extracted_text(asset)
|
|
209
|
+
prompt = build_text_comprehension_prompt(
|
|
210
|
+
asset=asset,
|
|
211
|
+
text=text,
|
|
212
|
+
questioning_mode=self.cfg.assets.comprehension.questioning_mode,
|
|
213
|
+
angle=angle,
|
|
214
|
+
)
|
|
215
|
+
record = self._llm_comprehend(
|
|
216
|
+
asset=asset,
|
|
217
|
+
prompt=prompt,
|
|
218
|
+
source="text_only",
|
|
219
|
+
model=model,
|
|
220
|
+
role=role,
|
|
221
|
+
profile=profile,
|
|
222
|
+
started=started,
|
|
223
|
+
confidence_modifier=1.0,
|
|
224
|
+
)
|
|
225
|
+
return self.index.write_comprehension(record)
|
|
226
|
+
|
|
227
|
+
def _comprehend_image_asset(
|
|
228
|
+
self,
|
|
229
|
+
*,
|
|
230
|
+
asset: AssetRecord,
|
|
231
|
+
started: float,
|
|
232
|
+
angle: str | None,
|
|
233
|
+
) -> ComprehensionRecord:
|
|
234
|
+
role, primary_model, primary_profile = self._primary_model()
|
|
235
|
+
if self.model_registry.get(primary_model).supports_vision:
|
|
236
|
+
if not _has_inspection_angle(angle):
|
|
237
|
+
cached = self._cache_hit(
|
|
238
|
+
asset=asset,
|
|
239
|
+
model=primary_model,
|
|
240
|
+
role=role,
|
|
241
|
+
started=started,
|
|
242
|
+
)
|
|
243
|
+
if cached is not None:
|
|
244
|
+
return cached
|
|
245
|
+
return self._vision_comprehend(
|
|
246
|
+
asset=asset,
|
|
247
|
+
model=primary_model,
|
|
248
|
+
role=role,
|
|
249
|
+
profile=primary_profile,
|
|
250
|
+
started=started,
|
|
251
|
+
angle=angle,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
fallback = self._vision_fallback_model()
|
|
255
|
+
if fallback is not None:
|
|
256
|
+
fallback_role, fallback_model, fallback_profile = fallback
|
|
257
|
+
if not _has_inspection_angle(angle):
|
|
258
|
+
cached = self._cache_hit(
|
|
259
|
+
asset=asset,
|
|
260
|
+
model=fallback_model,
|
|
261
|
+
role=fallback_role,
|
|
262
|
+
started=started,
|
|
263
|
+
)
|
|
264
|
+
if cached is not None:
|
|
265
|
+
return cached
|
|
266
|
+
return self._vision_comprehend(
|
|
267
|
+
asset=asset,
|
|
268
|
+
model=fallback_model,
|
|
269
|
+
role=fallback_role,
|
|
270
|
+
profile=fallback_profile,
|
|
271
|
+
started=started,
|
|
272
|
+
angle=angle,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
ocr_result = self._try_ocr(asset)
|
|
276
|
+
if ocr_result is not None and ocr_result.text.strip():
|
|
277
|
+
extracted_path = asset_extracted_text_path(self.run_paths, asset.id)
|
|
278
|
+
atomic_write_text(extracted_path, ocr_result.text)
|
|
279
|
+
updated_asset = self.index.update(
|
|
280
|
+
replace(asset, extracted_text_path=repo_rel(self.run_paths.root, extracted_path))
|
|
281
|
+
)
|
|
282
|
+
if not _has_inspection_angle(angle):
|
|
283
|
+
cached = self._cache_hit(
|
|
284
|
+
asset=updated_asset,
|
|
285
|
+
model=primary_model,
|
|
286
|
+
role=role,
|
|
287
|
+
started=started,
|
|
288
|
+
)
|
|
289
|
+
if cached is not None:
|
|
290
|
+
return cached
|
|
291
|
+
prompt = build_text_comprehension_prompt(
|
|
292
|
+
asset=updated_asset,
|
|
293
|
+
text=ocr_result.text,
|
|
294
|
+
questioning_mode=self.cfg.assets.comprehension.questioning_mode,
|
|
295
|
+
angle=angle,
|
|
296
|
+
)
|
|
297
|
+
record = self._llm_comprehend(
|
|
298
|
+
asset=updated_asset,
|
|
299
|
+
prompt=prompt,
|
|
300
|
+
source="ocr_only",
|
|
301
|
+
model=primary_model,
|
|
302
|
+
role=role,
|
|
303
|
+
profile=primary_profile,
|
|
304
|
+
started=started,
|
|
305
|
+
confidence_modifier=0.7,
|
|
306
|
+
ocr_result=ocr_result,
|
|
307
|
+
)
|
|
308
|
+
return self.index.write_comprehension(record)
|
|
309
|
+
|
|
310
|
+
minimal = self._minimal_record(asset=asset, started=started)
|
|
311
|
+
return self.index.write_comprehension(minimal)
|
|
312
|
+
|
|
313
|
+
def _vision_comprehend(
|
|
314
|
+
self,
|
|
315
|
+
*,
|
|
316
|
+
asset: AssetRecord,
|
|
317
|
+
model: str,
|
|
318
|
+
role: str,
|
|
319
|
+
profile: ProfileSpec | None,
|
|
320
|
+
started: float,
|
|
321
|
+
angle: str | None,
|
|
322
|
+
) -> ComprehensionRecord:
|
|
323
|
+
ocr_result: OcrResult | None = None
|
|
324
|
+
combine_with_ocr = (
|
|
325
|
+
self.cfg.assets.comprehension.vision_with_ocr_when_available
|
|
326
|
+
and self._ocr_allowed()
|
|
327
|
+
and self.ocr_provider is not None
|
|
328
|
+
and self.ocr_provider.is_available()
|
|
329
|
+
)
|
|
330
|
+
if combine_with_ocr:
|
|
331
|
+
ocr_result = self._try_ocr(asset)
|
|
332
|
+
if ocr_result is not None and ocr_result.text.strip():
|
|
333
|
+
extracted_path = asset_extracted_text_path(self.run_paths, asset.id)
|
|
334
|
+
atomic_write_text(extracted_path, ocr_result.text)
|
|
335
|
+
asset = self.index.update(
|
|
336
|
+
replace(
|
|
337
|
+
asset, extracted_text_path=repo_rel(self.run_paths.root, extracted_path)
|
|
338
|
+
)
|
|
339
|
+
)
|
|
340
|
+
preview_path = self._normalized_preview(asset)
|
|
341
|
+
prompt = build_vision_comprehension_prompt(
|
|
342
|
+
asset=asset,
|
|
343
|
+
questioning_mode=self.cfg.assets.comprehension.questioning_mode,
|
|
344
|
+
ocr_text=ocr_result.text if ocr_result is not None else None,
|
|
345
|
+
angle=angle,
|
|
346
|
+
)
|
|
347
|
+
source = (
|
|
348
|
+
"vision_with_ocr" if ocr_result is not None and ocr_result.text.strip() else "vision"
|
|
349
|
+
)
|
|
350
|
+
record = self._llm_comprehend(
|
|
351
|
+
asset=asset,
|
|
352
|
+
prompt=prompt,
|
|
353
|
+
source=source,
|
|
354
|
+
model=model,
|
|
355
|
+
role=role,
|
|
356
|
+
profile=profile,
|
|
357
|
+
started=started,
|
|
358
|
+
confidence_modifier=1.0,
|
|
359
|
+
ocr_result=ocr_result,
|
|
360
|
+
image_path=preview_path,
|
|
361
|
+
)
|
|
362
|
+
return self.index.write_comprehension(record)
|
|
363
|
+
|
|
364
|
+
def _llm_comprehend(
|
|
365
|
+
self,
|
|
366
|
+
*,
|
|
367
|
+
asset: AssetRecord,
|
|
368
|
+
prompt: str,
|
|
369
|
+
source: str,
|
|
370
|
+
model: str,
|
|
371
|
+
role: str,
|
|
372
|
+
profile: ProfileSpec | None,
|
|
373
|
+
started: float,
|
|
374
|
+
confidence_modifier: float,
|
|
375
|
+
ocr_result: OcrResult | None = None,
|
|
376
|
+
image_path: Path | None = None,
|
|
377
|
+
) -> ComprehensionRecord:
|
|
378
|
+
response = self._chat(
|
|
379
|
+
model=model, role=role, profile=profile, prompt=prompt, image_path=image_path
|
|
380
|
+
)
|
|
381
|
+
try:
|
|
382
|
+
payload = _parse_comprehension_payload(response)
|
|
383
|
+
except AssetError:
|
|
384
|
+
retry = self._chat(
|
|
385
|
+
model=model,
|
|
386
|
+
role=role,
|
|
387
|
+
profile=profile,
|
|
388
|
+
prompt=build_retry_prompt(prompt),
|
|
389
|
+
image_path=image_path,
|
|
390
|
+
)
|
|
391
|
+
payload = _parse_comprehension_payload(retry)
|
|
392
|
+
response = retry
|
|
393
|
+
data = ComprehensionData.from_dict(payload.get("data"))
|
|
394
|
+
usage = _tokens_used(response)
|
|
395
|
+
return ComprehensionRecord(
|
|
396
|
+
schema_version=1,
|
|
397
|
+
version=0,
|
|
398
|
+
asset_id=asset.id,
|
|
399
|
+
status="ready",
|
|
400
|
+
source=source, # type: ignore[arg-type]
|
|
401
|
+
model=model,
|
|
402
|
+
role=role,
|
|
403
|
+
ocr_engine=ocr_result.engine_version if ocr_result is not None else None,
|
|
404
|
+
ocr_languages_used=ocr_result.languages_used if ocr_result is not None else [],
|
|
405
|
+
detected_language=_optional_string(payload.get("detected_language")),
|
|
406
|
+
language_confidence=_optional_float(payload.get("language_confidence")),
|
|
407
|
+
confidence_modifier=confidence_modifier,
|
|
408
|
+
tokens_used=usage,
|
|
409
|
+
elapsed_ms=_elapsed_ms(started),
|
|
410
|
+
generated_at=now_iso(),
|
|
411
|
+
error=None,
|
|
412
|
+
data=data,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def _chat(
|
|
416
|
+
self,
|
|
417
|
+
*,
|
|
418
|
+
model: str,
|
|
419
|
+
role: str,
|
|
420
|
+
profile: ProfileSpec | None,
|
|
421
|
+
prompt: str,
|
|
422
|
+
image_path: Path | None,
|
|
423
|
+
) -> LLMResponse:
|
|
424
|
+
api_key = self._api_key_for_profile(profile)
|
|
425
|
+
client = make_llm_client(
|
|
426
|
+
cfg=self.cfg,
|
|
427
|
+
api_key=api_key,
|
|
428
|
+
model=model,
|
|
429
|
+
timeout_s=resolve_llm_timeout_s(self.cfg),
|
|
430
|
+
temperature=resolve_role_temperature(self.cfg, role=role),
|
|
431
|
+
prompt_cache_key=resolve_prompt_cache_key(self.cfg),
|
|
432
|
+
prompt_cache_retention=resolve_prompt_cache_retention(self.cfg),
|
|
433
|
+
enable_thinking=resolve_llm_enable_thinking(self.cfg),
|
|
434
|
+
profile=profile,
|
|
435
|
+
)
|
|
436
|
+
messages = [{"role": "system", "content": "Return structured JSON only."}]
|
|
437
|
+
if image_path is None:
|
|
438
|
+
messages.append({"role": "user", "content": prompt})
|
|
439
|
+
else:
|
|
440
|
+
messages.append({"role": "user", "content": _image_content_parts(prompt, image_path)})
|
|
441
|
+
try:
|
|
442
|
+
return client.chat(
|
|
443
|
+
messages=messages,
|
|
444
|
+
temperature=resolve_role_temperature(self.cfg, role=role),
|
|
445
|
+
tools=[_COMPREHENSION_TOOL],
|
|
446
|
+
tool_choice=_COMPREHENSION_TOOL_CHOICE,
|
|
447
|
+
)
|
|
448
|
+
except LLMError as exc:
|
|
449
|
+
if not _structured_tool_call_unsupported(exc):
|
|
450
|
+
raise
|
|
451
|
+
LOGGER.warning(
|
|
452
|
+
"Structured tool-call comprehension was rejected by the provider; retrying with JSON mode."
|
|
453
|
+
)
|
|
454
|
+
return client.chat(
|
|
455
|
+
messages=messages,
|
|
456
|
+
temperature=resolve_role_temperature(self.cfg, role=role),
|
|
457
|
+
response_format={"type": "json_object"},
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def _api_key_for_profile(self, profile: ProfileSpec | None) -> str:
|
|
461
|
+
if profile is None:
|
|
462
|
+
return resolve_model_access_api_key(self.cfg, legacy_resolver=get_api_key)
|
|
463
|
+
if profile.auth_provider:
|
|
464
|
+
return ""
|
|
465
|
+
resolved = resolve_profile_api_key(self.cfg, profile.name)
|
|
466
|
+
if not resolved.key:
|
|
467
|
+
raise ConfigError(f"Missing API key for profile {profile.name}.")
|
|
468
|
+
return resolved.key
|
|
469
|
+
|
|
470
|
+
def _primary_model(self) -> tuple[str, str, ProfileSpec | None]:
|
|
471
|
+
role = str(self.cfg.assets.comprehension.role or ROLE_COMPREHENSION).strip().lower()
|
|
472
|
+
model = resolve_model_for_role(cfg=self.cfg, role=role, plan=None)
|
|
473
|
+
return role, model, None
|
|
474
|
+
|
|
475
|
+
def _vision_fallback_model(self) -> tuple[str, str, ProfileSpec] | None:
|
|
476
|
+
profile_name = str(self.cfg.assets.comprehension.vision_fallback_profile or "").strip()
|
|
477
|
+
if not profile_name:
|
|
478
|
+
return None
|
|
479
|
+
profile = get_profile(self.cfg, profile_name)
|
|
480
|
+
if profile is None or not profile.default_model:
|
|
481
|
+
return None
|
|
482
|
+
if not self.model_registry.get(profile.default_model).supports_vision:
|
|
483
|
+
return None
|
|
484
|
+
return ROLE_COMPREHENSION, profile.default_model, profile
|
|
485
|
+
|
|
486
|
+
def _cache_hit(
|
|
487
|
+
self,
|
|
488
|
+
*,
|
|
489
|
+
asset: AssetRecord,
|
|
490
|
+
model: str | None,
|
|
491
|
+
role: str | None,
|
|
492
|
+
started: float,
|
|
493
|
+
) -> ComprehensionRecord | None:
|
|
494
|
+
cached = self.index.find_existing_comprehension_by_sha256(
|
|
495
|
+
asset.sha256,
|
|
496
|
+
model=model,
|
|
497
|
+
role=role,
|
|
498
|
+
)
|
|
499
|
+
if cached is None:
|
|
500
|
+
return None
|
|
501
|
+
cloned = replace(
|
|
502
|
+
cached,
|
|
503
|
+
version=0,
|
|
504
|
+
asset_id=asset.id,
|
|
505
|
+
elapsed_ms=_elapsed_ms(started),
|
|
506
|
+
generated_at=now_iso(),
|
|
507
|
+
)
|
|
508
|
+
return self.index.write_comprehension(cloned)
|
|
509
|
+
|
|
510
|
+
def _try_ocr(self, asset: AssetRecord) -> OcrResult | None:
|
|
511
|
+
if (
|
|
512
|
+
not self._ocr_allowed()
|
|
513
|
+
or self.ocr_provider is None
|
|
514
|
+
or not self.ocr_provider.is_available()
|
|
515
|
+
):
|
|
516
|
+
return None
|
|
517
|
+
try:
|
|
518
|
+
return self.ocr_provider.extract_text(self.run_paths.root / asset.stored_path)
|
|
519
|
+
except Exception as exc:
|
|
520
|
+
if self.cfg.assets.comprehension.ocr_enabled == "always":
|
|
521
|
+
raise
|
|
522
|
+
LOGGER.warning("Asset OCR failed for %s: %s", asset.id, exc)
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
def _ocr_allowed(self) -> bool:
|
|
526
|
+
return self.cfg.assets.comprehension.ocr_enabled != "never"
|
|
527
|
+
|
|
528
|
+
def _load_extracted_text(self, asset: AssetRecord) -> str:
|
|
529
|
+
path = asset.extracted_text_path or asset.stored_path
|
|
530
|
+
return (self.run_paths.root / path).read_text(encoding="utf-8")
|
|
531
|
+
|
|
532
|
+
def _normalized_preview(self, asset: AssetRecord) -> Path:
|
|
533
|
+
try:
|
|
534
|
+
from PIL import Image
|
|
535
|
+
except ModuleNotFoundError as e:
|
|
536
|
+
raise AssetError("Pillow is required for image comprehension.") from e
|
|
537
|
+
source = self.run_paths.root / asset.stored_path
|
|
538
|
+
destination = asset_preview_path(self.run_paths, asset.id)
|
|
539
|
+
max_edge = int(self.cfg.assets.comprehension.image_max_edge_pixels)
|
|
540
|
+
with Image.open(source) as image:
|
|
541
|
+
normalized = image.convert("RGB")
|
|
542
|
+
normalized.thumbnail((max_edge, max_edge))
|
|
543
|
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
544
|
+
fd, temp_name = tempfile.mkstemp(
|
|
545
|
+
dir=destination.parent,
|
|
546
|
+
prefix=f".{destination.name}.",
|
|
547
|
+
suffix=".tmp",
|
|
548
|
+
)
|
|
549
|
+
temp_path = Path(temp_name)
|
|
550
|
+
os.close(fd)
|
|
551
|
+
try:
|
|
552
|
+
normalized.save(temp_path, format="PNG")
|
|
553
|
+
os.replace(temp_path, destination)
|
|
554
|
+
finally:
|
|
555
|
+
with suppress(FileNotFoundError):
|
|
556
|
+
temp_path.unlink()
|
|
557
|
+
return destination
|
|
558
|
+
|
|
559
|
+
def _minimal_record(self, *, asset: AssetRecord, started: float) -> ComprehensionRecord:
|
|
560
|
+
has_description = bool(asset.description.strip())
|
|
561
|
+
source = "user_description" if has_description else "minimal"
|
|
562
|
+
confidence_modifier = 0.5 if has_description else 0.3
|
|
563
|
+
summary_parts = [asset.title]
|
|
564
|
+
if has_description:
|
|
565
|
+
summary_parts.append(asset.description)
|
|
566
|
+
return ComprehensionRecord(
|
|
567
|
+
schema_version=1,
|
|
568
|
+
version=0,
|
|
569
|
+
asset_id=asset.id,
|
|
570
|
+
status="minimal",
|
|
571
|
+
source=source,
|
|
572
|
+
model=None,
|
|
573
|
+
role=None,
|
|
574
|
+
ocr_engine=None,
|
|
575
|
+
ocr_languages_used=[],
|
|
576
|
+
detected_language=None,
|
|
577
|
+
language_confidence=None,
|
|
578
|
+
confidence_modifier=confidence_modifier,
|
|
579
|
+
tokens_used={},
|
|
580
|
+
elapsed_ms=_elapsed_ms(started),
|
|
581
|
+
generated_at=now_iso(),
|
|
582
|
+
error=None,
|
|
583
|
+
data=ComprehensionData(
|
|
584
|
+
semantic_summary="\n".join(part for part in summary_parts if part.strip()),
|
|
585
|
+
classification={"kind": asset.kind, "subkind": "", "domain": ""},
|
|
586
|
+
confidence={"summary": confidence_modifier, "classification": confidence_modifier},
|
|
587
|
+
),
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
def _failed_record(
|
|
591
|
+
self,
|
|
592
|
+
*,
|
|
593
|
+
asset: AssetRecord,
|
|
594
|
+
started: float,
|
|
595
|
+
error: str,
|
|
596
|
+
) -> ComprehensionRecord:
|
|
597
|
+
return ComprehensionRecord(
|
|
598
|
+
schema_version=1,
|
|
599
|
+
version=0,
|
|
600
|
+
asset_id=asset.id,
|
|
601
|
+
status="failed",
|
|
602
|
+
source="minimal",
|
|
603
|
+
model=None,
|
|
604
|
+
role=None,
|
|
605
|
+
ocr_engine=None,
|
|
606
|
+
ocr_languages_used=[],
|
|
607
|
+
detected_language=None,
|
|
608
|
+
language_confidence=None,
|
|
609
|
+
confidence_modifier=0.0,
|
|
610
|
+
tokens_used={},
|
|
611
|
+
elapsed_ms=_elapsed_ms(started),
|
|
612
|
+
generated_at=now_iso(),
|
|
613
|
+
error=error.strip()[:_MAX_FAILURE_ERROR_CHARS] or "unknown error",
|
|
614
|
+
data=ComprehensionData(),
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _parse_comprehension_payload(response: LLMResponse) -> dict[str, Any]:
|
|
619
|
+
if response.tool_calls:
|
|
620
|
+
payload = None
|
|
621
|
+
for tool_call in response.tool_calls:
|
|
622
|
+
if tool_call.name == _COMPREHENSION_TOOL_NAME:
|
|
623
|
+
payload = tool_call.arguments
|
|
624
|
+
break
|
|
625
|
+
if payload is None:
|
|
626
|
+
raise AssetError(f"LLM did not call required tool: {_COMPREHENSION_TOOL_NAME}.")
|
|
627
|
+
else:
|
|
628
|
+
match = _JSON_OBJECT_RE.search(response.content.strip())
|
|
629
|
+
if not match:
|
|
630
|
+
raise AssetError("LLM did not return a JSON object.")
|
|
631
|
+
try:
|
|
632
|
+
payload = json.loads(match.group(0))
|
|
633
|
+
except json.JSONDecodeError as e:
|
|
634
|
+
raise AssetError("LLM returned malformed JSON.") from e
|
|
635
|
+
if not isinstance(payload, dict):
|
|
636
|
+
raise AssetError("LLM comprehension payload must be a JSON object.")
|
|
637
|
+
_require_exact_keys(payload, _TOP_LEVEL_KEYS, "LLM comprehension payload")
|
|
638
|
+
if not isinstance(payload.get("data"), dict):
|
|
639
|
+
raise AssetError("LLM comprehension payload must contain data object.")
|
|
640
|
+
_validate_comprehension_data(payload["data"])
|
|
641
|
+
detected_language = payload.get("detected_language")
|
|
642
|
+
if detected_language is not None and not isinstance(detected_language, str):
|
|
643
|
+
raise AssetError("LLM comprehension detected_language must be a string or null.")
|
|
644
|
+
language_confidence = payload.get("language_confidence")
|
|
645
|
+
if language_confidence is not None and not isinstance(language_confidence, int | float):
|
|
646
|
+
raise AssetError("LLM comprehension language_confidence must be numeric or null.")
|
|
647
|
+
return payload
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _has_inspection_angle(angle: str | None) -> bool:
|
|
651
|
+
return bool(str(angle or "").strip())
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def _structured_tool_call_unsupported(error: LLMError) -> bool:
|
|
655
|
+
text = str(error).casefold()
|
|
656
|
+
if "tool" not in text and "function" not in text:
|
|
657
|
+
return False
|
|
658
|
+
return any(token in text for token in ("unsupported", "unknown", "invalid", "not allowed"))
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _validate_comprehension_data(data: dict[str, Any]) -> None:
|
|
662
|
+
_require_exact_keys(data, _DATA_KEYS, "LLM comprehension data")
|
|
663
|
+
if not isinstance(data["semantic_summary"], str):
|
|
664
|
+
raise AssetError("LLM comprehension data.semantic_summary must be a string.")
|
|
665
|
+
if not isinstance(data["relations_hint"], str):
|
|
666
|
+
raise AssetError("LLM comprehension data.relations_hint must be a string.")
|
|
667
|
+
classification = data["classification"]
|
|
668
|
+
if not isinstance(classification, dict):
|
|
669
|
+
raise AssetError("LLM comprehension data.classification must be an object.")
|
|
670
|
+
_require_exact_keys(classification, _CLASSIFICATION_KEYS, "LLM comprehension classification")
|
|
671
|
+
if not all(isinstance(value, str) for value in classification.values()):
|
|
672
|
+
raise AssetError("LLM comprehension classification values must be strings.")
|
|
673
|
+
_validate_entity_list(data["key_entities"])
|
|
674
|
+
for field_name in (
|
|
675
|
+
"stated_facts",
|
|
676
|
+
"stated_decisions",
|
|
677
|
+
"stated_constraints",
|
|
678
|
+
"actionable_signals",
|
|
679
|
+
"open_questions",
|
|
680
|
+
):
|
|
681
|
+
_validate_string_list(data[field_name], f"LLM comprehension data.{field_name}")
|
|
682
|
+
confidence = data["confidence"]
|
|
683
|
+
if not isinstance(confidence, dict):
|
|
684
|
+
raise AssetError("LLM comprehension data.confidence must be an object.")
|
|
685
|
+
for key, value in confidence.items():
|
|
686
|
+
if not isinstance(key, str) or not isinstance(value, int | float):
|
|
687
|
+
raise AssetError("LLM comprehension confidence must map string keys to numeric values.")
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def _require_exact_keys(payload: dict[str, Any], expected: set[str], label: str) -> None:
|
|
691
|
+
keys = set(payload)
|
|
692
|
+
missing = sorted(expected - keys)
|
|
693
|
+
unknown = sorted(keys - expected)
|
|
694
|
+
if missing:
|
|
695
|
+
raise AssetError(f"{label} is missing keys: {missing}")
|
|
696
|
+
if unknown:
|
|
697
|
+
raise AssetError(f"{label} has unexpected keys: {unknown}")
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _validate_entity_list(value: Any) -> None:
|
|
701
|
+
if not isinstance(value, list):
|
|
702
|
+
raise AssetError("LLM comprehension data.key_entities must be an array.")
|
|
703
|
+
for item in value:
|
|
704
|
+
if not isinstance(item, dict):
|
|
705
|
+
raise AssetError("LLM comprehension key_entities items must be objects.")
|
|
706
|
+
if set(item) != {"type", "value"}:
|
|
707
|
+
raise AssetError("LLM comprehension key_entities items need type and value keys.")
|
|
708
|
+
if not all(isinstance(raw_value, str) for raw_value in item.values()):
|
|
709
|
+
raise AssetError("LLM comprehension key_entities values must be strings.")
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _validate_string_list(value: Any, label: str) -> None:
|
|
713
|
+
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
714
|
+
raise AssetError(f"{label} must be an array of strings.")
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def _tokens_used(response: LLMResponse) -> dict[str, int]:
|
|
718
|
+
usage = response.usage
|
|
719
|
+
if usage is None:
|
|
720
|
+
return {}
|
|
721
|
+
out: dict[str, int] = {}
|
|
722
|
+
if usage.prompt_tokens is not None:
|
|
723
|
+
out["input"] = int(usage.prompt_tokens)
|
|
724
|
+
if usage.completion_tokens is not None:
|
|
725
|
+
out["output"] = int(usage.completion_tokens)
|
|
726
|
+
return out
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _optional_string(value: Any) -> str | None:
|
|
730
|
+
if value is None:
|
|
731
|
+
return None
|
|
732
|
+
text = str(value).strip()
|
|
733
|
+
return text or None
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _optional_float(value: Any) -> float | None:
|
|
737
|
+
if value is None:
|
|
738
|
+
return None
|
|
739
|
+
try:
|
|
740
|
+
return float(value)
|
|
741
|
+
except (TypeError, ValueError):
|
|
742
|
+
return None
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
def _elapsed_ms(started: float) -> int:
|
|
746
|
+
return int((time.monotonic() - started) * 1000)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
def _image_content_parts(prompt: str, image_path: Path) -> list[dict[str, Any]]:
|
|
750
|
+
mime, _ = mimetypes.guess_type(image_path.name)
|
|
751
|
+
image_mime = mime if mime and mime.startswith("image/") else "image/png"
|
|
752
|
+
encoded = base64.b64encode(image_path.read_bytes()).decode("ascii")
|
|
753
|
+
return [
|
|
754
|
+
{
|
|
755
|
+
"type": "image_url",
|
|
756
|
+
"image_url": {"url": f"data:{image_mime};base64,{encoded}"},
|
|
757
|
+
},
|
|
758
|
+
{"type": "text", "text": prompt},
|
|
759
|
+
]
|