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,557 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urlsplit
|
|
8
|
+
|
|
9
|
+
import httpx
|
|
10
|
+
|
|
11
|
+
from ..llm.provider_limits import (
|
|
12
|
+
ProviderRetrySettings,
|
|
13
|
+
best_effort_provider_key,
|
|
14
|
+
run_provider_limited_call,
|
|
15
|
+
)
|
|
16
|
+
from ..safety import SafeHttpError, safe_http_request
|
|
17
|
+
from ..safety.safe_http import Resolver
|
|
18
|
+
from ..web_research import extract_public_web_urls, normalize_web_url
|
|
19
|
+
from .http_timeout import build_http_timeout_budget, format_http_timeout_error
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DashScopeChatSearchError(RuntimeError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_DASHSCOPE_RESPONSES_SEARCH_MODEL_PREFIXES = (
|
|
27
|
+
"qwen3.7-plus",
|
|
28
|
+
"qwen3.7-max",
|
|
29
|
+
"qwen3.6-plus",
|
|
30
|
+
"qwen3.6-flash",
|
|
31
|
+
"qwen3.6-max",
|
|
32
|
+
)
|
|
33
|
+
_DASHSCOPE_CHAT_SEARCH_MODEL_PREFIXES = (
|
|
34
|
+
"qwen3.5-plus",
|
|
35
|
+
"qwen3.5-flash",
|
|
36
|
+
"qwen3-max",
|
|
37
|
+
"qwen-plus",
|
|
38
|
+
"qwen-flash",
|
|
39
|
+
"qwen-max",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _normalized_dashscope_model(model: str | None) -> str:
|
|
44
|
+
normalized = str(model or "").strip().lower()
|
|
45
|
+
if "/" in normalized:
|
|
46
|
+
normalized = normalized.rsplit("/", 1)[-1]
|
|
47
|
+
return normalized
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def dashscope_model_uses_responses_search(model: str | None) -> bool:
|
|
51
|
+
normalized = _normalized_dashscope_model(model)
|
|
52
|
+
return any(
|
|
53
|
+
normalized.startswith(prefix) for prefix in _DASHSCOPE_RESPONSES_SEARCH_MODEL_PREFIXES
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def dashscope_model_supports_web_search(model: str | None) -> bool:
|
|
58
|
+
normalized = _normalized_dashscope_model(model)
|
|
59
|
+
return dashscope_model_uses_responses_search(normalized) or any(
|
|
60
|
+
normalized.startswith(prefix) for prefix in _DASHSCOPE_CHAT_SEARCH_MODEL_PREFIXES
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _extract_error_message(data: Any) -> str | None:
|
|
65
|
+
if not isinstance(data, dict):
|
|
66
|
+
return None
|
|
67
|
+
error_obj = data.get("error")
|
|
68
|
+
if isinstance(error_obj, dict):
|
|
69
|
+
message = str(error_obj.get("message") or "").strip()
|
|
70
|
+
if message:
|
|
71
|
+
code = str(error_obj.get("code") or "").strip()
|
|
72
|
+
return f"{code}: {message}" if code else message
|
|
73
|
+
message = str(data.get("message") or "").strip()
|
|
74
|
+
if message:
|
|
75
|
+
return message
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _dashscope_error_from_response(
|
|
80
|
+
response: httpx.Response,
|
|
81
|
+
*,
|
|
82
|
+
operation_label: str = "DashScope chat search",
|
|
83
|
+
) -> DashScopeChatSearchError:
|
|
84
|
+
try:
|
|
85
|
+
data = response.json()
|
|
86
|
+
except Exception:
|
|
87
|
+
body = response.text
|
|
88
|
+
if len(body) > 1000:
|
|
89
|
+
body = body[:1000] + "...(truncated)"
|
|
90
|
+
return DashScopeChatSearchError(f"{operation_label} error {response.status_code}: {body}")
|
|
91
|
+
if isinstance(data, dict):
|
|
92
|
+
error_message = _extract_error_message(data)
|
|
93
|
+
if error_message:
|
|
94
|
+
return DashScopeChatSearchError(
|
|
95
|
+
f"{operation_label} error {response.status_code}: {error_message}"
|
|
96
|
+
)
|
|
97
|
+
return DashScopeChatSearchError(f"{operation_label} error {response.status_code}: {data!r}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _choice_message_text(data: dict[str, Any]) -> str:
|
|
101
|
+
choices = data.get("choices")
|
|
102
|
+
if not isinstance(choices, list):
|
|
103
|
+
return ""
|
|
104
|
+
for choice in choices:
|
|
105
|
+
if not isinstance(choice, dict):
|
|
106
|
+
continue
|
|
107
|
+
message = choice.get("message")
|
|
108
|
+
if not isinstance(message, dict):
|
|
109
|
+
continue
|
|
110
|
+
content = message.get("content")
|
|
111
|
+
if isinstance(content, str):
|
|
112
|
+
return content.strip()
|
|
113
|
+
return ""
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _responses_message_text(data: dict[str, Any]) -> str:
|
|
117
|
+
output_text = data.get("output_text")
|
|
118
|
+
if isinstance(output_text, str) and output_text.strip():
|
|
119
|
+
return output_text.strip()
|
|
120
|
+
parts: list[str] = []
|
|
121
|
+
output = data.get("output")
|
|
122
|
+
for item in output if isinstance(output, list) else []:
|
|
123
|
+
if not isinstance(item, dict) or str(item.get("type") or "") != "message":
|
|
124
|
+
continue
|
|
125
|
+
content = item.get("content")
|
|
126
|
+
for chunk in content if isinstance(content, list) else []:
|
|
127
|
+
if not isinstance(chunk, dict):
|
|
128
|
+
continue
|
|
129
|
+
text = chunk.get("text")
|
|
130
|
+
if isinstance(text, str):
|
|
131
|
+
parts.append(text)
|
|
132
|
+
return "".join(parts).strip()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _responses_sources(data: dict[str, Any]) -> list[dict[str, str]]:
|
|
136
|
+
sources: list[dict[str, str]] = []
|
|
137
|
+
|
|
138
|
+
def _walk(value: Any) -> None:
|
|
139
|
+
if isinstance(value, dict):
|
|
140
|
+
source = _coerce_source(value)
|
|
141
|
+
if source is not None:
|
|
142
|
+
sources.append(source)
|
|
143
|
+
for nested in value.values():
|
|
144
|
+
if isinstance(nested, (dict, list)):
|
|
145
|
+
_walk(nested)
|
|
146
|
+
elif isinstance(value, list):
|
|
147
|
+
for item in value:
|
|
148
|
+
_walk(item)
|
|
149
|
+
|
|
150
|
+
_walk(data.get("output"))
|
|
151
|
+
return sources
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _responses_queries(data: dict[str, Any]) -> list[str]:
|
|
155
|
+
queries: list[str] = []
|
|
156
|
+
|
|
157
|
+
def _append(raw: Any) -> None:
|
|
158
|
+
value = str(raw or "").strip()
|
|
159
|
+
if value and value not in queries:
|
|
160
|
+
queries.append(value)
|
|
161
|
+
|
|
162
|
+
def _walk(value: Any) -> None:
|
|
163
|
+
if isinstance(value, dict):
|
|
164
|
+
_append(value.get("query"))
|
|
165
|
+
raw_queries = value.get("queries")
|
|
166
|
+
if isinstance(raw_queries, list):
|
|
167
|
+
for query in raw_queries:
|
|
168
|
+
_append(query)
|
|
169
|
+
for nested in value.values():
|
|
170
|
+
if isinstance(nested, (dict, list)):
|
|
171
|
+
_walk(nested)
|
|
172
|
+
elif isinstance(value, list):
|
|
173
|
+
for item in value:
|
|
174
|
+
_walk(item)
|
|
175
|
+
|
|
176
|
+
_walk(data.get("output"))
|
|
177
|
+
return queries
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _sse_json_events(text: str) -> list[dict[str, Any]]:
|
|
181
|
+
events: list[dict[str, Any]] = []
|
|
182
|
+
for raw_line in text.splitlines():
|
|
183
|
+
line = raw_line.strip()
|
|
184
|
+
if not line.startswith("data:"):
|
|
185
|
+
continue
|
|
186
|
+
payload = line.removeprefix("data:").strip()
|
|
187
|
+
if not payload or payload == "[DONE]":
|
|
188
|
+
continue
|
|
189
|
+
try:
|
|
190
|
+
parsed = json.loads(payload)
|
|
191
|
+
except json.JSONDecodeError:
|
|
192
|
+
continue
|
|
193
|
+
if isinstance(parsed, dict):
|
|
194
|
+
events.append(parsed)
|
|
195
|
+
return events
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _coalesce_streaming_response(text: str) -> dict[str, Any] | None:
|
|
199
|
+
events = _sse_json_events(text)
|
|
200
|
+
if not events:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
content_parts: list[str] = []
|
|
204
|
+
response_id: str | None = None
|
|
205
|
+
response_model: str | None = None
|
|
206
|
+
sources: list[dict[str, str]] = []
|
|
207
|
+
for event in events:
|
|
208
|
+
if response_id is None:
|
|
209
|
+
response_id = str(event.get("id") or "").strip() or None
|
|
210
|
+
if response_model is None:
|
|
211
|
+
response_model = str(event.get("model") or "").strip() or None
|
|
212
|
+
sources.extend(_sources_from_json_payload(event))
|
|
213
|
+
|
|
214
|
+
choices = event.get("choices")
|
|
215
|
+
if not isinstance(choices, list):
|
|
216
|
+
continue
|
|
217
|
+
for choice in choices:
|
|
218
|
+
if not isinstance(choice, dict):
|
|
219
|
+
continue
|
|
220
|
+
delta = choice.get("delta")
|
|
221
|
+
if not isinstance(delta, dict):
|
|
222
|
+
delta = choice.get("message")
|
|
223
|
+
if not isinstance(delta, dict):
|
|
224
|
+
continue
|
|
225
|
+
content = delta.get("content")
|
|
226
|
+
if isinstance(content, str):
|
|
227
|
+
content_parts.append(content)
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
"id": response_id,
|
|
231
|
+
"model": response_model,
|
|
232
|
+
"choices": [{"message": {"content": "".join(content_parts)}}],
|
|
233
|
+
"search_info": {"search_results": sources},
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _coerce_source(raw: Any) -> dict[str, str] | None:
|
|
238
|
+
if not isinstance(raw, dict):
|
|
239
|
+
return None
|
|
240
|
+
url = str(raw.get("url") or raw.get("link") or "").strip()
|
|
241
|
+
if not url:
|
|
242
|
+
return None
|
|
243
|
+
title = str(raw.get("title") or raw.get("name") or "").strip()
|
|
244
|
+
snippet = str(raw.get("snippet") or raw.get("content") or raw.get("summary") or "").strip()
|
|
245
|
+
source = {"url": url, "title": title}
|
|
246
|
+
normalized_url = normalize_web_url(url)
|
|
247
|
+
if normalized_url:
|
|
248
|
+
source["normalized_url"] = normalized_url
|
|
249
|
+
if snippet:
|
|
250
|
+
source["snippet"] = snippet[:500]
|
|
251
|
+
return source
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _sources_from_json_payload(value: Any) -> list[dict[str, str]]:
|
|
255
|
+
sources: list[dict[str, str]] = []
|
|
256
|
+
if isinstance(value, dict):
|
|
257
|
+
for key in ("sources", "citations", "search_results", "results"):
|
|
258
|
+
raw_sources = value.get(key)
|
|
259
|
+
if isinstance(raw_sources, list):
|
|
260
|
+
for raw_source in raw_sources:
|
|
261
|
+
source = _coerce_source(raw_source)
|
|
262
|
+
if source is not None:
|
|
263
|
+
sources.append(source)
|
|
264
|
+
search_info = value.get("search_info")
|
|
265
|
+
if isinstance(search_info, dict):
|
|
266
|
+
sources.extend(_sources_from_json_payload(search_info))
|
|
267
|
+
return sources
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _strip_json_code_fence(text: str) -> str:
|
|
271
|
+
stripped = text.strip()
|
|
272
|
+
if not stripped.startswith("```"):
|
|
273
|
+
return stripped
|
|
274
|
+
lines = stripped.splitlines()
|
|
275
|
+
if len(lines) < 2 or not lines[-1].strip().startswith("```"):
|
|
276
|
+
return stripped
|
|
277
|
+
first_line = lines[0].strip().lower()
|
|
278
|
+
if first_line not in {"```", "```json", "```javascript", "```js"}:
|
|
279
|
+
return stripped
|
|
280
|
+
return "\n".join(lines[1:-1]).strip()
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _json_answer_and_sources(text: str) -> tuple[str, list[dict[str, str]]]:
|
|
284
|
+
stripped = _strip_json_code_fence(text)
|
|
285
|
+
if not stripped:
|
|
286
|
+
return "", []
|
|
287
|
+
try:
|
|
288
|
+
parsed = json.loads(stripped)
|
|
289
|
+
except json.JSONDecodeError:
|
|
290
|
+
return stripped, []
|
|
291
|
+
if not isinstance(parsed, dict):
|
|
292
|
+
return stripped, []
|
|
293
|
+
answer = str(
|
|
294
|
+
parsed.get("answer")
|
|
295
|
+
or parsed.get("summary")
|
|
296
|
+
or parsed.get("content")
|
|
297
|
+
or parsed.get("text")
|
|
298
|
+
or ""
|
|
299
|
+
).strip()
|
|
300
|
+
sources = _sources_from_json_payload(parsed)
|
|
301
|
+
return answer or stripped, sources
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _sources_from_urls(text: str) -> list[dict[str, str]]:
|
|
305
|
+
return [
|
|
306
|
+
{
|
|
307
|
+
"url": entry["url"],
|
|
308
|
+
"normalized_url": entry["normalized_url"],
|
|
309
|
+
"title": "",
|
|
310
|
+
}
|
|
311
|
+
for entry in extract_public_web_urls(text)
|
|
312
|
+
]
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _effective_source_url(source: dict[str, str]) -> str | None:
|
|
316
|
+
normalized_url = normalize_web_url(source.get("normalized_url"))
|
|
317
|
+
if normalized_url:
|
|
318
|
+
return normalized_url
|
|
319
|
+
return normalize_web_url(source.get("url"))
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _host_matches_allowed_domains(url: str, allowed_domains: list[str] | None) -> bool:
|
|
323
|
+
if not allowed_domains:
|
|
324
|
+
return True
|
|
325
|
+
try:
|
|
326
|
+
host = (urlsplit(url).hostname or "").rstrip(".").lower()
|
|
327
|
+
except ValueError:
|
|
328
|
+
return False
|
|
329
|
+
for domain in allowed_domains:
|
|
330
|
+
normalized = str(domain or "").strip().rstrip(".").lower()
|
|
331
|
+
if host == normalized or host.endswith(f".{normalized}"):
|
|
332
|
+
return True
|
|
333
|
+
return False
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _dedupe_sources(
|
|
337
|
+
sources: list[dict[str, str]],
|
|
338
|
+
*,
|
|
339
|
+
allowed_domains: list[str] | None,
|
|
340
|
+
) -> list[dict[str, str]]:
|
|
341
|
+
deduped: list[dict[str, str]] = []
|
|
342
|
+
index_by_url: dict[str, int] = {}
|
|
343
|
+
for source in sources:
|
|
344
|
+
raw_url = str(source.get("url") or "").strip()
|
|
345
|
+
if not raw_url:
|
|
346
|
+
continue
|
|
347
|
+
effective_url = _effective_source_url(source)
|
|
348
|
+
dedupe_key = effective_url or raw_url
|
|
349
|
+
if dedupe_key in index_by_url:
|
|
350
|
+
existing = deduped[index_by_url[dedupe_key]]
|
|
351
|
+
if not existing.get("title") and source.get("title"):
|
|
352
|
+
existing["title"] = str(source["title"]).strip()
|
|
353
|
+
if not existing.get("snippet") and source.get("snippet"):
|
|
354
|
+
existing["snippet"] = str(source["snippet"]).strip()
|
|
355
|
+
continue
|
|
356
|
+
if not _host_matches_allowed_domains(effective_url or raw_url, allowed_domains):
|
|
357
|
+
continue
|
|
358
|
+
index_by_url[dedupe_key] = len(deduped)
|
|
359
|
+
cleaned = {
|
|
360
|
+
"url": effective_url or raw_url,
|
|
361
|
+
"title": str(source.get("title") or "").strip(),
|
|
362
|
+
}
|
|
363
|
+
snippet = str(source.get("snippet") or "").strip()
|
|
364
|
+
if snippet:
|
|
365
|
+
cleaned["snippet"] = snippet
|
|
366
|
+
deduped.append(cleaned)
|
|
367
|
+
return deduped
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _search_prompt(query: str, allowed_domains: list[str] | None) -> str:
|
|
371
|
+
domain_text = ""
|
|
372
|
+
if allowed_domains:
|
|
373
|
+
domain_text = (
|
|
374
|
+
"\nOnly use results from these domains: "
|
|
375
|
+
+ ", ".join(str(domain).strip() for domain in allowed_domains)
|
|
376
|
+
+ "."
|
|
377
|
+
)
|
|
378
|
+
return (
|
|
379
|
+
"Use live web search to answer the query. Return only JSON with this shape: "
|
|
380
|
+
'{"answer":"...", "sources":[{"title":"...", "url":"..."}]}. '
|
|
381
|
+
"Include source URLs whenever the search backend provides them."
|
|
382
|
+
f"{domain_text}\nQuery: {query}"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def dashscope_chat_search(
|
|
387
|
+
*,
|
|
388
|
+
query: str,
|
|
389
|
+
base_url: str,
|
|
390
|
+
api_key: str,
|
|
391
|
+
model: str,
|
|
392
|
+
max_results: int = 8,
|
|
393
|
+
include_domains: list[str] | None = None,
|
|
394
|
+
timeout_s: float = 45.0,
|
|
395
|
+
transport: httpx.BaseTransport | None = None,
|
|
396
|
+
resolver: Resolver | None = None,
|
|
397
|
+
provider_key: str | None = None,
|
|
398
|
+
provider_concurrency_caps: dict[str, int] | None = None,
|
|
399
|
+
provider_retry_settings: ProviderRetrySettings | None = None,
|
|
400
|
+
provider_sleep_fn: Callable[[float], None] | None = None,
|
|
401
|
+
provider_random_fn: Callable[[], float] | None = None,
|
|
402
|
+
) -> dict[str, Any]:
|
|
403
|
+
if not str(query or "").strip():
|
|
404
|
+
raise DashScopeChatSearchError("query must be a non-empty string.")
|
|
405
|
+
if not str(api_key or "").strip():
|
|
406
|
+
raise DashScopeChatSearchError("API key is required for DashScope chat search.")
|
|
407
|
+
if not str(base_url or "").strip():
|
|
408
|
+
raise DashScopeChatSearchError("base_url is required for DashScope chat search.")
|
|
409
|
+
if not str(model or "").strip():
|
|
410
|
+
raise DashScopeChatSearchError("model is required for DashScope chat search.")
|
|
411
|
+
|
|
412
|
+
use_responses = dashscope_model_uses_responses_search(model)
|
|
413
|
+
operation_label = "DashScope Responses search" if use_responses else "DashScope chat search"
|
|
414
|
+
if use_responses:
|
|
415
|
+
url = f"{base_url.rstrip('/')}/responses"
|
|
416
|
+
prompt = f"Use live web search and cite source URLs.\n\n{query}"
|
|
417
|
+
if include_domains:
|
|
418
|
+
prompt += "\nOnly use sources from these domains: " + ", ".join(include_domains) + "."
|
|
419
|
+
payload: dict[str, Any] = {
|
|
420
|
+
"model": model,
|
|
421
|
+
"input": prompt,
|
|
422
|
+
"tools": [{"type": "web_search"}],
|
|
423
|
+
"stream": False,
|
|
424
|
+
}
|
|
425
|
+
else:
|
|
426
|
+
url = f"{base_url.rstrip('/')}/chat/completions"
|
|
427
|
+
payload = {
|
|
428
|
+
"model": model,
|
|
429
|
+
"messages": [{"role": "user", "content": _search_prompt(query, include_domains)}],
|
|
430
|
+
"enable_search": True,
|
|
431
|
+
"search_options": {
|
|
432
|
+
"forced_search": True,
|
|
433
|
+
"search_strategy": "agent",
|
|
434
|
+
"enable_source": True,
|
|
435
|
+
},
|
|
436
|
+
"enable_thinking": False,
|
|
437
|
+
"stream": True,
|
|
438
|
+
}
|
|
439
|
+
headers = {
|
|
440
|
+
"Authorization": f"Bearer {api_key}",
|
|
441
|
+
"Content-Type": "application/json",
|
|
442
|
+
"User-Agent": "alysis-code/0.1.0",
|
|
443
|
+
}
|
|
444
|
+
timeout_budget = build_http_timeout_budget(timeout_s, profile="search")
|
|
445
|
+
|
|
446
|
+
resolved_provider_key = provider_key or best_effort_provider_key(
|
|
447
|
+
base_url=base_url,
|
|
448
|
+
model=model,
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
def _send_request() -> httpx.Response:
|
|
452
|
+
try:
|
|
453
|
+
response = asyncio.run(
|
|
454
|
+
safe_http_request(
|
|
455
|
+
"POST",
|
|
456
|
+
url,
|
|
457
|
+
timeout=timeout_s,
|
|
458
|
+
headers=headers,
|
|
459
|
+
json=payload,
|
|
460
|
+
_transport=transport, # type: ignore[arg-type]
|
|
461
|
+
_resolver=resolver,
|
|
462
|
+
)
|
|
463
|
+
)
|
|
464
|
+
except httpx.TimeoutException as e:
|
|
465
|
+
raise DashScopeChatSearchError(
|
|
466
|
+
format_http_timeout_error(
|
|
467
|
+
operation=operation_label,
|
|
468
|
+
budget=timeout_budget,
|
|
469
|
+
error=e,
|
|
470
|
+
)
|
|
471
|
+
) from e
|
|
472
|
+
except SafeHttpError as e:
|
|
473
|
+
raise DashScopeChatSearchError(f"{operation_label} request blocked: {e}") from e
|
|
474
|
+
except Exception as e: # noqa: BLE001
|
|
475
|
+
raise DashScopeChatSearchError(f"{operation_label} request failed: {e}") from e
|
|
476
|
+
if response.status_code >= 400:
|
|
477
|
+
raise _dashscope_error_from_response(response, operation_label=operation_label)
|
|
478
|
+
return response
|
|
479
|
+
|
|
480
|
+
response = run_provider_limited_call(
|
|
481
|
+
call=_send_request,
|
|
482
|
+
provider_key=resolved_provider_key,
|
|
483
|
+
provider_concurrency_caps=provider_concurrency_caps,
|
|
484
|
+
retry_settings=provider_retry_settings,
|
|
485
|
+
operation=("dashscope_responses_search" if use_responses else "dashscope_chat_search"),
|
|
486
|
+
sleep_fn=provider_sleep_fn,
|
|
487
|
+
random_fn=provider_random_fn,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
try:
|
|
491
|
+
data = response.json()
|
|
492
|
+
except Exception as e: # noqa: BLE001
|
|
493
|
+
stream_data = _coalesce_streaming_response(response.text)
|
|
494
|
+
if stream_data is None:
|
|
495
|
+
raise DashScopeChatSearchError(
|
|
496
|
+
"DashScope chat search returned non-JSON response"
|
|
497
|
+
) from e
|
|
498
|
+
data = stream_data
|
|
499
|
+
|
|
500
|
+
if not isinstance(data, dict):
|
|
501
|
+
raise DashScopeChatSearchError("Unexpected DashScope chat search payload")
|
|
502
|
+
|
|
503
|
+
if response.status_code >= 400:
|
|
504
|
+
error_message = _extract_error_message(data)
|
|
505
|
+
if error_message:
|
|
506
|
+
raise DashScopeChatSearchError(
|
|
507
|
+
f"DashScope chat search error {response.status_code}: {error_message}"
|
|
508
|
+
)
|
|
509
|
+
raise DashScopeChatSearchError(
|
|
510
|
+
f"DashScope chat search error {response.status_code}: {data!r}"
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
raw_answer = _responses_message_text(data) if use_responses else _choice_message_text(data)
|
|
514
|
+
if use_responses:
|
|
515
|
+
answer = raw_answer
|
|
516
|
+
json_sources: list[dict[str, str]] = []
|
|
517
|
+
payload_sources = _responses_sources(data)
|
|
518
|
+
queries = _responses_queries(data) or [query]
|
|
519
|
+
else:
|
|
520
|
+
answer, json_sources = _json_answer_and_sources(raw_answer)
|
|
521
|
+
payload_sources = _sources_from_json_payload(data)
|
|
522
|
+
queries = [query]
|
|
523
|
+
url_sources = _sources_from_urls(answer)
|
|
524
|
+
deduped_sources = _dedupe_sources(
|
|
525
|
+
[*json_sources, *payload_sources, *url_sources],
|
|
526
|
+
allowed_domains=include_domains,
|
|
527
|
+
)
|
|
528
|
+
sources_truncated = len(deduped_sources) > max_results
|
|
529
|
+
final_sources = deduped_sources[:max_results]
|
|
530
|
+
citations = [
|
|
531
|
+
{
|
|
532
|
+
"title": str(source.get("title") or "").strip(),
|
|
533
|
+
"url": str(source.get("url") or "").strip(),
|
|
534
|
+
"start_index": None,
|
|
535
|
+
"end_index": None,
|
|
536
|
+
}
|
|
537
|
+
for source in final_sources
|
|
538
|
+
]
|
|
539
|
+
|
|
540
|
+
if not answer and not final_sources:
|
|
541
|
+
raise DashScopeChatSearchError("DashScope chat search payload missing answer and sources")
|
|
542
|
+
|
|
543
|
+
response_id = str(data.get("id") or "").strip() or None
|
|
544
|
+
response_model = str(data.get("model") or "").strip() or None
|
|
545
|
+
return {
|
|
546
|
+
"query": query,
|
|
547
|
+
"answer": answer,
|
|
548
|
+
"citations": citations,
|
|
549
|
+
"sources": final_sources,
|
|
550
|
+
"queries": queries,
|
|
551
|
+
"model": response_model or model,
|
|
552
|
+
"backend": "dashscope_responses" if use_responses else "dashscope_chat",
|
|
553
|
+
"allowed_domains": include_domains or [],
|
|
554
|
+
"external_web_access": True,
|
|
555
|
+
"response_id": response_id,
|
|
556
|
+
"sources_truncated": sources_truncated,
|
|
557
|
+
}
|