novacode-cli 1.2.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.
- novacode_cli/__init__.py +29 -0
- novacode_cli/__main__.py +6 -0
- novacode_cli/_lazy_heavy.py +99 -0
- novacode_cli/_version.py +3 -0
- novacode_cli/agent_stream.py +178 -0
- novacode_cli/agents/agent_file.py +250 -0
- novacode_cli/agents/agent_mailbox.py +120 -0
- novacode_cli/agents/approval_decisions.py +146 -0
- novacode_cli/agents/arg_repair.py +196 -0
- novacode_cli/agents/async_agents/__init__.py +6 -0
- novacode_cli/agents/async_agents/_config.py +213 -0
- novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
- novacode_cli/agents/async_agents/_specialist.py +84 -0
- novacode_cli/agents/async_agents/_test_commands.py +89 -0
- novacode_cli/agents/async_agents/_tools.py +432 -0
- novacode_cli/agents/async_agents/general_purpose.py +42 -0
- novacode_cli/agents/async_agents/langgraph.json +9 -0
- novacode_cli/agents/async_agents/research.py +22 -0
- novacode_cli/agents/async_context.py +303 -0
- novacode_cli/agents/async_on_demand.py +52 -0
- novacode_cli/agents/async_task_errors.py +75 -0
- novacode_cli/agents/async_workspace.py +27 -0
- novacode_cli/agents/attribution.py +165 -0
- novacode_cli/agents/browser_mode.py +119 -0
- novacode_cli/agents/btw_agent.py +71 -0
- novacode_cli/agents/caveman.py +352 -0
- novacode_cli/agents/caveman_support.py +370 -0
- novacode_cli/agents/code_mode.py +774 -0
- novacode_cli/agents/code_runtime.py +160 -0
- novacode_cli/agents/context_compaction.py +1339 -0
- novacode_cli/agents/core_agent.py +2710 -0
- novacode_cli/agents/default_subagents/__init__.py +24 -0
- novacode_cli/agents/default_subagents/async_subagents.py +296 -0
- novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
- novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
- novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
- novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
- novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
- novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
- novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
- novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
- novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
- novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
- novacode_cli/agents/default_subagents/prompt.py +88 -0
- novacode_cli/agents/default_subagents/subagents.py +138 -0
- novacode_cli/agents/dream_agent.py +93 -0
- novacode_cli/agents/edit_diagnostics.py +362 -0
- novacode_cli/agents/edit_fallback.py +62 -0
- novacode_cli/agents/handoff.py +166 -0
- novacode_cli/agents/invocation.py +152 -0
- novacode_cli/agents/model_budget.py +42 -0
- novacode_cli/agents/model_retry.py +113 -0
- novacode_cli/agents/model_router.py +420 -0
- novacode_cli/agents/openai_decisions.py +186 -0
- novacode_cli/agents/plan_agent/__init__.py +12 -0
- novacode_cli/agents/plan_agent/plan_agent.py +357 -0
- novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
- novacode_cli/agents/plan_execution.py +237 -0
- novacode_cli/agents/ponytail.py +66 -0
- novacode_cli/agents/prime.py +274 -0
- novacode_cli/agents/provider_caching.py +99 -0
- novacode_cli/agents/server_launcher.py +904 -0
- novacode_cli/agents/strict_tools.py +384 -0
- novacode_cli/agents/task_discipline.py +294 -0
- novacode_cli/agents/tool_call_repair.py +290 -0
- novacode_cli/agents/tool_offload.py +291 -0
- novacode_cli/agents/tool_search.py +806 -0
- novacode_cli/agents/tool_search_variants.py +106 -0
- novacode_cli/agents/tool_verdicts.py +1194 -0
- novacode_cli/agents/user_async_agents.py +312 -0
- novacode_cli/artifacts/__init__.py +10 -0
- novacode_cli/artifacts/registry.py +278 -0
- novacode_cli/artifacts/server.py +426 -0
- novacode_cli/assets/Nova.png +0 -0
- novacode_cli/assets/nova-avatar.png +0 -0
- novacode_cli/audio/__init__.py +301 -0
- novacode_cli/audio/capture.py +137 -0
- novacode_cli/audio/downloads.py +273 -0
- novacode_cli/audio/pipeline.py +391 -0
- novacode_cli/audio/playback.py +87 -0
- novacode_cli/audio/providers.py +208 -0
- novacode_cli/audio/speakable.py +78 -0
- novacode_cli/audio/stt.py +132 -0
- novacode_cli/audio/stt_deepgram.py +69 -0
- novacode_cli/audio/stt_elevenlabs.py +89 -0
- novacode_cli/audio/stt_parakeet.py +94 -0
- novacode_cli/audio/stt_whistle.py +61 -0
- novacode_cli/audio/summarize.py +92 -0
- novacode_cli/audio/tts.py +85 -0
- novacode_cli/audio/tts_kokoro.py +73 -0
- novacode_cli/audio/tts_orpheus.py +180 -0
- novacode_cli/audio/tts_pocket.py +131 -0
- novacode_cli/audio/tts_sano.py +69 -0
- novacode_cli/audio/vad.py +145 -0
- novacode_cli/backends/__init__.py +17 -0
- novacode_cli/backends/filesystem.py +894 -0
- novacode_cli/backends/native_search.py +92 -0
- novacode_cli/bootstrap/__init__.py +9 -0
- novacode_cli/bootstrap/env_snapshot.py +240 -0
- novacode_cli/bootstrap/steering.py +464 -0
- novacode_cli/bootstrap/vision_router.py +604 -0
- novacode_cli/brand.py +362 -0
- novacode_cli/cli_args.py +718 -0
- novacode_cli/commands/__init__.py +237 -0
- novacode_cli/commands/agents_commands.py +231 -0
- novacode_cli/commands/auth_handler.py +40 -0
- novacode_cli/commands/browser_use_handler.py +75 -0
- novacode_cli/commands/cache_handler.py +32 -0
- novacode_cli/commands/chat_handler.py +1357 -0
- novacode_cli/commands/ci_fix.py +171 -0
- novacode_cli/commands/claude_plan.py +331 -0
- novacode_cli/commands/codex_plan.py +195 -0
- novacode_cli/commands/commands.py +1113 -0
- novacode_cli/commands/create_handler.py +124 -0
- novacode_cli/commands/create_server.py +1887 -0
- novacode_cli/commands/cron_handler.py +119 -0
- novacode_cli/commands/dream_handler.py +203 -0
- novacode_cli/commands/effort_handler.py +65 -0
- novacode_cli/commands/evolution_handler.py +214 -0
- novacode_cli/commands/file_commands.py +381 -0
- novacode_cli/commands/handoff_handler.py +68 -0
- novacode_cli/commands/hooks_handler.py +269 -0
- novacode_cli/commands/init_handler.py +146 -0
- novacode_cli/commands/learning_handler.py +160 -0
- novacode_cli/commands/lite_mode_handler.py +76 -0
- novacode_cli/commands/log_commands.py +363 -0
- novacode_cli/commands/mcp_handler.py +29 -0
- novacode_cli/commands/model_handler.py +29 -0
- novacode_cli/commands/notifications_handler.py +158 -0
- novacode_cli/commands/onboard_handler.py +121 -0
- novacode_cli/commands/plan_handler.py +440 -0
- novacode_cli/commands/planner_usage.py +250 -0
- novacode_cli/commands/plugin_install_handler.py +38 -0
- novacode_cli/commands/plugins_handler.py +35 -0
- novacode_cli/commands/prime_handler.py +455 -0
- novacode_cli/commands/prompt_handler.py +149 -0
- novacode_cli/commands/provider_usage.py +160 -0
- novacode_cli/commands/ralph_events.py +106 -0
- novacode_cli/commands/ralph_handler.py +1293 -0
- novacode_cli/commands/refine_handler.py +213 -0
- novacode_cli/commands/research_handler.py +202 -0
- novacode_cli/commands/rootapp_remote.py +83 -0
- novacode_cli/commands/router_handler.py +36 -0
- novacode_cli/commands/server_commands.py +249 -0
- novacode_cli/commands/session_commands.py +203 -0
- novacode_cli/commands/share_handler.py +450 -0
- novacode_cli/commands/side_commands.py +366 -0
- novacode_cli/commands/skill_invoke.py +420 -0
- novacode_cli/commands/skills_commands.py +48 -0
- novacode_cli/commands/trace_handler.py +322 -0
- novacode_cli/commands/trello_handler.py +226 -0
- novacode_cli/commands/trello_remote.py +177 -0
- novacode_cli/commands/trello_server.py +1214 -0
- novacode_cli/commands/vision_handler.py +127 -0
- novacode_cli/commands/voice_handler.py +447 -0
- novacode_cli/commands/webhook_handler.py +134 -0
- novacode_cli/commands/wiki_commands.py +227 -0
- novacode_cli/compaction.py +753 -0
- novacode_cli/computation_cache.py +159 -0
- novacode_cli/computer/__init__.py +1 -0
- novacode_cli/computer/persistent.py +234 -0
- novacode_cli/computer/preview.py +253 -0
- novacode_cli/computer/runtime.py +474 -0
- novacode_cli/computer/windows_console.py +152 -0
- novacode_cli/computer/windows_qemu.py +211 -0
- novacode_cli/config/browser_mode.py +159 -0
- novacode_cli/config/catalog_models.py +95 -0
- novacode_cli/config/config.py +1388 -0
- novacode_cli/config/credentials.py +540 -0
- novacode_cli/config/google_oauth_auth.py +281 -0
- novacode_cli/config/model_capabilities.py +343 -0
- novacode_cli/config/model_catalog.py +275 -0
- novacode_cli/config/model_create.py +1325 -0
- novacode_cli/config/model_manager.py +695 -0
- novacode_cli/config/nova_config.py +1283 -0
- novacode_cli/config/openai_chatgpt_auth.py +384 -0
- novacode_cli/config/opencode_gateway.py +25 -0
- novacode_cli/config/plan_mode.py +188 -0
- novacode_cli/config/provider_auth.py +438 -0
- novacode_cli/config/provider_catalog.json +5127 -0
- novacode_cli/config/provider_registry.py +38 -0
- novacode_cli/config/provider_transports.py +258 -0
- novacode_cli/config/role_models.py +175 -0
- novacode_cli/context/__init__.py +50 -0
- novacode_cli/context/_analysis.py +799 -0
- novacode_cli/context/_budget.py +116 -0
- novacode_cli/context/_conversation.py +73 -0
- novacode_cli/context/_dynamic.py +264 -0
- novacode_cli/context/_model_config.py +380 -0
- novacode_cli/context/_models_dev.py +99 -0
- novacode_cli/context/archives.py +248 -0
- novacode_cli/context/estimates.py +65 -0
- novacode_cli/context/history.py +17 -0
- novacode_cli/context/knowledge_cache.py +292 -0
- novacode_cli/context/manager.py +101 -0
- novacode_cli/context/observations.py +65 -0
- novacode_cli/context/pressure.py +168 -0
- novacode_cli/context/provider_accounting.py +109 -0
- novacode_cli/context/snapshot.py +493 -0
- novacode_cli/context/strategies.py +279 -0
- novacode_cli/context/task_progress.py +122 -0
- novacode_cli/core/__init__.py +0 -0
- novacode_cli/core/agent_loop.py +1821 -0
- novacode_cli/core/autonomous_loop.py +277 -0
- novacode_cli/core/code_approvals.py +57 -0
- novacode_cli/core/input_preparation.py +276 -0
- novacode_cli/core/reasoning.py +67 -0
- novacode_cli/core/streaming.py +326 -0
- novacode_cli/core/subagent_tasks.py +696 -0
- novacode_cli/core/subagent_tracking.py +235 -0
- novacode_cli/core/tool_results.py +116 -0
- novacode_cli/core/verification_loop.py +272 -0
- novacode_cli/council.py +595 -0
- novacode_cli/council_consensus.py +115 -0
- novacode_cli/council_decision.py +157 -0
- novacode_cli/council_planner.py +1421 -0
- novacode_cli/council_settings.py +208 -0
- novacode_cli/council_web_settings.py +41 -0
- novacode_cli/council_workspace.py +56 -0
- novacode_cli/cowork/__init__.py +10 -0
- novacode_cli/cowork/broker_middleware.py +162 -0
- novacode_cli/cowork/launcher.py +102 -0
- novacode_cli/cowork/policy.py +284 -0
- novacode_cli/cowork/ui.py +826 -0
- novacode_cli/daemons/__init__.py +1 -0
- novacode_cli/daemons/registry.py +342 -0
- novacode_cli/desktop_notifications.py +302 -0
- novacode_cli/doctor.py +251 -0
- novacode_cli/embeddings.py +109 -0
- novacode_cli/entrypoint.py +56 -0
- novacode_cli/errors/__init__.py +19 -0
- novacode_cli/errors/handlers.py +648 -0
- novacode_cli/errors/provider_errors.py +249 -0
- novacode_cli/errors/taxonomy.py +80 -0
- novacode_cli/events.py +179 -0
- novacode_cli/file_ops.py +714 -0
- novacode_cli/git_safety.py +281 -0
- novacode_cli/handoff.py +383 -0
- novacode_cli/headless/__init__.py +18 -0
- novacode_cli/headless/input.py +58 -0
- novacode_cli/headless/output.py +224 -0
- novacode_cli/headless/pipe.py +532 -0
- novacode_cli/headless/runner.py +461 -0
- novacode_cli/hermes/__init__.py +39 -0
- novacode_cli/hermes/activation.py +297 -0
- novacode_cli/hermes/config.py +129 -0
- novacode_cli/hermes/curator.py +199 -0
- novacode_cli/hermes/evolution.py +374 -0
- novacode_cli/hermes/ledger.py +364 -0
- novacode_cli/hermes/memory_tiers.py +969 -0
- novacode_cli/hermes/middleware.py +678 -0
- novacode_cli/hermes/overview.py +174 -0
- novacode_cli/hermes/prompt_evolution.py +503 -0
- novacode_cli/hermes/refine_loop.py +643 -0
- novacode_cli/hermes/refinement_log.py +190 -0
- novacode_cli/hermes/review.py +674 -0
- novacode_cli/hermes/skill_debate.py +135 -0
- novacode_cli/hermes/skill_discovery.py +450 -0
- novacode_cli/hermes/skill_manager.py +335 -0
- novacode_cli/hermes/skill_proposals.py +43 -0
- novacode_cli/hermes/test_gaming.py +119 -0
- novacode_cli/hermes/tracker.py +435 -0
- novacode_cli/hermes/tuner.py +206 -0
- novacode_cli/hermes/verification_memory.py +134 -0
- novacode_cli/hermes/verifier.py +277 -0
- novacode_cli/hitl/__init__.py +22 -0
- novacode_cli/hitl/interrupts.py +493 -0
- novacode_cli/hooks.py +359 -0
- novacode_cli/image_utils.py +517 -0
- novacode_cli/init/__init__.py +7 -0
- novacode_cli/init/events.py +43 -0
- novacode_cli/input_utils.py +526 -0
- novacode_cli/install_commands.py +87 -0
- novacode_cli/integrations/__init__.py +82 -0
- novacode_cli/integrations/daytona.py +122 -0
- novacode_cli/integrations/docker.py +309 -0
- novacode_cli/integrations/host_path.py +111 -0
- novacode_cli/integrations/langsmith.py +151 -0
- novacode_cli/integrations/modal.py +134 -0
- novacode_cli/integrations/runloop.py +130 -0
- novacode_cli/integrations/sandbox_factory.py +1388 -0
- novacode_cli/integrations/sandbox_registry.py +312 -0
- novacode_cli/integrations/workdir_backend.py +589 -0
- novacode_cli/main.py +2078 -0
- novacode_cli/mcp/__init__.py +73 -0
- novacode_cli/mcp/auth.py +189 -0
- novacode_cli/mcp/client.py +571 -0
- novacode_cli/mcp/code_mode.py +5 -0
- novacode_cli/mcp/code_runtime.py +5 -0
- novacode_cli/mcp/commands.py +902 -0
- novacode_cli/mcp/config.py +406 -0
- novacode_cli/mcp/generic.py +128 -0
- novacode_cli/mcp/middleware.py +993 -0
- novacode_cli/mcp/presets.py +367 -0
- novacode_cli/memory/__init__.py +1 -0
- novacode_cli/memory/agent_memory.py +1312 -0
- novacode_cli/memory/builtin_lessons.py +60 -0
- novacode_cli/memory/dream_gates.py +192 -0
- novacode_cli/memory/limits.py +59 -0
- novacode_cli/memory/parsed_cache.py +106 -0
- novacode_cli/memory/recall_stats.py +97 -0
- novacode_cli/memory/semantic.py +116 -0
- novacode_cli/memory/store.py +335 -0
- novacode_cli/memory/wiki.py +274 -0
- novacode_cli/migrate.py +185 -0
- novacode_cli/onboarding.py +649 -0
- novacode_cli/onboarding_check.py +119 -0
- novacode_cli/path_approval.py +297 -0
- novacode_cli/plan_archive.py +148 -0
- novacode_cli/plugins/__init__.py +121 -0
- novacode_cli/plugins/claude_plugins.py +538 -0
- novacode_cli/plugins/cli.py +168 -0
- novacode_cli/plugins/components.py +413 -0
- novacode_cli/plugins/hooks.py +356 -0
- novacode_cli/plugins/loader.py +317 -0
- novacode_cli/plugins/marketplaces.py +199 -0
- novacode_cli/plugins/storage.py +70 -0
- novacode_cli/prime.py +759 -0
- novacode_cli/prime_factory/__init__.py +15 -0
- novacode_cli/prime_factory/container.py +198 -0
- novacode_cli/prime_factory/controller.py +565 -0
- novacode_cli/prime_factory/knowledge.py +261 -0
- novacode_cli/prime_factory/publication.py +218 -0
- novacode_cli/prime_factory/recovery.py +50 -0
- novacode_cli/prime_factory/research.py +370 -0
- novacode_cli/prime_factory/runner.py +608 -0
- novacode_cli/prime_factory/runtime.py +261 -0
- novacode_cli/prime_factory/state.py +201 -0
- novacode_cli/prime_factory/store.py +128 -0
- novacode_cli/prime_factory/worker.py +129 -0
- novacode_cli/prime_factory/workflow.py +899 -0
- novacode_cli/prime_factory/workspace.py +317 -0
- novacode_cli/prime_memory.py +290 -0
- novacode_cli/process_manager.py +585 -0
- novacode_cli/project_onboard.py +537 -0
- novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
- novacode_cli/prompts/__init__.py +168 -0
- novacode_cli/prompts/agent_definitions.jinja +11 -0
- novacode_cli/prompts/agent_generation.jinja +125 -0
- novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
- novacode_cli/prompts/claude_plan_mode.jinja +12 -0
- novacode_cli/prompts/coding_workflow.jinja +18 -0
- novacode_cli/prompts/computer_use.jinja +17 -0
- novacode_cli/prompts/core_agent_system.jinja +81 -0
- novacode_cli/prompts/delegation.jinja +15 -0
- novacode_cli/prompts/desktop_power.jinja +4 -0
- novacode_cli/prompts/file_paths.jinja +15 -0
- novacode_cli/prompts/file_tracker.jinja +23 -0
- novacode_cli/prompts/init_agents_md.jinja +56 -0
- novacode_cli/prompts/init_exploration.jinja +174 -0
- novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
- novacode_cli/prompts/instruction_scope.jinja +5 -0
- novacode_cli/prompts/learnings_extraction.jinja +45 -0
- novacode_cli/prompts/longterm_memory.jinja +54 -0
- novacode_cli/prompts/mcp.jinja +84 -0
- novacode_cli/prompts/memory_consolidation.jinja +40 -0
- novacode_cli/prompts/nova_evolution.jinja +73 -0
- novacode_cli/prompts/nova_review.jinja +158 -0
- novacode_cli/prompts/nova_skill_debate.jinja +36 -0
- novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
- novacode_cli/prompts/nova_verify.jinja +100 -0
- novacode_cli/prompts/onboard_dashboard.jinja +378 -0
- novacode_cli/prompts/plan_agent.jinja +87 -0
- novacode_cli/prompts/plan_scouts.jinja +5 -0
- novacode_cli/prompts/plan_storage.jinja +8 -0
- novacode_cli/prompts/planning.jinja +82 -0
- novacode_cli/prompts/ponytail/LICENSE +21 -0
- novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
- novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
- novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
- novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
- novacode_cli/prompts/prompt_evolution.jinja +27 -0
- novacode_cli/prompts/ralph_iteration.jinja +75 -0
- novacode_cli/prompts/refine_gate.jinja +31 -0
- novacode_cli/prompts/refine_plan.jinja +64 -0
- novacode_cli/prompts/refine_review.jinja +34 -0
- novacode_cli/prompts/research_delivery.jinja +9 -0
- novacode_cli/prompts/research_swarm.jinja +130 -0
- novacode_cli/prompts/response_style.jinja +5 -0
- novacode_cli/prompts/session_summarization.jinja +86 -0
- novacode_cli/prompts/skill_creation.jinja +85 -0
- novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
- novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
- novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
- novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
- novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
- novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
- novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
- novacode_cli/prompts/summarization.jinja +107 -0
- novacode_cli/prompts/summarization_answers.jinja +23 -0
- novacode_cli/prompts/summarization_questions.jinja +32 -0
- novacode_cli/prompts/vision_caption.jinja +21 -0
- novacode_cli/prompts/voice_summary.jinja +65 -0
- novacode_cli/prompts/wiki_file.jinja +57 -0
- novacode_cli/prompts/wiki_ingest.jinja +87 -0
- novacode_cli/recovery.py +383 -0
- novacode_cli/remote/__init__.py +45 -0
- novacode_cli/remote/approvals.py +282 -0
- novacode_cli/remote/async_task_clients.py +97 -0
- novacode_cli/remote/async_task_watcher.py +240 -0
- novacode_cli/remote/bridge.py +905 -0
- novacode_cli/remote/command_menu.py +26 -0
- novacode_cli/remote/command_ui.py +243 -0
- novacode_cli/remote/config.py +199 -0
- novacode_cli/remote/discord_bridge.py +530 -0
- novacode_cli/remote/discord_format.py +31 -0
- novacode_cli/remote/discord_threads.py +225 -0
- novacode_cli/remote/images.py +97 -0
- novacode_cli/remote/processor.py +601 -0
- novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
- novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
- novacode_cli/remote/rootapp_bot/package.json +18 -0
- novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
- novacode_cli/remote/rootapp_bridge.py +370 -0
- novacode_cli/remote/rootapp_setup.py +89 -0
- novacode_cli/remote/routing.py +103 -0
- novacode_cli/remote/scheduler.py +302 -0
- novacode_cli/remote/session.py +164 -0
- novacode_cli/remote/status.py +411 -0
- novacode_cli/remote/streaming.py +188 -0
- novacode_cli/remote/telegram_bridge.py +1118 -0
- novacode_cli/remote/telegram_format.py +215 -0
- novacode_cli/remote/telegram_hub.py +256 -0
- novacode_cli/remote/voice_notes.py +120 -0
- novacode_cli/remote/webhook_adapters.py +167 -0
- novacode_cli/remote/webhook_server.py +154 -0
- novacode_cli/rewind.py +150 -0
- novacode_cli/rooms/__init__.py +1 -0
- novacode_cli/rooms/bridge.py +86 -0
- novacode_cli/rooms/cli.py +231 -0
- novacode_cli/rooms/nova.py +227 -0
- novacode_cli/rooms/runtime.py +299 -0
- novacode_cli/rooms/store.py +538 -0
- novacode_cli/rooms/workspace.py +94 -0
- novacode_cli/security/__init__.py +29 -0
- novacode_cli/security/approval_context.py +45 -0
- novacode_cli/security/delegated_approval.py +68 -0
- novacode_cli/security/local_http.py +85 -0
- novacode_cli/security/middleware.py +87 -0
- novacode_cli/security/paths.py +35 -0
- novacode_cli/security/policy.py +535 -0
- novacode_cli/security/policy_writer.py +90 -0
- novacode_cli/security/prompt_injection.py +203 -0
- novacode_cli/security/remember.py +81 -0
- novacode_cli/security/rule_synthesis.py +116 -0
- novacode_cli/security/secret_files.py +61 -0
- novacode_cli/security/session_allow.py +114 -0
- novacode_cli/security/unicode_security.py +516 -0
- novacode_cli/security/validator.py +173 -0
- novacode_cli/server/__init__.py +9 -0
- novacode_cli/server/app.py +632 -0
- novacode_cli/server/event_adapter.py +189 -0
- novacode_cli/server/session_manager.py +90 -0
- novacode_cli/server_main.py +64 -0
- novacode_cli/server_runner/dev_server.py +530 -0
- novacode_cli/server_runner/test_runner.py +470 -0
- novacode_cli/session/__init__.py +1 -0
- novacode_cli/session/adapters/__init__.py +240 -0
- novacode_cli/session/adapters/claude_code.py +102 -0
- novacode_cli/session/adapters/codex.py +77 -0
- novacode_cli/session/adapters/harness_native.py +75 -0
- novacode_cli/session/checkpoint_coordinator.py +448 -0
- novacode_cli/session/context_restore.py +25 -0
- novacode_cli/session/history_export.py +228 -0
- novacode_cli/session/history_tree.py +174 -0
- novacode_cli/session/imported_context.py +267 -0
- novacode_cli/session/runtime_identity.py +32 -0
- novacode_cli/session/session_persistence.py +1168 -0
- novacode_cli/session/session_prompt_builder.py +279 -0
- novacode_cli/session/session_restore.py +351 -0
- novacode_cli/session/session_summarization.py +20 -0
- novacode_cli/session/transcript_journal.py +153 -0
- novacode_cli/sessions/__init__.py +11 -0
- novacode_cli/sessions/gateway.py +84 -0
- novacode_cli/sessions/launch.py +51 -0
- novacode_cli/sessions/lease.py +213 -0
- novacode_cli/sessions/messaging.py +255 -0
- novacode_cli/sessions/protocol.py +216 -0
- novacode_cli/sessions/resume.py +31 -0
- novacode_cli/sessions/supervisor.py +498 -0
- novacode_cli/sessions/tasks.py +73 -0
- novacode_cli/sessions/tui_messaging.py +413 -0
- novacode_cli/sessions/worker.py +652 -0
- novacode_cli/sessions/worktree.py +262 -0
- novacode_cli/shell/__init__.py +27 -0
- novacode_cli/shell/jobs.py +475 -0
- novacode_cli/shell/middleware.py +1568 -0
- novacode_cli/shell/os_sandbox.py +205 -0
- novacode_cli/shell/output.py +181 -0
- novacode_cli/shell/patterns.py +275 -0
- novacode_cli/shell/processes.py +87 -0
- novacode_cli/shell/utils.py +236 -0
- novacode_cli/skills/__init__.py +26 -0
- novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
- novacode_cli/skills/curation_middleware.py +69 -0
- novacode_cli/skills/executable.py +161 -0
- novacode_cli/skills/libraries.py +102 -0
- novacode_cli/skills/load.py +248 -0
- novacode_cli/skills/prune.py +215 -0
- novacode_cli/skills/refreshing_middleware.py +896 -0
- novacode_cli/skills/retrieval.py +248 -0
- novacode_cli/skills/runtime.py +118 -0
- novacode_cli/skills/schema.py +254 -0
- novacode_cli/skills/skill_creation.py +1549 -0
- novacode_cli/skills/skill_lock.py +75 -0
- novacode_cli/skills/skill_system_prompt.py +16 -0
- novacode_cli/skills/skills_prefs.py +126 -0
- novacode_cli/skills/tool_resolver.py +31 -0
- novacode_cli/skills/upstream.py +50 -0
- novacode_cli/skills/versioning.py +291 -0
- novacode_cli/states/Session.py +847 -0
- novacode_cli/states/__init__.py +31 -0
- novacode_cli/states/slices/__init__.py +17 -0
- novacode_cli/states/slices/agent_runtime.py +249 -0
- novacode_cli/states/slices/background_tasks.py +21 -0
- novacode_cli/states/slices/notifications.py +74 -0
- novacode_cli/states/slices/remote_bridge.py +56 -0
- novacode_cli/states/slices/ui_settings.py +49 -0
- novacode_cli/states/slices/wiki.py +25 -0
- novacode_cli/teammates/__init__.py +1 -0
- novacode_cli/teammates/controller.py +557 -0
- novacode_cli/teammates/graph.py +104 -0
- novacode_cli/teammates/registry.py +175 -0
- novacode_cli/teammates/service.py +254 -0
- novacode_cli/teammates/worker.py +19 -0
- novacode_cli/teams/__init__.py +1 -0
- novacode_cli/teams/adapters/__init__.py +1 -0
- novacode_cli/teams/adapters/container.py +316 -0
- novacode_cli/teams/adapters/inprocess.py +206 -0
- novacode_cli/teams/adapters/process.py +364 -0
- novacode_cli/teams/bootstrap.py +246 -0
- novacode_cli/teams/container_git.py +67 -0
- novacode_cli/teams/context.py +34 -0
- novacode_cli/teams/coordinator.py +815 -0
- novacode_cli/teams/gateway.py +200 -0
- novacode_cli/teams/host.py +64 -0
- novacode_cli/teams/legacy.py +52 -0
- novacode_cli/teams/middleware.py +176 -0
- novacode_cli/teams/models.py +104 -0
- novacode_cli/teams/output.py +47 -0
- novacode_cli/teams/protocol.py +50 -0
- novacode_cli/teams/recovery.py +80 -0
- novacode_cli/teams/scheduler.py +44 -0
- novacode_cli/teams/store.py +631 -0
- novacode_cli/teams/worker.py +251 -0
- novacode_cli/teams/workspace.py +100 -0
- novacode_cli/token_utils.py +256 -0
- novacode_cli/tools/__init__.py +104 -0
- novacode_cli/tools/_shared.py +97 -0
- novacode_cli/tools/_types.py +24 -0
- novacode_cli/tools/artifact_tools.py +137 -0
- novacode_cli/tools/code_search_tools.py +330 -0
- novacode_cli/tools/computer_tools.py +77 -0
- novacode_cli/tools/cron_tools.py +65 -0
- novacode_cli/tools/daemon_tool.py +290 -0
- novacode_cli/tools/desktop_tools.py +161 -0
- novacode_cli/tools/fetch_tools.py +530 -0
- novacode_cli/tools/http_tools.py +279 -0
- novacode_cli/tools/inspection_tools.py +107 -0
- novacode_cli/tools/job_tools.py +143 -0
- novacode_cli/tools/knowledge_tools.py +427 -0
- novacode_cli/tools/memory_tools.py +461 -0
- novacode_cli/tools/memory_wiki_tools.py +40 -0
- novacode_cli/tools/notebook_tools.py +238 -0
- novacode_cli/tools/oracle_tool.py +83 -0
- novacode_cli/tools/plan_archive_tools.py +80 -0
- novacode_cli/tools/plan_mode_tools.py +315 -0
- novacode_cli/tools/python_kernel_loop.py +165 -0
- novacode_cli/tools/python_kernel_tool.py +222 -0
- novacode_cli/tools/reflection_tools.py +34 -0
- novacode_cli/tools/scraper_tools.py +377 -0
- novacode_cli/tools/skill_tools.py +275 -0
- novacode_cli/tools/speak_tool.py +51 -0
- novacode_cli/tools/store_memory_tools.py +227 -0
- novacode_cli/tools/ui_tools.py +62 -0
- novacode_cli/tools/waiter_tools.py +408 -0
- novacode_cli/tools/web_tools.py +349 -0
- novacode_cli/tools/wiki_tools.py +152 -0
- novacode_cli/tools/worktree_tools.py +219 -0
- novacode_cli/tracking/__init__.py +10 -0
- novacode_cli/tracking/file_tracker.py +826 -0
- novacode_cli/tracking/local_metrics.py +170 -0
- novacode_cli/tracking/loop_guard.py +507 -0
- novacode_cli/tracking/request_metrics.py +60 -0
- novacode_cli/tracking/tracing.py +692 -0
- novacode_cli/tracking/usage_tree.py +677 -0
- novacode_cli/tracking/workspace_anchoring.py +231 -0
- novacode_cli/tui/__init__.py +11 -0
- novacode_cli/tui/animation_rate.py +9 -0
- novacode_cli/tui/animations.py +332 -0
- novacode_cli/tui/app.py +19315 -0
- novacode_cli/tui/auth_display.py +87 -0
- novacode_cli/tui/auth_screens.py +826 -0
- novacode_cli/tui/avatar.py +294 -0
- novacode_cli/tui/avatar_frames.py +21 -0
- novacode_cli/tui/background_tasks.py +35 -0
- novacode_cli/tui/browser_settings_screen.py +70 -0
- novacode_cli/tui/caveman_screen.py +185 -0
- novacode_cli/tui/council_settings_screen.py +182 -0
- novacode_cli/tui/council_view.py +535 -0
- novacode_cli/tui/decision_settings.py +268 -0
- novacode_cli/tui/diff_preview.py +199 -0
- novacode_cli/tui/gc_tuning.py +63 -0
- novacode_cli/tui/handoff_screen.py +206 -0
- novacode_cli/tui/harness.py +244 -0
- novacode_cli/tui/history_tree_screen.py +179 -0
- novacode_cli/tui/home.py +328 -0
- novacode_cli/tui/home_scene.py +94 -0
- novacode_cli/tui/icons.py +81 -0
- novacode_cli/tui/interface_styles.tcss +327 -0
- novacode_cli/tui/matrix3.py +108 -0
- novacode_cli/tui/mcp_tools_screen.py +131 -0
- novacode_cli/tui/motion.py +178 -0
- novacode_cli/tui/output_body.py +136 -0
- novacode_cli/tui/output_buffer.py +59 -0
- novacode_cli/tui/palette.py +334 -0
- novacode_cli/tui/pickers.py +237 -0
- novacode_cli/tui/prime_panel.py +506 -0
- novacode_cli/tui/provider_signin.py +213 -0
- novacode_cli/tui/pty_shell.py +159 -0
- novacode_cli/tui/reference_screens.py +444 -0
- novacode_cli/tui/resume_picker.py +346 -0
- novacode_cli/tui/rooms.py +597 -0
- novacode_cli/tui/screens.py +7088 -0
- novacode_cli/tui/session_import.py +333 -0
- novacode_cli/tui/session_pane.py +242 -0
- novacode_cli/tui/session_tab.py +42 -0
- novacode_cli/tui/session_transcript.py +118 -0
- novacode_cli/tui/shell_output.py +7 -0
- novacode_cli/tui/stall_watch.py +174 -0
- novacode_cli/tui/statusline.py +40 -0
- novacode_cli/tui/statusline_screen.py +91 -0
- novacode_cli/tui/teammate_updates.py +162 -0
- novacode_cli/tui/teammates.py +548 -0
- novacode_cli/tui/terminal.py +289 -0
- novacode_cli/tui/terminal_view.py +235 -0
- novacode_cli/tui/themes.py +118 -0
- novacode_cli/tui/update_notice.py +84 -0
- novacode_cli/tui/voice_downloads.py +113 -0
- novacode_cli/tui/widgets.py +2038 -0
- novacode_cli/tui/windows_input.py +38 -0
- novacode_cli/tui/workspace_approval.py +165 -0
- novacode_cli/ui/council_web.py +113 -0
- novacode_cli/ui/council_workspace.py +72 -0
- novacode_cli/ui/cowork_web.py +41 -0
- novacode_cli/ui/diff_highlight.py +160 -0
- novacode_cli/ui/execution.py +1396 -0
- novacode_cli/ui/exit_summary.py +65 -0
- novacode_cli/ui/hitl_approval.py +196 -0
- novacode_cli/ui/input_preparation.py +2 -0
- novacode_cli/ui/interrupt_handlers.py +215 -0
- novacode_cli/ui/question_prompt.py +151 -0
- novacode_cli/ui/status_phrases.py +157 -0
- novacode_cli/ui/stream_preview.py +66 -0
- novacode_cli/ui/streaming.py +2 -0
- novacode_cli/ui/subagent_tracking.py +2 -0
- novacode_cli/ui/ui_elements.py +1585 -0
- novacode_cli/ui/web_workspace.py +244 -0
- novacode_cli/ui_events.py +299 -0
- novacode_cli/update_ui.py +243 -0
- novacode_cli/updates.py +489 -0
- novacode_cli/utils/backend_patches.py +399 -0
- novacode_cli/utils/backend_paths.py +467 -0
- novacode_cli/utils/lazy_providers.py +51 -0
- novacode_cli/utils/model_info.py +223 -0
- novacode_cli/utils/pdf_extraction.py +183 -0
- novacode_cli/utils/startup.py +29 -0
- novacode_cli/utils/tasks.py +42 -0
- novacode_cli/utils/windows.py +9 -0
- novacode_cli/vixie/hook_adapter.py +127 -0
- novacode_cli/vixie/server.py +375 -0
- novacode_cli/wiki/__init__.py +15 -0
- novacode_cli/wiki/ask.py +56 -0
- novacode_cli/wiki/file.py +71 -0
- novacode_cli/wiki/ingest.py +184 -0
- novacode_cli/wiki/manager.py +261 -0
- novacode_cli-1.2.0.dist-info/METADATA +271 -0
- novacode_cli-1.2.0.dist-info/RECORD +691 -0
- novacode_cli-1.2.0.dist-info/WHEEL +5 -0
- novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
- novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
- novacode_cli-1.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"""On-demand image inspection for a text-only main model.
|
|
2
|
+
|
|
3
|
+
When the main model **cannot** see images, raw image content stays in graph state
|
|
4
|
+
and is stripped from its model requests. The main model receives a note that it
|
|
5
|
+
can invoke ``VISION`` when it needs visual details. That tool makes one bounded
|
|
6
|
+
side-call to the separately configured vision model and returns a text caption.
|
|
7
|
+
|
|
8
|
+
When the main model **can** see images (a multimodal model — see
|
|
9
|
+
:mod:`novacode_cli.config.model_capabilities`), captioning is skipped entirely:
|
|
10
|
+
the image is passed straight through to the main model, which reads it directly.
|
|
11
|
+
This is the fix for the long-standing "read_file on an image returns
|
|
12
|
+
``[image: vision model unavailable]`` even though my model is multimodal" bug —
|
|
13
|
+
the capable model was being bypassed in favour of an auxiliary one.
|
|
14
|
+
|
|
15
|
+
Two image sources are available to ``VISION`` (text-only main model only):
|
|
16
|
+
|
|
17
|
+
- **disk reads** — image-bearing ``read_file`` results remain in graph state.
|
|
18
|
+
- **clipboard pastes** — image blocks remain in the user message.
|
|
19
|
+
|
|
20
|
+
:meth:`VisionCaptionMiddleware.awrap_model_call` strips image blocks from
|
|
21
|
+
text-only requests and appends a tool-use hint. Multimodal main models receive
|
|
22
|
+
the image blocks directly. The session model never changes.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import asyncio
|
|
28
|
+
import base64
|
|
29
|
+
import functools
|
|
30
|
+
import io
|
|
31
|
+
import logging
|
|
32
|
+
from typing import TYPE_CHECKING, Any
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from collections.abc import Awaitable, Callable
|
|
36
|
+
|
|
37
|
+
from langchain.tools.tool_node import ToolCallRequest
|
|
38
|
+
from langchain_core.language_models import BaseChatModel
|
|
39
|
+
from langgraph.types import Command
|
|
40
|
+
|
|
41
|
+
from langchain.agents.middleware.types import (
|
|
42
|
+
AgentMiddleware,
|
|
43
|
+
ModelRequest,
|
|
44
|
+
ModelResponse,
|
|
45
|
+
)
|
|
46
|
+
from langchain.tools import ToolRuntime, tool
|
|
47
|
+
from langchain_core.messages import AIMessage, AnyMessage, HumanMessage, ToolMessage
|
|
48
|
+
|
|
49
|
+
logger = logging.getLogger("nova.vision_router")
|
|
50
|
+
|
|
51
|
+
# MIME type fallback by extension suffix
|
|
52
|
+
_MIME_BY_SUFFIX: dict[str, str] = {
|
|
53
|
+
".png": "image/png",
|
|
54
|
+
".jpg": "image/jpeg",
|
|
55
|
+
".jpeg": "image/jpeg",
|
|
56
|
+
".gif": "image/gif",
|
|
57
|
+
".webp": "image/webp",
|
|
58
|
+
".bmp": "image/bmp",
|
|
59
|
+
".tiff": "image/tiff",
|
|
60
|
+
".tif": "image/tiff",
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#: Appended to every failure placeholder. Without it the model is handed a
|
|
64
|
+
#: bracketed note about an image it cannot see and improvises — in practice it
|
|
65
|
+
#: writes a script to decode the file and read raw pixel values, which answers
|
|
66
|
+
#: nothing and costs a tool round-trip. Saying so explicitly stops that.
|
|
67
|
+
_NO_PIXEL_ARCHAEOLOGY = (
|
|
68
|
+
" Do not try to inspect the image yourself (no pixel decoding, no PIL/ffmpeg "
|
|
69
|
+
"scripts) — it cannot be read that way. Ask the user what it shows, or "
|
|
70
|
+
"suggest `/vision on` if their model can see images."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Placeholder captions returned (instead of leaking an image) when vision fails.
|
|
74
|
+
_VISION_UNAVAILABLE = (
|
|
75
|
+
"[image: vision model unavailable — configure it in `/model` → `Vision`, "
|
|
76
|
+
"or run `/vision <provider> <model>`]"
|
|
77
|
+
)
|
|
78
|
+
_VISION_UNAVAILABLE += _NO_PIXEL_ARCHAEOLOGY
|
|
79
|
+
_VISION_FAILED = (
|
|
80
|
+
"[image: vision captioning failed — the configured vision_model rejected the "
|
|
81
|
+
"image or is unavailable]" + _NO_PIXEL_ARCHAEOLOGY
|
|
82
|
+
)
|
|
83
|
+
_VISION_EMPTY = "[image: vision model returned no description]" + _NO_PIXEL_ARCHAEOLOGY
|
|
84
|
+
|
|
85
|
+
#: How many consecutive images to skip captioning for after a hard failure.
|
|
86
|
+
#: A single transient failure (a cold Ollama model, a momentary network blip)
|
|
87
|
+
#: must not disable image reading for the rest of the session — the old
|
|
88
|
+
#: permanent latch did exactly that. After this many skips the vision model is
|
|
89
|
+
#: retried, so a recovered provider starts working again without a restart.
|
|
90
|
+
_VISION_FAILURE_COOLDOWN = 3
|
|
91
|
+
VISION_TOOL_TIMEOUT_SECONDS = 30
|
|
92
|
+
_VISION_TOOL_NOTE = (
|
|
93
|
+
"[An image is attached but was not sent to this text-only model. "
|
|
94
|
+
"Call VISION if you need to inspect its visual contents.]"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _suffix_to_mime_type(suffix: str | None) -> str:
|
|
99
|
+
"""Best-effort MIME type from a file suffix (defaults to ``image/png``)."""
|
|
100
|
+
if suffix:
|
|
101
|
+
return _MIME_BY_SUFFIX.get(suffix.lower(), "image/png")
|
|
102
|
+
return "image/png"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── Vision model (process-wide cache, shared by both entry points) ──────────
|
|
106
|
+
|
|
107
|
+
_vision_model_instance: BaseChatModel | None = None
|
|
108
|
+
_vision_model_errored: bool = False
|
|
109
|
+
_vision_failure_reason: str | None = None
|
|
110
|
+
#: Remaining images to skip before retrying the vision model after a failure.
|
|
111
|
+
#: Decremented on each caption attempt while > 0; when it reaches 0 the model is
|
|
112
|
+
#: retried. Replaces the old permanent latch (one failure killed vision for the
|
|
113
|
+
#: whole session).
|
|
114
|
+
_vision_cooldown_remaining: int = 0
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def get_vision_model() -> BaseChatModel | None:
|
|
118
|
+
"""Lazily create the configured vision model (cached process-wide).
|
|
119
|
+
|
|
120
|
+
Returns ``None`` when creation fails or the provider is unavailable. A
|
|
121
|
+
failure starts a short cooldown (see :data:`_VISION_FAILURE_COOLDOWN`) rather
|
|
122
|
+
than disabling vision permanently, so a transient problem self-heals.
|
|
123
|
+
"""
|
|
124
|
+
global _vision_model_instance, _vision_model_errored # noqa: PLW0603
|
|
125
|
+
if _vision_model_errored:
|
|
126
|
+
return None
|
|
127
|
+
if _vision_model_instance is not None:
|
|
128
|
+
return _vision_model_instance
|
|
129
|
+
try:
|
|
130
|
+
from novacode_cli.config.model_create import create_model_from_config
|
|
131
|
+
from novacode_cli.config.nova_config import NovaConfig
|
|
132
|
+
|
|
133
|
+
cfg = NovaConfig().get_vision_model_config()
|
|
134
|
+
model = create_model_from_config(cfg["provider"], cfg["model"])
|
|
135
|
+
except Exception:
|
|
136
|
+
logger.exception("Failed to create vision model")
|
|
137
|
+
_vision_model_errored = True
|
|
138
|
+
return None
|
|
139
|
+
if model is None:
|
|
140
|
+
logger.warning("Vision model unavailable (missing provider/API key?)")
|
|
141
|
+
_vision_model_errored = True
|
|
142
|
+
return None
|
|
143
|
+
_vision_model_instance = model
|
|
144
|
+
logger.info("Vision captioning model ready")
|
|
145
|
+
return model
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _mark_vision_errored() -> None:
|
|
149
|
+
"""Start a short cooldown after a hard failure (not a permanent disable).
|
|
150
|
+
|
|
151
|
+
The vision model is dropped and skipped for the next
|
|
152
|
+
:data:`_VISION_FAILURE_COOLDOWN` images, then retried — so a transient
|
|
153
|
+
failure (cold model, network blip) recovers without a Nova restart.
|
|
154
|
+
"""
|
|
155
|
+
global _vision_model_instance, _vision_cooldown_remaining # noqa: PLW0603
|
|
156
|
+
_vision_model_instance = None
|
|
157
|
+
_vision_cooldown_remaining = _VISION_FAILURE_COOLDOWN
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def reset_vision_model_cache() -> None:
|
|
161
|
+
"""Forget a cached model/error after its provider or model is reconfigured."""
|
|
162
|
+
global _vision_model_instance, _vision_model_errored, _vision_cooldown_remaining, _vision_failure_reason # noqa: PLW0603
|
|
163
|
+
_vision_model_instance = None
|
|
164
|
+
_vision_model_errored = False
|
|
165
|
+
_vision_cooldown_remaining = 0
|
|
166
|
+
_vision_failure_reason = None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _vision_in_cooldown() -> bool:
|
|
170
|
+
"""True while the vision model is being skipped after a recent failure."""
|
|
171
|
+
global _vision_cooldown_remaining # noqa: PLW0603
|
|
172
|
+
if _vision_cooldown_remaining > 0:
|
|
173
|
+
_vision_cooldown_remaining -= 1
|
|
174
|
+
return True
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def caption_images(image_urls: list[str], task_hint: str = "") -> str:
|
|
179
|
+
"""Caption one or more images to text via the vision model (out-of-band).
|
|
180
|
+
|
|
181
|
+
Never raises and never returns image data — on any failure it returns a
|
|
182
|
+
short placeholder string so the conversation continues as text-only.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
image_urls: ``data:<mime>;base64,<…>`` URLs (or http(s) URLs).
|
|
186
|
+
task_hint: The user's current request, so the caption targets the task.
|
|
187
|
+
"""
|
|
188
|
+
global _vision_failure_reason # noqa: PLW0603
|
|
189
|
+
if not image_urls:
|
|
190
|
+
return ""
|
|
191
|
+
if _vision_in_cooldown():
|
|
192
|
+
reason = _vision_failure_reason or "temporarily paused after a previous failure"
|
|
193
|
+
return f"{_VISION_FAILED} ({reason})"
|
|
194
|
+
model = get_vision_model()
|
|
195
|
+
if model is None:
|
|
196
|
+
try:
|
|
197
|
+
from novacode_cli.config.nova_config import NovaConfig
|
|
198
|
+
|
|
199
|
+
cfg = NovaConfig().get_vision_model_config()
|
|
200
|
+
selected = f"{cfg['provider']}/{cfg['model']}"
|
|
201
|
+
except Exception: # noqa: BLE001 — this is already a fail-open path
|
|
202
|
+
selected = "the configured vision model"
|
|
203
|
+
return f"{_VISION_UNAVAILABLE} (could not initialize {selected})"
|
|
204
|
+
try:
|
|
205
|
+
from novacode_cli.prompts import render_template
|
|
206
|
+
|
|
207
|
+
prompt = render_template("vision_caption.jinja", task_hint=(task_hint or "").strip())
|
|
208
|
+
human_content: list[dict[str, Any]] = [
|
|
209
|
+
{"type": "text", "text": prompt},
|
|
210
|
+
*[{"type": "image_url", "image_url": {"url": u}} for u in image_urls],
|
|
211
|
+
]
|
|
212
|
+
async with asyncio.timeout(VISION_TOOL_TIMEOUT_SECONDS):
|
|
213
|
+
resp = await model.ainvoke(
|
|
214
|
+
[HumanMessage(content=human_content)],
|
|
215
|
+
config={
|
|
216
|
+
"run_name": "nova_vision_caption",
|
|
217
|
+
"tags": ["nova", "vision", "caption"],
|
|
218
|
+
"metadata": {"nova_oob": True},
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
raw = getattr(resp, "content", "")
|
|
222
|
+
text = (raw if isinstance(raw, str) else str(raw)).strip()
|
|
223
|
+
except TimeoutError:
|
|
224
|
+
_vision_failure_reason = f"request timed out after {VISION_TOOL_TIMEOUT_SECONDS} seconds"
|
|
225
|
+
_mark_vision_errored()
|
|
226
|
+
return f"{_VISION_FAILED} ({_vision_failure_reason})"
|
|
227
|
+
except Exception as e: # noqa: BLE001 — never crash a turn over a caption
|
|
228
|
+
# A KNOWN provider problem (region opt-in, auth, quota) means the vision
|
|
229
|
+
# model is simply unavailable — log one clean line, not a stack trace, so
|
|
230
|
+
# a region-restricted auxiliary model doesn't spam tracebacks every image.
|
|
231
|
+
from novacode_cli.errors import friendly_model_error
|
|
232
|
+
|
|
233
|
+
notice = friendly_model_error(e)
|
|
234
|
+
if notice:
|
|
235
|
+
reason = notice.splitlines()[0]
|
|
236
|
+
logger.warning("Vision captioning unavailable: %s", reason)
|
|
237
|
+
else:
|
|
238
|
+
reason = str(e).splitlines()[0] if str(e) else "unknown model error"
|
|
239
|
+
logger.warning("Vision captioning failed", exc_info=True)
|
|
240
|
+
_vision_failure_reason = reason
|
|
241
|
+
_mark_vision_errored()
|
|
242
|
+
return f"{_VISION_FAILED} ({reason})"
|
|
243
|
+
_vision_failure_reason = None
|
|
244
|
+
return text or _VISION_EMPTY
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# ── Image extraction / stripping helpers ────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _extract_image_urls(msg: AnyMessage) -> list[str]:
|
|
251
|
+
"""Extract ``data:...;base64,...`` URLs from a message's ``content_blocks``.
|
|
252
|
+
|
|
253
|
+
deepagents' ``read_file`` stores image data as ``content_blocks`` of
|
|
254
|
+
``type == "image"`` with a ``base64`` field; the MIME type falls back to the
|
|
255
|
+
file suffix recorded in ``additional_kwargs['read_file_path']``.
|
|
256
|
+
"""
|
|
257
|
+
urls: list[str] = []
|
|
258
|
+
content_blocks = getattr(msg, "content_blocks", None) or []
|
|
259
|
+
for block in content_blocks:
|
|
260
|
+
if isinstance(block, dict) and block.get("type") == "image":
|
|
261
|
+
b64 = block.get("base64", "")
|
|
262
|
+
mime = block.get("mime_type", None)
|
|
263
|
+
if not mime:
|
|
264
|
+
rfp = getattr(msg, "additional_kwargs", {}).get("read_file_path", "")
|
|
265
|
+
suffix = None
|
|
266
|
+
if rfp:
|
|
267
|
+
from pathlib import Path
|
|
268
|
+
|
|
269
|
+
suffix = Path(rfp).suffix
|
|
270
|
+
mime = _suffix_to_mime_type(suffix)
|
|
271
|
+
if b64:
|
|
272
|
+
urls.append(f"data:{mime};base64,{b64}")
|
|
273
|
+
return urls
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _collect_image_urls(msg: AnyMessage) -> list[str]:
|
|
277
|
+
"""Collect image URLs from BOTH ``content_blocks`` and a list ``content``.
|
|
278
|
+
|
|
279
|
+
Covers read_file (image blocks in ``content_blocks``) and pasted multimodal
|
|
280
|
+
messages (``{"type": "image_url", "image_url": {"url": …}}`` in ``content``).
|
|
281
|
+
Order-preserving and de-duplicated.
|
|
282
|
+
"""
|
|
283
|
+
urls: list[str] = list(_extract_image_urls(msg))
|
|
284
|
+
content = getattr(msg, "content", None)
|
|
285
|
+
if isinstance(content, list):
|
|
286
|
+
for block in content:
|
|
287
|
+
if not isinstance(block, dict):
|
|
288
|
+
continue
|
|
289
|
+
btype = block.get("type")
|
|
290
|
+
if btype == "image_url":
|
|
291
|
+
image_url = block.get("image_url")
|
|
292
|
+
url = image_url.get("url") if isinstance(image_url, dict) else image_url
|
|
293
|
+
if url:
|
|
294
|
+
urls.append(url)
|
|
295
|
+
elif btype == "image" and block.get("base64"):
|
|
296
|
+
mime = block.get("mime_type") or "image/png"
|
|
297
|
+
urls.append(f"data:{mime};base64,{block['base64']}")
|
|
298
|
+
seen: set[str] = set()
|
|
299
|
+
out: list[str] = []
|
|
300
|
+
for url in urls:
|
|
301
|
+
if url not in seen:
|
|
302
|
+
seen.add(url)
|
|
303
|
+
out.append(url)
|
|
304
|
+
return out
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _strip_image_content(msg: AnyMessage) -> AnyMessage:
|
|
308
|
+
"""Return *msg* stripped of any image content blocks.
|
|
309
|
+
|
|
310
|
+
Fast path: returns the same object when no image data is present (zero
|
|
311
|
+
allocation for the common text-only case). Used as a defensive safety net so
|
|
312
|
+
no residual image reaches the main model.
|
|
313
|
+
"""
|
|
314
|
+
content_blocks = getattr(msg, "content_blocks", None) or []
|
|
315
|
+
has_image_in_blocks = any(
|
|
316
|
+
isinstance(b, dict) and b.get("type") == "image" for b in content_blocks
|
|
317
|
+
)
|
|
318
|
+
raw_content = msg.content
|
|
319
|
+
has_image_in_content = isinstance(raw_content, list) and any(
|
|
320
|
+
isinstance(b, dict) and b.get("type") in ("image", "image_url") for b in raw_content
|
|
321
|
+
)
|
|
322
|
+
if not has_image_in_blocks and not has_image_in_content:
|
|
323
|
+
return msg # fast path — same object
|
|
324
|
+
|
|
325
|
+
if isinstance(raw_content, list):
|
|
326
|
+
clean_blocks = [
|
|
327
|
+
b
|
|
328
|
+
for b in raw_content
|
|
329
|
+
if not (isinstance(b, dict) and b.get("type") in ("image", "image_url"))
|
|
330
|
+
]
|
|
331
|
+
if isinstance(msg, ToolMessage):
|
|
332
|
+
text_parts = [
|
|
333
|
+
b.get("text", "")
|
|
334
|
+
for b in clean_blocks
|
|
335
|
+
if isinstance(b, dict) and b.get("type") == "text"
|
|
336
|
+
]
|
|
337
|
+
new_content: str | list = " ".join(text_parts) if text_parts else ""
|
|
338
|
+
else:
|
|
339
|
+
new_content = clean_blocks if clean_blocks else ""
|
|
340
|
+
else:
|
|
341
|
+
new_content = raw_content
|
|
342
|
+
|
|
343
|
+
# Keep a useful breadcrumb for the text-only model, but never send the
|
|
344
|
+
# encoded image itself. The original message remains in graph state for the
|
|
345
|
+
# VISION tool to read only if the model chooses to call it.
|
|
346
|
+
if isinstance(msg, HumanMessage):
|
|
347
|
+
text_content = (
|
|
348
|
+
" ".join(
|
|
349
|
+
b.get("text", "")
|
|
350
|
+
for b in new_content
|
|
351
|
+
if isinstance(b, dict) and b.get("type") == "text"
|
|
352
|
+
)
|
|
353
|
+
if isinstance(new_content, list)
|
|
354
|
+
else str(new_content or "")
|
|
355
|
+
)
|
|
356
|
+
if _VISION_TOOL_NOTE not in text_content:
|
|
357
|
+
if isinstance(new_content, list):
|
|
358
|
+
new_content.append({"type": "text", "text": _VISION_TOOL_NOTE})
|
|
359
|
+
else:
|
|
360
|
+
new_content = f"{text_content}\n\n{_VISION_TOOL_NOTE}".strip()
|
|
361
|
+
elif isinstance(msg, ToolMessage):
|
|
362
|
+
text_content = (
|
|
363
|
+
" ".join(
|
|
364
|
+
b.get("text", "")
|
|
365
|
+
for b in new_content
|
|
366
|
+
if isinstance(b, dict) and b.get("type") == "text"
|
|
367
|
+
)
|
|
368
|
+
if isinstance(new_content, list)
|
|
369
|
+
else str(new_content or "")
|
|
370
|
+
)
|
|
371
|
+
notice = "[Image content omitted; call VISION to inspect it if needed.]"
|
|
372
|
+
new_content = f"{text_content}\n{notice}".strip()
|
|
373
|
+
|
|
374
|
+
kwargs: dict[str, Any] = {
|
|
375
|
+
"content": new_content,
|
|
376
|
+
"additional_kwargs": getattr(msg, "additional_kwargs", {}),
|
|
377
|
+
}
|
|
378
|
+
if isinstance(msg, HumanMessage):
|
|
379
|
+
return HumanMessage(**kwargs)
|
|
380
|
+
if isinstance(msg, AIMessage):
|
|
381
|
+
return AIMessage(**kwargs)
|
|
382
|
+
if isinstance(msg, ToolMessage):
|
|
383
|
+
return ToolMessage(
|
|
384
|
+
**kwargs,
|
|
385
|
+
name=getattr(msg, "name", None),
|
|
386
|
+
tool_call_id=getattr(msg, "tool_call_id", ""),
|
|
387
|
+
artifact=getattr(msg, "artifact", None),
|
|
388
|
+
)
|
|
389
|
+
return msg
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _strip_all_images(messages: list[AnyMessage]) -> list[AnyMessage]:
|
|
393
|
+
"""Return *messages* with all image blocks stripped (copy-on-write).
|
|
394
|
+
|
|
395
|
+
Returns the original list (by identity) when nothing needs stripping.
|
|
396
|
+
"""
|
|
397
|
+
needs_strip = False
|
|
398
|
+
for i, msg in enumerate(messages):
|
|
399
|
+
stripped = _strip_image_content(msg)
|
|
400
|
+
if stripped is not msg:
|
|
401
|
+
if not needs_strip:
|
|
402
|
+
messages = list(messages)
|
|
403
|
+
needs_strip = True
|
|
404
|
+
messages[i] = stripped
|
|
405
|
+
return messages
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
#: Formats every vision API accepts. Ollama rejects the rest ("invalid image
|
|
409
|
+
#: input"), and one such block in history fails every later call.
|
|
410
|
+
_PORTABLE_IMAGE_TYPES = frozenset({"image/png", "image/jpeg"})
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
@functools.lru_cache(maxsize=32)
|
|
414
|
+
def _as_jpeg(data: str) -> str:
|
|
415
|
+
from PIL import Image
|
|
416
|
+
|
|
417
|
+
from novacode_cli.image_utils import ImageProcessor
|
|
418
|
+
|
|
419
|
+
image = Image.open(io.BytesIO(base64.b64decode(data)))
|
|
420
|
+
return base64.b64encode(ImageProcessor.optimize_image(image, output_format="jpeg")).decode()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _portable_images(messages: list[AnyMessage]) -> list[AnyMessage]:
|
|
424
|
+
"""Re-encode WebP/GIF/BMP/TIFF image blocks as JPEG (copy-on-write)."""
|
|
425
|
+
out = messages
|
|
426
|
+
for i, msg in enumerate(messages):
|
|
427
|
+
if not isinstance(msg.content, list):
|
|
428
|
+
continue
|
|
429
|
+
blocks = list(msg.content)
|
|
430
|
+
for j, block in enumerate(blocks):
|
|
431
|
+
if not (
|
|
432
|
+
isinstance(block, dict)
|
|
433
|
+
and block.get("type") == "image"
|
|
434
|
+
and isinstance(block.get("base64"), str)
|
|
435
|
+
and block.get("mime_type") not in _PORTABLE_IMAGE_TYPES
|
|
436
|
+
):
|
|
437
|
+
continue
|
|
438
|
+
try:
|
|
439
|
+
jpeg = _as_jpeg(block["base64"])
|
|
440
|
+
blocks[j] = {**block, "base64": jpeg, "mime_type": "image/jpeg"}
|
|
441
|
+
except Exception: # noqa: BLE001 — unreadable: send as is, the provider decides
|
|
442
|
+
logger.warning("could not convert %s", block.get("mime_type"), exc_info=True)
|
|
443
|
+
if blocks != msg.content:
|
|
444
|
+
if out is messages:
|
|
445
|
+
out = list(messages)
|
|
446
|
+
out[i] = msg.model_copy(update={"content": blocks})
|
|
447
|
+
return out
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _with_portable_images(request: ModelRequest) -> ModelRequest:
|
|
451
|
+
messages = _portable_images(request.messages)
|
|
452
|
+
return request if messages is request.messages else request.override(messages=messages)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def _latest_user_text(messages: list[AnyMessage]) -> str:
|
|
456
|
+
"""Return the newest HumanMessage's text (capped), for the caption task hint."""
|
|
457
|
+
for msg in reversed(messages or []):
|
|
458
|
+
if not isinstance(msg, HumanMessage):
|
|
459
|
+
continue
|
|
460
|
+
content = msg.content
|
|
461
|
+
if isinstance(content, str):
|
|
462
|
+
return content[:500]
|
|
463
|
+
if isinstance(content, list):
|
|
464
|
+
texts = [
|
|
465
|
+
b.get("text", "")
|
|
466
|
+
for b in content
|
|
467
|
+
if isinstance(b, dict) and b.get("type") == "text"
|
|
468
|
+
]
|
|
469
|
+
return " ".join(t for t in texts if t)[:500]
|
|
470
|
+
return ""
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class VisionCaptionMiddleware(AgentMiddleware):
|
|
474
|
+
"""Route images safely for text-only and multimodal main models.
|
|
475
|
+
|
|
476
|
+
When ``main_model_supports_images`` is ``True`` (a multimodal main model),
|
|
477
|
+
this middleware is a **pass-through**: images flow straight to the main
|
|
478
|
+
model, which reads them directly. Captioning and stripping are skipped.
|
|
479
|
+
|
|
480
|
+
When it is ``False``, image data stays in graph state for the ``VISION`` tool
|
|
481
|
+
and is stripped from model requests with a note inviting the model to invoke
|
|
482
|
+
that tool. The main model never receives image blocks and never changes.
|
|
483
|
+
"""
|
|
484
|
+
|
|
485
|
+
def __init__(self, main_model_supports_images: bool = False) -> None: # noqa: FBT001, FBT002
|
|
486
|
+
"""Create the middleware.
|
|
487
|
+
|
|
488
|
+
Args:
|
|
489
|
+
main_model_supports_images: ``True`` when the main model can accept
|
|
490
|
+
image input, in which case images are passed through untouched.
|
|
491
|
+
``False`` (default) preserves the on-demand VISION tool path for
|
|
492
|
+
text-only models.
|
|
493
|
+
"""
|
|
494
|
+
super().__init__()
|
|
495
|
+
self.main_model_supports_images = main_model_supports_images
|
|
496
|
+
|
|
497
|
+
@staticmethod
|
|
498
|
+
def get_vision_model() -> BaseChatModel | None:
|
|
499
|
+
"""Expose the shared vision model accessor (back-compat / tests)."""
|
|
500
|
+
return get_vision_model()
|
|
501
|
+
|
|
502
|
+
# -- tool path: retain image results in graph state ----------------------
|
|
503
|
+
|
|
504
|
+
async def awrap_tool_call(
|
|
505
|
+
self,
|
|
506
|
+
request: ToolCallRequest,
|
|
507
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
|
508
|
+
) -> ToolMessage | Command:
|
|
509
|
+
"""Leave image results in graph state; defer inspection to VISION."""
|
|
510
|
+
return await handler(request)
|
|
511
|
+
|
|
512
|
+
def wrap_tool_call(
|
|
513
|
+
self,
|
|
514
|
+
request: ToolCallRequest,
|
|
515
|
+
handler: Callable[[ToolCallRequest], ToolMessage | Command],
|
|
516
|
+
) -> ToolMessage | Command:
|
|
517
|
+
"""Synchronous pass-through; image inspection is handled by VISION."""
|
|
518
|
+
return handler(request)
|
|
519
|
+
|
|
520
|
+
# -- model path: strip-only safety net ----------------------------------
|
|
521
|
+
|
|
522
|
+
async def awrap_model_call(
|
|
523
|
+
self,
|
|
524
|
+
request: ModelRequest,
|
|
525
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
526
|
+
) -> ModelResponse:
|
|
527
|
+
"""Strip residual image blocks before a text-only main model.
|
|
528
|
+
|
|
529
|
+
Pass-through when the main model is multimodal — images are left intact
|
|
530
|
+
for the model to read.
|
|
531
|
+
"""
|
|
532
|
+
if self.main_model_supports_images:
|
|
533
|
+
return await handler(_with_portable_images(request))
|
|
534
|
+
return await handler(
|
|
535
|
+
request.override(messages=_strip_all_images(request.messages))
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
def wrap_model_call(
|
|
539
|
+
self,
|
|
540
|
+
request: ModelRequest,
|
|
541
|
+
handler: Callable[[ModelRequest], ModelResponse],
|
|
542
|
+
) -> ModelResponse:
|
|
543
|
+
"""Synchronous variant of the strip safety net."""
|
|
544
|
+
if self.main_model_supports_images:
|
|
545
|
+
return handler(_with_portable_images(request))
|
|
546
|
+
return handler(request.override(messages=_strip_all_images(request.messages)))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def build_vision_tool():
|
|
550
|
+
"""Create the on-demand VISION tool for a text-only main model.
|
|
551
|
+
|
|
552
|
+
Image bytes remain in graph state but are omitted from the main model's
|
|
553
|
+
request. This tool selects the most recent image-bearing message and sends
|
|
554
|
+
it to the independently configured vision model only after the main model
|
|
555
|
+
explicitly calls it.
|
|
556
|
+
"""
|
|
557
|
+
@tool(
|
|
558
|
+
"VISION",
|
|
559
|
+
description=(
|
|
560
|
+
"Inspect the most recently attached image using the separately configured "
|
|
561
|
+
"vision model. Call only when visual details are needed; returns a textual "
|
|
562
|
+
"description and never changes the main model."
|
|
563
|
+
),
|
|
564
|
+
)
|
|
565
|
+
async def vision(runtime: ToolRuntime) -> str:
|
|
566
|
+
messages = runtime.state.get("messages", [])
|
|
567
|
+
# Do not let a later turn accidentally inspect an image attached in an
|
|
568
|
+
# earlier one. read_file images from this turn follow the latest user
|
|
569
|
+
# message, so they remain eligible alongside pasted attachments.
|
|
570
|
+
turn_start = next(
|
|
571
|
+
(i for i in range(len(messages) - 1, -1, -1) if isinstance(messages[i], HumanMessage)),
|
|
572
|
+
0,
|
|
573
|
+
)
|
|
574
|
+
current_turn = messages[turn_start:]
|
|
575
|
+
for message in reversed(current_turn):
|
|
576
|
+
image_urls = _collect_image_urls(message)
|
|
577
|
+
if not image_urls:
|
|
578
|
+
continue
|
|
579
|
+
task_hint = _latest_user_text(current_turn)
|
|
580
|
+
return await caption_images(image_urls, task_hint)
|
|
581
|
+
return "VISION could not find an attached or recently read image in this turn."
|
|
582
|
+
|
|
583
|
+
return vision
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def is_vision_failure(caption: str) -> bool:
|
|
587
|
+
"""True when *caption* is a failure placeholder rather than a description.
|
|
588
|
+
|
|
589
|
+
The placeholders are ordinary (truthy) strings, so a caller that only tests
|
|
590
|
+
``if captions`` will treat a failed caption as a successful one. Use this to
|
|
591
|
+
tell them apart.
|
|
592
|
+
"""
|
|
593
|
+
return caption.startswith((_VISION_FAILED, _VISION_UNAVAILABLE, _VISION_EMPTY))
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
__all__ = [
|
|
597
|
+
"VisionCaptionMiddleware",
|
|
598
|
+
"VISION_TOOL_TIMEOUT_SECONDS",
|
|
599
|
+
"build_vision_tool",
|
|
600
|
+
"caption_images",
|
|
601
|
+
"get_vision_model",
|
|
602
|
+
"is_vision_failure",
|
|
603
|
+
"reset_vision_model_cache",
|
|
604
|
+
]
|