android-midscene-automation 0.2.4 → 1.1.1
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.
- package/CHANGELOG.md +10 -0
- package/README.md +159 -29
- package/artemis-main/.dockerignore +15 -0
- package/artemis-main/.env.example +34 -0
- package/artemis-main/CONTRIBUTING.md +35 -0
- package/artemis-main/Dockerfile +68 -0
- package/artemis-main/LICENSE +202 -0
- package/artemis-main/Makefile +115 -0
- package/artemis-main/README.md +322 -0
- package/artemis-main/README_CN.md +320 -0
- package/artemis-main/apps/__init__.py +15 -0
- package/artemis-main/apps/admin_console/__init__.py +19 -0
- package/artemis-main/apps/admin_console/core/__init__.py +13 -0
- package/artemis-main/apps/admin_console/core/config.py +43 -0
- package/artemis-main/apps/admin_console/core/security.py +178 -0
- package/artemis-main/apps/admin_console/core/state.py +243 -0
- package/artemis-main/apps/admin_console/database/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/connection.py +60 -0
- package/artemis-main/apps/admin_console/database/repositories/__init__.py +13 -0
- package/artemis-main/apps/admin_console/database/repositories/session_repository.py +564 -0
- package/artemis-main/apps/admin_console/database/repositories/step_repository.py +512 -0
- package/artemis-main/apps/admin_console/database/repositories/trace_repository.py +133 -0
- package/artemis-main/apps/admin_console/index.html +3787 -0
- package/artemis-main/apps/admin_console/replay_manager.py +2585 -0
- package/artemis-main/apps/admin_console/routers/__init__.py +13 -0
- package/artemis-main/apps/admin_console/routers/media.py +198 -0
- package/artemis-main/apps/admin_console/routers/replay.py +91 -0
- package/artemis-main/apps/admin_console/routers/sessions.py +239 -0
- package/artemis-main/apps/admin_console/routers/steps.py +109 -0
- package/artemis-main/apps/admin_console/routers/stream.py +56 -0
- package/artemis-main/apps/admin_console/routers/system.py +761 -0
- package/artemis-main/apps/admin_console/routers/tasks.py +517 -0
- package/artemis-main/apps/admin_console/schemas/__init__.py +13 -0
- package/artemis-main/apps/admin_console/schemas/task_schema.py +47 -0
- package/artemis-main/apps/admin_console/server.py +508 -0
- package/artemis-main/apps/admin_console/services/__init__.py +13 -0
- package/artemis-main/apps/admin_console/services/device_stream_service.py +137 -0
- package/artemis-main/apps/admin_console/services/ipc_service.py +262 -0
- package/artemis-main/apps/admin_console/services/media_service.py +623 -0
- package/artemis-main/apps/admin_console/services/model_service.py +148 -0
- package/artemis-main/apps/admin_console/services/task_preset_catalog.py +514 -0
- package/artemis-main/apps/admin_console/services/task_queue_service.py +1719 -0
- package/artemis-main/apps/admin_console/services/worker_process_io.py +144 -0
- package/artemis-main/apps/showcase_ui/README.md +31 -0
- package/artemis-main/apps/showcase_ui/angular.json +132 -0
- package/artemis-main/apps/showcase_ui/package-lock.json +9472 -0
- package/artemis-main/apps/showcase_ui/package.json +39 -0
- package/artemis-main/apps/showcase_ui/proxy.conf.json +23 -0
- package/artemis-main/apps/showcase_ui/public/favicon.ico +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.png +0 -0
- package/artemis-main/apps/showcase_ui/public/logo.svg +56 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.html +24 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.scss +49 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.spec.ts +42 -0
- package/artemis-main/apps/showcase_ui/src/app/app.component.ts +32 -0
- package/artemis-main/apps/showcase_ui/src/app/app.config.ts +29 -0
- package/artemis-main/apps/showcase_ui/src/app/app.routes.ts +27 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream-startup.spec.ts +108 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.html +1436 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.scss +3973 -0
- package/artemis-main/apps/showcase_ui/src/app/components/agent-stream/agent-stream.component.ts +2235 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.html +380 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.scss +1171 -0
- package/artemis-main/apps/showcase_ui/src/app/components/chat-interface/chat-interface.component.ts +270 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +433 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.scss +987 -0
- package/artemis-main/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.ts +632 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.html +130 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.scss +406 -0
- package/artemis-main/apps/showcase_ui/src/app/components/legacy-agent-stream/legacy-agent-stream.component.ts +74 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.scss +115 -0
- package/artemis-main/apps/showcase_ui/src/app/components/nav-switcher/nav-switcher.component.ts +52 -0
- package/artemis-main/apps/showcase_ui/src/app/core/data/smart-tasks.data.ts +334 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.spec.ts +55 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/language.service.ts +61 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/translate.pipe.ts +11 -0
- package/artemis-main/apps/showcase_ui/src/app/core/i18n/zh-cn.ts +651 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/markdown.model.ts +43 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.spec.ts +47 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/pro-tuning.model.ts +165 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/session.model.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/stream.model.ts +246 -0
- package/artemis-main/apps/showcase_ui/src/app/core/models/system.model.ts +118 -0
- package/artemis-main/apps/showcase_ui/src/app/core/services/task-recommendation.service.ts +111 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.html +2130 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.scss +5204 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/home/home.component.ts +1189 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.html +39 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.scss +151 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/legacy-workspace/legacy-workspace.component.ts +58 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.html +201 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.scss +886 -0
- package/artemis-main/apps/showcase_ui/src/app/pages/workspace/workspace.component.ts +324 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.spec.ts +418 -0
- package/artemis-main/apps/showcase_ui/src/app/services/agent.service.ts +2012 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.spec.ts +173 -0
- package/artemis-main/apps/showcase_ui/src/app/services/system.service.ts +620 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.spec.ts +513 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/action-formatter.util.ts +1015 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.spec.ts +85 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/image-overlay.util.ts +662 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.spec.ts +70 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/markdown-parser.util.ts +383 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.spec.ts +107 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/recording-timeline.util.ts +123 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.spec.ts +92 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/run-info.util.ts +116 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.spec.ts +640 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/stream-aggregator.util.ts +1021 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.spec.ts +375 -0
- package/artemis-main/apps/showcase_ui/src/app/utils/tool-formatter.util.ts +1419 -0
- package/artemis-main/apps/showcase_ui/src/index.html +37 -0
- package/artemis-main/apps/showcase_ui/src/main.ts +22 -0
- package/artemis-main/apps/showcase_ui/src/styles.scss +60 -0
- package/artemis-main/apps/showcase_ui/tsconfig.app.json +23 -0
- package/artemis-main/apps/showcase_ui/tsconfig.json +27 -0
- package/artemis-main/apps/showcase_ui/tsconfig.spec.json +23 -0
- package/artemis-main/artemis/__init__.py +44 -0
- package/artemis-main/artemis/__main__.py +18 -0
- package/artemis-main/artemis/_legacy_imports.py +94 -0
- package/artemis-main/artemis/_version.py +30 -0
- package/artemis-main/artemis/agents/__init__.py +17 -0
- package/artemis-main/artemis/agents/checker/checker.json +9 -0
- package/artemis-main/artemis/agents/checker/checker.py +859 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.json +23 -0
- package/artemis-main/artemis/agents/diagnoser/diagnoser.py +608 -0
- package/artemis-main/artemis/agents/explorer/constants.py +52 -0
- package/artemis-main/artemis/agents/explorer/explorer.json +57 -0
- package/artemis-main/artemis/agents/explorer/explorer.py +473 -0
- package/artemis-main/artemis/agents/explorer/geometry.py +73 -0
- package/artemis-main/artemis/agents/explorer/native_runner.py +878 -0
- package/artemis-main/artemis/agents/explorer/perception_tools.py +632 -0
- package/artemis-main/artemis/agents/explorer/run_setup.py +403 -0
- package/artemis-main/artemis/agents/explorer/screen_index.py +266 -0
- package/artemis-main/artemis/agents/explorer/tiers.py +110 -0
- package/artemis-main/artemis/agents/explorer/tool_declarations.py +394 -0
- package/artemis-main/artemis/agents/explorer/universal_runner.py +317 -0
- package/artemis-main/artemis/agents/flash/context_compressor.py +452 -0
- package/artemis-main/artemis/agents/flash/flash_runner.md +51 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer.md +53 -0
- package/artemis-main/artemis/agents/flash/flash_summarizer_single.md +54 -0
- package/artemis-main/artemis/agents/flash/runner.py +1167 -0
- package/artemis-main/artemis/agents/flash/summarizer.py +511 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.md +8 -0
- package/artemis-main/artemis/agents/history_analyzer/history_analyzer.py +182 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.md +74 -0
- package/artemis-main/artemis/agents/image_processor/image_processor.py +322 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.md +15 -0
- package/artemis-main/artemis/agents/log_analyzer/log_analyzer.py +403 -0
- package/artemis-main/artemis/agents/log_analyzer/log_reader_sub_agent.md +8 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.md +11 -0
- package/artemis-main/artemis/agents/log_analyzer/output_analyzer.py +163 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.json +9 -0
- package/artemis-main/artemis/agents/object_detector/object_detector.py +211 -0
- package/artemis-main/artemis/agents/operator/operator.json +3 -0
- package/artemis-main/artemis/agents/operator/operator.py +1394 -0
- package/artemis-main/artemis/agents/operator/prompts.py +1064 -0
- package/artemis-main/artemis/agents/outputter/outputter.md +36 -0
- package/artemis-main/artemis/agents/outputter/outputter.py +552 -0
- package/artemis-main/artemis/agents/planner/planner.json +32 -0
- package/artemis-main/artemis/agents/planner/planner.py +436 -0
- package/artemis-main/artemis/agents/prompt_assembly.py +79 -0
- package/artemis-main/artemis/agents/summarizer/summarizer.py +190 -0
- package/artemis-main/artemis/agents/validator/action_execution.py +98 -0
- package/artemis-main/artemis/agents/validator/categories.py +33 -0
- package/artemis-main/artemis/agents/validator/execution_loop.py +488 -0
- package/artemis-main/artemis/agents/validator/incidents.py +120 -0
- package/artemis-main/artemis/agents/validator/pixel_safety_net.md +15 -0
- package/artemis-main/artemis/agents/validator/precondition_pixel.py +320 -0
- package/artemis-main/artemis/agents/validator/precondition_xml.py +672 -0
- package/artemis-main/artemis/agents/validator/tool_declarations.py +332 -0
- package/artemis-main/artemis/agents/validator/validator.py +188 -0
- package/artemis-main/artemis/agents/video_analyzer/__init__.py +15 -0
- package/artemis-main/artemis/agents/video_analyzer/audio_native.py +543 -0
- package/artemis-main/artemis/agents/video_analyzer/blackboard.py +821 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_agentic.py +455 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_conversation.py +491 -0
- package/artemis-main/artemis/agents/video_analyzer/chunk_native.py +437 -0
- package/artemis-main/artemis/agents/video_analyzer/conflict_resolution.py +137 -0
- package/artemis-main/artemis/agents/video_analyzer/gemini_files.py +162 -0
- package/artemis-main/artemis/agents/video_analyzer/native_coordinator.py +541 -0
- package/artemis-main/artemis/agents/video_analyzer/reliability.py +128 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_engine.py +487 -0
- package/artemis-main/artemis/agents/video_analyzer/universal_tools.py +323 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.md +51 -0
- package/artemis-main/artemis/agents/video_analyzer/video_analyzer.py +875 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent.md +15 -0
- package/artemis-main/artemis/agents/video_analyzer/video_sub_agent_audio.md +12 -0
- package/artemis-main/artemis/clients/accessibility_client.py +438 -0
- package/artemis-main/artemis/clients/screen_client_factory.py +403 -0
- package/artemis-main/artemis/config/__init__.py +309 -0
- package/artemis-main/artemis/config/agent.py +1098 -0
- package/artemis-main/artemis/config/constants.py +151 -0
- package/artemis-main/artemis/config/core/__init__.py +24 -0
- package/artemis-main/artemis/config/core/engine.py +118 -0
- package/artemis-main/artemis/config/core/interpolator.py +78 -0
- package/artemis-main/artemis/config/llm.py +267 -0
- package/artemis-main/artemis/config/paths.py +244 -0
- package/artemis-main/artemis/config/runtime.py +197 -0
- package/artemis-main/artemis/config/settings.py +315 -0
- package/artemis-main/artemis/constants.py +47 -0
- package/artemis-main/artemis/context.py +289 -0
- package/artemis-main/artemis/controllers/__init__.py +41 -0
- package/artemis-main/artemis/controllers/controller_factory.py +41 -0
- package/artemis-main/artemis/controllers/device_controller.py +29 -0
- package/artemis-main/artemis/controllers/types.py +37 -0
- package/artemis-main/artemis/controllers/unified_controller.py +649 -0
- package/artemis-main/artemis/core/__init__.py +21 -0
- package/artemis-main/artemis/core/diagnostics/__init__.py +90 -0
- package/artemis-main/artemis/core/diagnostics/adb_keys.py +270 -0
- package/artemis-main/artemis/core/diagnostics/adb_server_connection.py +330 -0
- package/artemis-main/artemis/core/diagnostics/device_smoke.py +364 -0
- package/artemis-main/artemis/core/diagnostics/emulator_manager.py +501 -0
- package/artemis-main/artemis/core/diagnostics/engine.py +438 -0
- package/artemis-main/artemis/core/diagnostics/hierarchy_parity.py +216 -0
- package/artemis-main/artemis/core/diagnostics/probes/adb_probe.py +887 -0
- package/artemis-main/artemis/core/diagnostics/probes/base.py +45 -0
- package/artemis-main/artemis/core/diagnostics/probes/credentials_probe.py +296 -0
- package/artemis-main/artemis/core/diagnostics/probes/host_probe.py +427 -0
- package/artemis-main/artemis/core/diagnostics/probes/runtime_probe.py +169 -0
- package/artemis-main/artemis/core/diagnostics/probes/toolchain_probe.py +139 -0
- package/artemis-main/artemis/core/diagnostics/readiness.py +83 -0
- package/artemis-main/artemis/core/diagnostics/schema.py +118 -0
- package/artemis-main/artemis/core/tool_declaration.py +43 -0
- package/artemis-main/artemis/core/tool_failure.py +56 -0
- package/artemis-main/artemis/data_engine/context_vars.py +24 -0
- package/artemis-main/artemis/data_engine/engine.py +1615 -0
- package/artemis-main/artemis/data_engine/history_reader.py +142 -0
- package/artemis-main/artemis/data_engine/models.py +131 -0
- package/artemis-main/artemis/data_engine/storage.py +1245 -0
- package/artemis-main/artemis/data_engine/trace.py +727 -0
- package/artemis-main/artemis/drivers/__init__.py +48 -0
- package/artemis-main/artemis/drivers/android/adb_driver.py +507 -0
- package/artemis-main/artemis/drivers/android/hierarchy.py +64 -0
- package/artemis-main/artemis/drivers/android/input_ime.py +120 -0
- package/artemis-main/artemis/drivers/android/recorder.py +57 -0
- package/artemis-main/artemis/drivers/base.py +290 -0
- package/artemis-main/artemis/drivers/cloud/cloud_driver.py +117 -0
- package/artemis-main/artemis/drivers/factory.py +80 -0
- package/artemis-main/artemis/drivers/mock/mock_driver.py +198 -0
- package/artemis-main/artemis/drivers/types.py +67 -0
- package/artemis-main/artemis/graph/checkpoints.py +1243 -0
- package/artemis-main/artemis/graph/graph.py +1055 -0
- package/artemis-main/artemis/graph/perception.py +234 -0
- package/artemis-main/artemis/graph/state.py +151 -0
- package/artemis-main/artemis/graph/visibility.py +238 -0
- package/artemis-main/artemis/interfaces/cli/__init__.py +19 -0
- package/artemis-main/artemis/interfaces/cli/commands/__init__.py +15 -0
- package/artemis-main/artemis/interfaces/cli/commands/batch.py +278 -0
- package/artemis-main/artemis/interfaces/cli/commands/doctor.py +468 -0
- package/artemis-main/artemis/interfaces/cli/commands/helper.py +226 -0
- package/artemis-main/artemis/interfaces/cli/commands/init.py +193 -0
- package/artemis-main/artemis/interfaces/cli/commands/mcp.py +862 -0
- package/artemis-main/artemis/interfaces/cli/commands/run.py +463 -0
- package/artemis-main/artemis/interfaces/cli/commands/server.py +151 -0
- package/artemis-main/artemis/interfaces/cli/commands/server_lifecycle.py +328 -0
- package/artemis-main/artemis/interfaces/cli/commands/trace.py +111 -0
- package/artemis-main/artemis/interfaces/cli/commands/ui.py +404 -0
- package/artemis-main/artemis/interfaces/cli/main.py +109 -0
- package/artemis-main/artemis/interfaces/sdk/__init__.py +31 -0
- package/artemis-main/artemis/interfaces/sdk/client.py +19 -0
- package/artemis-main/artemis/interfaces/sdk/task.py +72 -0
- package/artemis-main/artemis/llm/__init__.py +25 -0
- package/artemis-main/artemis/llm/fake_model.py +179 -0
- package/artemis-main/artemis/llm/google/__init__.py +57 -0
- package/artemis-main/artemis/llm/google/provider.py +185 -0
- package/artemis-main/artemis/llm/google/usage.py +148 -0
- package/artemis-main/artemis/llm/parser.py +32 -0
- package/artemis-main/artemis/llm/reliability.py +267 -0
- package/artemis-main/artemis/llm/router.py +389 -0
- package/artemis-main/artemis/llm/schemas.py +36 -0
- package/artemis-main/artemis/llm/structured.py +196 -0
- package/artemis-main/artemis/main.py +58 -0
- package/artemis-main/artemis/mcp/__init__.py +20 -0
- package/artemis-main/artemis/mcp/action_executor.py +760 -0
- package/artemis-main/artemis/mcp/action_manifest.py +320 -0
- package/artemis-main/artemis/mcp/action_names.py +172 -0
- package/artemis-main/artemis/mcp/action_server.py +202 -0
- package/artemis-main/artemis/mcp/action_session.py +245 -0
- package/artemis-main/artemis/mcp/action_specs.py +868 -0
- package/artemis-main/artemis/mcp/action_types.py +86 -0
- package/artemis-main/artemis/mcp/actuators/__init__.py +21 -0
- package/artemis-main/artemis/mcp/actuators/adb.py +414 -0
- package/artemis-main/artemis/mcp/actuators/base.py +113 -0
- package/artemis-main/artemis/mcp/actuators/mock.py +70 -0
- package/artemis-main/artemis/mcp/adb_server.py +490 -0
- package/artemis-main/artemis/mcp/observation.py +133 -0
- package/artemis-main/artemis/mcp/xml_search_server.py +297 -0
- package/artemis-main/artemis/memory/__init__.py +90 -0
- package/artemis-main/artemis/memory/chunking.py +1843 -0
- package/artemis-main/artemis/memory/context_policy.py +226 -0
- package/artemis-main/artemis/memory/step_capsule.md +73 -0
- package/artemis-main/artemis/memory/step_memory.py +281 -0
- package/artemis-main/artemis/memory/transcript.py +798 -0
- package/artemis-main/artemis/platform/__init__.py +61 -0
- package/artemis-main/artemis/platform/base.py +104 -0
- package/artemis-main/artemis/platform/darwin.py +184 -0
- package/artemis-main/artemis/platform/linux.py +207 -0
- package/artemis-main/artemis/platform/windows.py +193 -0
- package/artemis-main/artemis/resources/__init__.py +46 -0
- package/artemis-main/artemis/runtime/__init__.py +108 -0
- package/artemis-main/artemis/runtime/adb_endpoint.py +173 -0
- package/artemis-main/artemis/runtime/awake_lease.py +283 -0
- package/artemis-main/artemis/runtime/awake_service.py +343 -0
- package/artemis-main/artemis/runtime/cancel_requests.py +254 -0
- package/artemis-main/artemis/runtime/daemon_client.py +422 -0
- package/artemis-main/artemis/runtime/device_lock.py +1013 -0
- package/artemis-main/artemis/runtime/device_pool.py +478 -0
- package/artemis-main/artemis/runtime/helper_manager.py +1030 -0
- package/artemis-main/artemis/runtime/process_probe.py +98 -0
- package/artemis-main/artemis/runtime/server_lifecycle.py +435 -0
- package/artemis-main/artemis/runtime/supervisor.py +121 -0
- package/artemis-main/artemis/runtime/trace_store.py +375 -0
- package/artemis-main/artemis/sdk/__init__.py +26 -0
- package/artemis-main/artemis/sdk/agent.py +485 -0
- package/artemis-main/artemis/sdk/builders/__init__.py +29 -0
- package/artemis-main/artemis/sdk/builders/agent_config_builder.py +474 -0
- package/artemis-main/artemis/sdk/constants.py +15 -0
- package/artemis-main/artemis/sdk/run_outcome.py +58 -0
- package/artemis-main/artemis/sdk/types/__init__.py +61 -0
- package/artemis-main/artemis/sdk/types/agent.py +89 -0
- package/artemis-main/artemis/sdk/utils.py +28 -0
- package/artemis-main/artemis/services/llm.py +1040 -0
- package/artemis-main/artemis/services/token_meter.py +267 -0
- package/artemis-main/artemis/telemetry/__init__.py +35 -0
- package/artemis-main/artemis/telemetry/models.py +64 -0
- package/artemis-main/artemis/telemetry/replay.py +34 -0
- package/artemis-main/artemis/telemetry/storage/json_storage.py +47 -0
- package/artemis-main/artemis/telemetry/tracer.py +85 -0
- package/artemis-main/artemis/toolchain/__init__.py +52 -0
- package/artemis-main/artemis/toolchain/descriptors.py +92 -0
- package/artemis-main/artemis/toolchain/resolver.py +185 -0
- package/artemis-main/artemis/tools/base.py +232 -0
- package/artemis-main/artemis/tools/command_tool.py +1240 -0
- package/artemis-main/artemis/tools/committee_prompts.json +5 -0
- package/artemis-main/artemis/tools/committee_tool.py +422 -0
- package/artemis-main/artemis/tools/diagnoser_submit_answer_tool.py +87 -0
- package/artemis-main/artemis/tools/diagnostic_tool.py +120 -0
- package/artemis-main/artemis/tools/explorer_tool.py +581 -0
- package/artemis-main/artemis/tools/history/__init__.py +398 -0
- package/artemis-main/artemis/tools/history/replay.py +120 -0
- package/artemis-main/artemis/tools/history/screenshot.py +139 -0
- package/artemis-main/artemis/tools/history/search.py +563 -0
- package/artemis-main/artemis/tools/image_processor_tool.py +124 -0
- package/artemis-main/artemis/tools/index.py +47 -0
- package/artemis-main/artemis/tools/log_tool.py +108 -0
- package/artemis-main/artemis/tools/mobile/launch_app.py +132 -0
- package/artemis-main/artemis/tools/mobile/log_utils.py +138 -0
- package/artemis-main/artemis/tools/mobile/ocr.py +202 -0
- package/artemis-main/artemis/tools/mobile/read_hierarchy.py +108 -0
- package/artemis-main/artemis/tools/mobile/read_logs.py +116 -0
- package/artemis-main/artemis/tools/mobile/search_logs.py +260 -0
- package/artemis-main/artemis/tools/object_detection_tool.py +244 -0
- package/artemis-main/artemis/tools/scratchpad.py +819 -0
- package/artemis-main/artemis/tools/system/adb_shell.py +31 -0
- package/artemis-main/artemis/tools/tool_wrapper.py +270 -0
- package/artemis-main/artemis/tools/video_tool.py +261 -0
- package/artemis-main/artemis/tools/wait_tool.py +97 -0
- package/artemis-main/artemis/utils/app_launch_utils.py +207 -0
- package/artemis-main/artemis/utils/coordinates.py +606 -0
- package/artemis-main/artemis/utils/credentials_validator.py +201 -0
- package/artemis-main/artemis/utils/cv_canvas.py +189 -0
- package/artemis-main/artemis/utils/cython_compat.py +45 -0
- package/artemis-main/artemis/utils/element_hit_test.py +80 -0
- package/artemis-main/artemis/utils/image_diff.py +335 -0
- package/artemis-main/artemis/utils/image_hash.py +80 -0
- package/artemis-main/artemis/utils/notes.py +470 -0
- package/artemis-main/artemis/utils/ocr_api.py +105 -0
- package/artemis-main/artemis/utils/ocr_xml_fusion.py +318 -0
- package/artemis-main/artemis/utils/plan_grammar.py +513 -0
- package/artemis-main/artemis/utils/python_executor.py +213 -0
- package/artemis-main/artemis/utils/startup_progress.py +60 -0
- package/artemis-main/artemis/utils/task_tree.py +1174 -0
- package/artemis-main/artemis/utils/text.py +37 -0
- package/artemis-main/artemis/utils/ui_filter.py +651 -0
- package/artemis-main/artemis/utils/video.py +685 -0
- package/artemis-main/artemis/utils/visualization.py +802 -0
- package/artemis-main/config/artemis.example.jsonc +12 -0
- package/artemis-main/mcp_server/README.md +111 -0
- package/artemis-main/mcp_server/__init__.py +21 -0
- package/artemis-main/mcp_server/__main__.py +20 -0
- package/artemis-main/mcp_server/background/__init__.py +15 -0
- package/artemis-main/mcp_server/background/task_runner.py +453 -0
- package/artemis-main/mcp_server/base.py +49 -0
- package/artemis-main/mcp_server/notifiers/__init__.py +74 -0
- package/artemis-main/mcp_server/notifiers/agentapi.py +289 -0
- package/artemis-main/mcp_server/notifiers/base.py +45 -0
- package/artemis-main/mcp_server/notifiers/composite.py +87 -0
- package/artemis-main/mcp_server/notifiers/desktop.py +108 -0
- package/artemis-main/mcp_server/notifiers/file.py +69 -0
- package/artemis-main/mcp_server/notifiers/script.py +90 -0
- package/artemis-main/mcp_server/notifiers/webhook.py +87 -0
- package/artemis-main/mcp_server/rules.md +68 -0
- package/artemis-main/mcp_server/server.py +106 -0
- package/artemis-main/mcp_server/tools/__init__.py +29 -0
- package/artemis-main/mcp_server/tools/device_state.py +107 -0
- package/artemis-main/mcp_server/tools/diagnose.py +1249 -0
- package/artemis-main/mcp_server/tools/inspect_trace.py +394 -0
- package/artemis-main/mcp_server/tools/task_manager.py +627 -0
- package/artemis-main/mcp_server/tools/task_runner.py +548 -0
- package/artemis-main/mcp_server/utils/__init__.py +15 -0
- package/artemis-main/mcp_server/utils/device_utils.py +180 -0
- package/artemis-main/mcp_server/utils/env_utils.py +65 -0
- package/artemis-main/packages/artemis-accessibility-helper/ArtemisAccessibilityHelper.apk +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/build.gradle.kts +35 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/AndroidManifest.xml +49 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/A11yNode.java +274 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/ArtemisAccessibilityService.java +227 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/CommandServer.java +463 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/DisplayUtils.java +111 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/GestureController.java +232 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/HierarchyDumper.java +832 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenReceiver.java +36 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/TokenStore.java +38 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/java/com/artemis/helper/XmlUtils.java +102 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/values/strings.xml +4 -0
- package/artemis-main/packages/artemis-accessibility-helper/app/src/main/res/xml/accessibility_service_config.xml +10 -0
- package/artemis-main/packages/artemis-accessibility-helper/build.gradle.kts +3 -0
- package/artemis-main/packages/artemis-accessibility-helper/build_apk.sh +215 -0
- package/artemis-main/packages/artemis-accessibility-helper/debug.keystore +0 -0
- package/artemis-main/packages/artemis-accessibility-helper/gradle.properties +2 -0
- package/artemis-main/packages/artemis-accessibility-helper/helper_manifest.json +7 -0
- package/artemis-main/packages/artemis-accessibility-helper/settings.gradle.kts +17 -0
- package/artemis-main/packages/artemis-client/DESIGN.md +89 -0
- package/artemis-main/packages/artemis-client/README.md +112 -0
- package/artemis-main/packages/artemis-client/examples/basic.py +30 -0
- package/artemis-main/packages/artemis-client/pyproject.toml +36 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/__init__.py +45 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/client.py +401 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/errors.py +66 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/models.py +212 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/py.typed +1 -0
- package/artemis-main/packages/artemis-client/src/artemis_client/transport.py +134 -0
- package/artemis-main/packages/artemis-client/tests/test_client.py +289 -0
- package/artemis-main/packages/artemis-client/tests/test_transport.py +85 -0
- package/artemis-main/pyproject.toml +259 -0
- package/artemis-main/pyright-core.json +17 -0
- package/artemis-main/scripts/check_dependency_sources.py +68 -0
- package/artemis-main/scripts/install_deps.ps1 +520 -0
- package/artemis-main/scripts/install_deps.sh +533 -0
- package/artemis-main/scripts/quality_ratchet.py +97 -0
- package/artemis-main/scripts/real_device_video_acceptance.py +483 -0
- package/artemis-main/scripts/run_edge_case_part.py +327 -0
- package/artemis-main/scripts/run_ui.py +242 -0
- package/artemis-main/scripts/start.ps1 +500 -0
- package/artemis-main/scripts/test_all_edge_cases.py +269 -0
- package/artemis-main/scripts/verify_concurrency_and_devices.py +385 -0
- package/artemis-main/setup.py +164 -0
- package/artemis-main/start.bat +35 -0
- package/artemis-main/start.sh +423 -0
- package/artemis-main/third_party/mobile_use/LICENSE +190 -0
- package/artemis-main/third_party/mobile_use/METADATA +27 -0
- package/artemis-main/third_party/mobile_use/NOTICE +12 -0
- package/artemis-main/third_party/mobile_use/__init__.py +19 -0
- package/artemis-main/third_party/mobile_use/agents/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.md +22 -0
- package/artemis-main/third_party/mobile_use/agents/hopper/hopper.py +80 -0
- package/artemis-main/third_party/mobile_use/clients/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/clients/ui_automator_client.py +482 -0
- package/artemis-main/third_party/mobile_use/config/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/config/llm.py +183 -0
- package/artemis-main/third_party/mobile_use/config/output.py +184 -0
- package/artemis-main/third_party/mobile_use/context.py +119 -0
- package/artemis-main/third_party/mobile_use/controllers/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/controllers/platform_specific_commands_controller.py +151 -0
- package/artemis-main/third_party/mobile_use/controllers/types.py +92 -0
- package/artemis-main/third_party/mobile_use/controllers/unified_controller.py +183 -0
- package/artemis-main/third_party/mobile_use/graph.py +54 -0
- package/artemis-main/third_party/mobile_use/llm_service.py +193 -0
- package/artemis-main/third_party/mobile_use/main.py +130 -0
- package/artemis-main/third_party/mobile_use/sdk/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/agent.py +1060 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/agent_config_builder.py +225 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/index.py +33 -0
- package/artemis-main/third_party/mobile_use/sdk/builders/task_request_builder.py +243 -0
- package/artemis-main/third_party/mobile_use/sdk/types/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/sdk/types/agent.py +98 -0
- package/artemis-main/third_party/mobile_use/sdk/types/exceptions.py +109 -0
- package/artemis-main/third_party/mobile_use/sdk/types/task.py +234 -0
- package/artemis-main/third_party/mobile_use/testing.py +59 -0
- package/artemis-main/third_party/mobile_use/tools/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/index.py +44 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/tools/mobile/launch_app.py +89 -0
- package/artemis-main/third_party/mobile_use/tools/tool_wrapper.py +52 -0
- package/artemis-main/third_party/mobile_use/utils/__init__.py +13 -0
- package/artemis-main/third_party/mobile_use/utils/app_launch_utils.py +217 -0
- package/artemis-main/third_party/mobile_use/utils/cli_helpers.py +60 -0
- package/artemis-main/third_party/mobile_use/utils/decorators.py +158 -0
- package/artemis-main/third_party/mobile_use/utils/file.py +31 -0
- package/artemis-main/third_party/mobile_use/utils/logger.py +216 -0
- package/artemis-main/third_party/mobile_use/utils/media.py +213 -0
- package/artemis-main/third_party/mobile_use/utils/shell_utils.py +39 -0
- package/artemis-main/third_party/mobile_use/utils/video.py +398 -0
- package/artemis-main/uv.lock +4908 -0
- package/bin/android-midscene-automation.js +1 -5
- package/bin/artemis-config.mjs +15 -0
- package/bin/artemis-embedded.py +56 -0
- package/bin/start-artemis.mjs +30 -0
- package/bin/start-dev.mjs +112 -0
- package/bin/test_artemis_embedded.py +44 -0
- package/package.json +4 -2
- package/server/artemis.ts +34 -0
- package/server/http-api.ts +12 -0
- package/src/App.vue +10 -5
- package/src/api.ts +5 -0
- package/src/appium-recorder/AppiumPage.vue +1 -1
- package/src/appium-recorder/components/ComponentInspector.vue +1 -1
- package/src/appium-recorder/components/FlowNodeSearch.vue +3 -3
- package/src/appium-recorder/components/ImageCheckDialog.vue +3 -3
- package/src/appium-recorder/components/RecorderWorkspace.vue +4 -4
- package/src/appium-recorder/flow-appearance.ts +4 -4
- package/src/components/AppSidebar.vue +8 -7
- package/src/components/MidsceneMaintenanceNotice.vue +17 -0
- package/src/components/analytics/AnalysisCharts.vue +4 -4
- package/src/components/analytics/ReportAnalysis.vue +1 -1
- package/src/components/generator/TestCaseFileUpload.vue +1 -1
- package/src/components/help/HelpCenter.vue +56 -37
- package/src/pages/AiGeneratorPage.vue +1 -1
- package/src/pages/ArtemisPage.vue +72 -0
- package/src/pages/ConfigPage.vue +18 -19
- package/src/style.css +340 -121
- package/src/types.ts +1 -1
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
# Copyright 2026 Google LLC
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""VideoAnalyzer facade: entry point and shared runtime state.
|
|
16
|
+
|
|
17
|
+
The execution engines live in sibling modules (``chunk_native``,
|
|
18
|
+
``audio_native``, ``universal_engine``, ``native_coordinator``,
|
|
19
|
+
``gemini_files``, ``universal_tools``) and resolve their patchable
|
|
20
|
+
collaborators through THIS module's namespace, so existing
|
|
21
|
+
``mock.patch("artemis.agents.video_analyzer.video_analyzer.<name>")``
|
|
22
|
+
targets keep working. Keep the imports below in place even when the facade
|
|
23
|
+
body no longer references them directly.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import asyncio
|
|
27
|
+
import base64
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
import glob
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
import re
|
|
34
|
+
import shutil
|
|
35
|
+
import tempfile
|
|
36
|
+
import time
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
from google import genai
|
|
40
|
+
from google.genai import types
|
|
41
|
+
from langchain_core.messages import (
|
|
42
|
+
BaseMessage,
|
|
43
|
+
HumanMessage,
|
|
44
|
+
SystemMessage,
|
|
45
|
+
ToolMessage,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
from artemis.agents.video_analyzer.conflict_resolution import (
|
|
49
|
+
ConflictResolutionService,
|
|
50
|
+
)
|
|
51
|
+
from artemis.agents.video_analyzer.blackboard import get_video_blackboard
|
|
52
|
+
from artemis.agents.video_analyzer.gemini_files import (
|
|
53
|
+
cleanup_abandoned_gemini_files,
|
|
54
|
+
)
|
|
55
|
+
from artemis.agents.video_analyzer.reliability import (
|
|
56
|
+
AgenticVideoDegraded,
|
|
57
|
+
VideoCircuitBreaker,
|
|
58
|
+
classify_video_failure,
|
|
59
|
+
)
|
|
60
|
+
from artemis.agents.video_analyzer.universal_tools import (
|
|
61
|
+
UNIVERSAL_MAIN_TOOLS,
|
|
62
|
+
UNIVERSAL_SUBMIT_ANSWER_TOOL,
|
|
63
|
+
build_native_tools_declaration,
|
|
64
|
+
build_submit_answer_declaration,
|
|
65
|
+
)
|
|
66
|
+
from artemis.config import get_temp_dir, settings
|
|
67
|
+
from artemis.constants import SAFETY_SETTINGS_BLOCK_NONE
|
|
68
|
+
from artemis.context import ArtemisContext
|
|
69
|
+
from artemis.controllers.controller_factory import get_controller
|
|
70
|
+
from artemis.data_engine.trace import CURRENT_TRACE_ID, TraceSpan, trace
|
|
71
|
+
from artemis.llm.google import (
|
|
72
|
+
is_gemini_model,
|
|
73
|
+
resolve_video_processing,
|
|
74
|
+
strip_provider_prefix,
|
|
75
|
+
)
|
|
76
|
+
from artemis.llm.reliability import retry_policy_for
|
|
77
|
+
from artemis.services.llm import _record_llm_event, _record_llm_retry, get_llm
|
|
78
|
+
from artemis.utils.video import extract_frames_at_timestamps, extract_keyframes_from_video
|
|
79
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
80
|
+
from third_party.mobile_use.utils.video import (
|
|
81
|
+
compress_video_for_api,
|
|
82
|
+
extract_audio_from_video,
|
|
83
|
+
get_active_session,
|
|
84
|
+
probe_duration,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
from datetime import UTC
|
|
89
|
+
except ImportError:
|
|
90
|
+
UTC = UTC
|
|
91
|
+
|
|
92
|
+
logger = get_logger(__name__)
|
|
93
|
+
|
|
94
|
+
TRANSCODE_SEMAPHORE = asyncio.Semaphore(2)
|
|
95
|
+
MAIN_AGENT_SEMAPHORE = asyncio.Semaphore(2)
|
|
96
|
+
API_SEMAPHORE = asyncio.Semaphore(5)
|
|
97
|
+
SLOWDOWN_THRESHOLD_SECONDS = 30.0
|
|
98
|
+
SLOWDOWN_FACTOR = 2.0
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def _invoke_with_retry(operation, label: str, max_attempts: int = 3):
|
|
102
|
+
"""Retry side-effect-free model coordination calls using shared classification.
|
|
103
|
+
|
|
104
|
+
Retry decisions come from ``classify_video_failure`` plus the shared
|
|
105
|
+
per-category policies in :mod:`artemis.llm.reliability` instead of ad-hoc
|
|
106
|
+
status-code sets. ``operation`` must be restartable from scratch: each
|
|
107
|
+
retry re-invokes it and any partially accumulated result (e.g. consumed
|
|
108
|
+
stream chunks) from the failed attempt is discarded, never surfaced as a
|
|
109
|
+
complete output. The original exception is re-raised on giving up so
|
|
110
|
+
downstream ``classify_video_failure`` consumers keep seeing the provider's
|
|
111
|
+
native error shape.
|
|
112
|
+
"""
|
|
113
|
+
attempt = 0
|
|
114
|
+
while True:
|
|
115
|
+
attempt += 1
|
|
116
|
+
try:
|
|
117
|
+
return await operation()
|
|
118
|
+
except Exception as exc:
|
|
119
|
+
failure = classify_video_failure(exc)
|
|
120
|
+
if not failure.retryable:
|
|
121
|
+
logger.warning(
|
|
122
|
+
f"{label} non-retryable failure [{failure.category.value}] "
|
|
123
|
+
f"on attempt {attempt}: {exc}"
|
|
124
|
+
)
|
|
125
|
+
raise
|
|
126
|
+
policy = retry_policy_for(failure.category)
|
|
127
|
+
allowed_attempts = min(max_attempts, policy.max_attempts)
|
|
128
|
+
if attempt >= allowed_attempts:
|
|
129
|
+
logger.warning(
|
|
130
|
+
f"{label} exhausted {attempt} attempt(s) [{failure.category.value}]: {exc}"
|
|
131
|
+
)
|
|
132
|
+
raise
|
|
133
|
+
delay = policy.delay_for(attempt)
|
|
134
|
+
logger.warning(
|
|
135
|
+
f"{label} retryable failure [{failure.category.value}] "
|
|
136
|
+
f"({attempt}/{allowed_attempts}): {exc}; retrying from scratch "
|
|
137
|
+
f"in {delay:.2f}s (partial output from the failed attempt is discarded)"
|
|
138
|
+
)
|
|
139
|
+
_record_llm_retry(
|
|
140
|
+
str(exc),
|
|
141
|
+
delay,
|
|
142
|
+
attempt=attempt,
|
|
143
|
+
max_retries=allowed_attempts,
|
|
144
|
+
source=label,
|
|
145
|
+
)
|
|
146
|
+
await asyncio.sleep(delay)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class VideoAnalyzer:
|
|
150
|
+
def __init__(self, ctx: ArtemisContext):
|
|
151
|
+
self.ctx = ctx
|
|
152
|
+
self.blackboard = get_video_blackboard(ctx)
|
|
153
|
+
self.blackboard_entries = self.blackboard.list_ledger_entries()
|
|
154
|
+
agent_config = getattr(ctx, "agent_config", None)
|
|
155
|
+
self.video_config = getattr(agent_config, "video_analyzer", None)
|
|
156
|
+
self.enable_ledger = getattr(
|
|
157
|
+
self.video_config,
|
|
158
|
+
"enable_ledger",
|
|
159
|
+
getattr(agent_config, "enable_video_ledger", True),
|
|
160
|
+
)
|
|
161
|
+
self.static_chunk_size_seconds = float(
|
|
162
|
+
getattr(self.video_config, "chunk_size_seconds", 60.0)
|
|
163
|
+
)
|
|
164
|
+
self.agentic_chunk_size_seconds = float(
|
|
165
|
+
getattr(self.video_config, "agentic_chunk_size_seconds", 600.0)
|
|
166
|
+
)
|
|
167
|
+
self.agentic_call_timeout_seconds = float(
|
|
168
|
+
getattr(self.video_config, "agentic_call_timeout_seconds", 300.0)
|
|
169
|
+
)
|
|
170
|
+
# Effective chunk ceiling; re-derived once the engine picks a
|
|
171
|
+
# video-processing mode (agentic sub-agents take long clips).
|
|
172
|
+
self.chunk_size_seconds = self.static_chunk_size_seconds
|
|
173
|
+
self.video_processing = "static"
|
|
174
|
+
self.min_chunk_seconds = float(getattr(self.video_config, "min_chunk_seconds", 4.0))
|
|
175
|
+
self.max_split_depth = int(getattr(self.video_config, "max_split_depth", 4))
|
|
176
|
+
self.action_window_seconds = float(getattr(self.video_config, "action_window_seconds", 2.0))
|
|
177
|
+
self.dense_action_fps = float(getattr(self.video_config, "dense_action_fps", 4.0))
|
|
178
|
+
self.max_dense_action_frames = int(
|
|
179
|
+
getattr(self.video_config, "max_dense_action_frames", 24)
|
|
180
|
+
)
|
|
181
|
+
self.native_max_retries = int(getattr(self.video_config, "native_max_retries", 1))
|
|
182
|
+
self.model_call_timeout_seconds = float(
|
|
183
|
+
getattr(self.video_config, "model_call_timeout_seconds", 120.0)
|
|
184
|
+
)
|
|
185
|
+
breaker = getattr(ctx, "_video_circuit_breaker", None)
|
|
186
|
+
if breaker is None:
|
|
187
|
+
breaker = VideoCircuitBreaker(
|
|
188
|
+
threshold=int(getattr(self.video_config, "circuit_breaker_threshold", 3)),
|
|
189
|
+
cooldown_seconds=float(
|
|
190
|
+
getattr(
|
|
191
|
+
self.video_config,
|
|
192
|
+
"circuit_breaker_cooldown_seconds",
|
|
193
|
+
60.0,
|
|
194
|
+
)
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
ctx._video_circuit_breaker = breaker
|
|
198
|
+
self.circuit_breaker = breaker
|
|
199
|
+
self.local_files_to_cleanup = set()
|
|
200
|
+
self.local_dirs_to_cleanup = set()
|
|
201
|
+
self.cloud_files_to_cleanup = set()
|
|
202
|
+
|
|
203
|
+
sub_prompt_path = Path(__file__).parent / "video_sub_agent.md"
|
|
204
|
+
self.sub_system_prompt = (
|
|
205
|
+
sub_prompt_path.read_text(encoding="utf-8") if sub_prompt_path.exists() else ""
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
audio_prompt_path = Path(__file__).parent / "video_sub_agent_audio.md"
|
|
209
|
+
self.audio_system_prompt = (
|
|
210
|
+
audio_prompt_path.read_text(encoding="utf-8") if audio_prompt_path.exists() else ""
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
self._init_tools()
|
|
214
|
+
self._init_engine()
|
|
215
|
+
|
|
216
|
+
def _get_universal_llm(self, *, use_fallback: bool = False):
|
|
217
|
+
"""Resolve the utility-scoped video model, including its configured fallback."""
|
|
218
|
+
|
|
219
|
+
return get_llm(
|
|
220
|
+
self.ctx,
|
|
221
|
+
name="video_analyzer",
|
|
222
|
+
is_utils=True,
|
|
223
|
+
use_fallback=use_fallback,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
async def _invoke_universal_model(
|
|
227
|
+
self,
|
|
228
|
+
messages: list[BaseMessage],
|
|
229
|
+
tools: list[dict[str, Any]],
|
|
230
|
+
*,
|
|
231
|
+
timeout: float,
|
|
232
|
+
label: str,
|
|
233
|
+
force_fallback: bool = False,
|
|
234
|
+
):
|
|
235
|
+
"""Invoke primary then fallback model under a context-scoped circuit breaker."""
|
|
236
|
+
|
|
237
|
+
primary_key = f"video_analyzer:{self.model_name}:primary"
|
|
238
|
+
attempts = [True] if force_fallback else [False, True]
|
|
239
|
+
last_error: BaseException | None = None
|
|
240
|
+
for use_fallback in attempts:
|
|
241
|
+
if not use_fallback and not self.circuit_breaker.allow(primary_key):
|
|
242
|
+
logger.warning(f"{label}: primary video-model circuit is open; using fallback")
|
|
243
|
+
continue
|
|
244
|
+
try:
|
|
245
|
+
llm = self._get_universal_llm(use_fallback=use_fallback)
|
|
246
|
+
except (ValueError, AttributeError) as config_error:
|
|
247
|
+
if use_fallback:
|
|
248
|
+
if last_error is not None:
|
|
249
|
+
raise last_error
|
|
250
|
+
raise config_error
|
|
251
|
+
raise
|
|
252
|
+
try:
|
|
253
|
+
bound_llm = llm.bind_tools(tools)
|
|
254
|
+
response = await asyncio.wait_for(bound_llm.ainvoke(messages), timeout=timeout)
|
|
255
|
+
if not use_fallback:
|
|
256
|
+
self.circuit_breaker.record_success(primary_key)
|
|
257
|
+
return response
|
|
258
|
+
except BaseException as error:
|
|
259
|
+
failure = classify_video_failure(error)
|
|
260
|
+
if not use_fallback:
|
|
261
|
+
self.circuit_breaker.record_failure(primary_key, failure)
|
|
262
|
+
last_error = error
|
|
263
|
+
if use_fallback or not failure.should_fallback:
|
|
264
|
+
raise
|
|
265
|
+
logger.warning(
|
|
266
|
+
f"{label}: {failure.category.value} from primary; trying configured fallback"
|
|
267
|
+
)
|
|
268
|
+
if last_error is not None:
|
|
269
|
+
raise last_error
|
|
270
|
+
raise RuntimeError(f"{label}: no video model is available")
|
|
271
|
+
|
|
272
|
+
def _record_reliability_metrics(self) -> None:
|
|
273
|
+
"""Persist a compact blackboard/circuit snapshot into the active trace."""
|
|
274
|
+
|
|
275
|
+
trace_id = CURRENT_TRACE_ID.get()
|
|
276
|
+
if not self.ctx.data_engine or not trace_id:
|
|
277
|
+
return
|
|
278
|
+
try:
|
|
279
|
+
self.ctx.data_engine.record_trace(
|
|
280
|
+
type="video_analysis",
|
|
281
|
+
name="video_analysis_reliability",
|
|
282
|
+
payload={
|
|
283
|
+
"blackboard": self.blackboard.metrics(),
|
|
284
|
+
"circuits": self.circuit_breaker.snapshot(),
|
|
285
|
+
},
|
|
286
|
+
parent_trace_id=trace_id,
|
|
287
|
+
)
|
|
288
|
+
except Exception as error:
|
|
289
|
+
logger.warning(f"Failed to record video reliability metrics: {error}")
|
|
290
|
+
|
|
291
|
+
async def _cleanup_universal_resources(self) -> None:
|
|
292
|
+
"""Release universal-path temporary media without touching durable evidence."""
|
|
293
|
+
|
|
294
|
+
if os.environ.get("KEEP_VIDEOS") or os.environ.get("ARTEMIS_DEBUG"):
|
|
295
|
+
return
|
|
296
|
+
for path in list(self.local_files_to_cleanup):
|
|
297
|
+
try:
|
|
298
|
+
if path.exists():
|
|
299
|
+
path.unlink()
|
|
300
|
+
except OSError as error:
|
|
301
|
+
logger.warning(f"Failed to delete temporary video artifact {path}: {error}")
|
|
302
|
+
protected = {
|
|
303
|
+
Path("/").resolve(),
|
|
304
|
+
Path("/tmp").resolve(),
|
|
305
|
+
Path("/var/tmp").resolve(),
|
|
306
|
+
Path.home().resolve(),
|
|
307
|
+
Path(tempfile.gettempdir()).resolve(),
|
|
308
|
+
}
|
|
309
|
+
for directory in sorted(
|
|
310
|
+
self.local_dirs_to_cleanup,
|
|
311
|
+
key=lambda value: len(value.parts),
|
|
312
|
+
reverse=True,
|
|
313
|
+
):
|
|
314
|
+
try:
|
|
315
|
+
resolved = directory.resolve()
|
|
316
|
+
if resolved not in protected and directory.exists():
|
|
317
|
+
shutil.rmtree(directory, ignore_errors=True)
|
|
318
|
+
except OSError as error:
|
|
319
|
+
logger.warning(f"Failed to delete temporary video directory {directory}: {error}")
|
|
320
|
+
|
|
321
|
+
def _action_timestamps(self, start: float, end: float) -> list[float]:
|
|
322
|
+
"""Return test-relative timestamps for recorded actions in an interval."""
|
|
323
|
+
|
|
324
|
+
engine = self.ctx.data_engine
|
|
325
|
+
session_id = getattr(engine, "current_session_id", None) if engine else None
|
|
326
|
+
storage = getattr(engine, "storage", None) if engine else None
|
|
327
|
+
session_start = getattr(engine, "session_start_time", None) if engine else None
|
|
328
|
+
if not storage or not session_id or not isinstance(session_start, (int, float)):
|
|
329
|
+
return []
|
|
330
|
+
try:
|
|
331
|
+
return sorted(
|
|
332
|
+
{
|
|
333
|
+
round(float(step.timestamp) - float(session_start), 3)
|
|
334
|
+
for step in storage.get_steps(session_id)
|
|
335
|
+
if step.action_taken
|
|
336
|
+
and start <= float(step.timestamp) - float(session_start) <= end
|
|
337
|
+
}
|
|
338
|
+
)
|
|
339
|
+
except Exception as error:
|
|
340
|
+
logger.warning(f"Failed to load action timestamps for video sampling: {error}")
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
def _dense_action_offsets(self, start: float, end: float, actual_start: float) -> list[float]:
|
|
344
|
+
"""Build bounded, video-relative sampling offsets around mobile actions."""
|
|
345
|
+
|
|
346
|
+
if self.max_dense_action_frames <= 0:
|
|
347
|
+
return []
|
|
348
|
+
interval = 1.0 / max(1.0, self.dense_action_fps)
|
|
349
|
+
offsets: list[float] = []
|
|
350
|
+
for action_time in self._action_timestamps(start, end):
|
|
351
|
+
window_start = max(start, action_time - self.action_window_seconds)
|
|
352
|
+
window_end = min(end, action_time + self.action_window_seconds)
|
|
353
|
+
cursor = window_start
|
|
354
|
+
while cursor <= window_end + 1e-6:
|
|
355
|
+
offsets.append(max(0.0, cursor - actual_start))
|
|
356
|
+
if len(offsets) >= self.max_dense_action_frames:
|
|
357
|
+
return offsets
|
|
358
|
+
cursor += interval
|
|
359
|
+
return offsets
|
|
360
|
+
|
|
361
|
+
@staticmethod
|
|
362
|
+
def _audio_content_block(audio_path: Path) -> dict[str, Any]:
|
|
363
|
+
suffix = audio_path.suffix.lower()
|
|
364
|
+
mime_type = {
|
|
365
|
+
".aac": "audio/aac",
|
|
366
|
+
".m4a": "audio/mp4",
|
|
367
|
+
".mp3": "audio/mpeg",
|
|
368
|
+
".wav": "audio/wav",
|
|
369
|
+
".ogg": "audio/ogg",
|
|
370
|
+
}.get(suffix, "application/octet-stream")
|
|
371
|
+
return {
|
|
372
|
+
"type": "audio",
|
|
373
|
+
"base64": base64.b64encode(audio_path.read_bytes()).decode("utf-8"),
|
|
374
|
+
"mime_type": mime_type,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
def _refresh_blackboard(self) -> None:
|
|
378
|
+
"""Refresh the presentation ledger from persistent video memory."""
|
|
379
|
+
self.blackboard_entries = self.blackboard.list_ledger_entries()
|
|
380
|
+
|
|
381
|
+
def _clean_blackboard(self) -> None:
|
|
382
|
+
"""Clean the local view without deleting another agent's durable records."""
|
|
383
|
+
self.blackboard_entries = ConflictResolutionService.clean(self.blackboard_entries)
|
|
384
|
+
|
|
385
|
+
def _record_blackboard_entry(
|
|
386
|
+
self, entry: dict[str, Any], *, modality: str = "video"
|
|
387
|
+
) -> dict[str, Any]:
|
|
388
|
+
"""Persist an observation immediately, then update this agent's ledger."""
|
|
389
|
+
stored = self.blackboard.add_observation(entry, modality=modality)
|
|
390
|
+
observation_id = stored.get("observation_id")
|
|
391
|
+
if not any(
|
|
392
|
+
existing.get("observation_id") == observation_id
|
|
393
|
+
for existing in self.blackboard_entries
|
|
394
|
+
if isinstance(existing, dict)
|
|
395
|
+
):
|
|
396
|
+
self.blackboard_entries.append(stored)
|
|
397
|
+
return stored
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def _parse_chunk_result(result: str) -> tuple[str, str | None]:
|
|
401
|
+
"""Extract summary/analysis from the stable child response envelope."""
|
|
402
|
+
match = re.search(r"Summary:\s*(.*?)(?:\s+Analysis:\s*(.*))?$", result, re.DOTALL)
|
|
403
|
+
if not match:
|
|
404
|
+
return result.strip(), None
|
|
405
|
+
summary = match.group(1).strip()
|
|
406
|
+
analysis = match.group(2).strip() if match.group(2) else None
|
|
407
|
+
return summary, analysis
|
|
408
|
+
|
|
409
|
+
def _init_tools(self) -> None:
|
|
410
|
+
"""Initializes native and sub-agent tool declarations."""
|
|
411
|
+
self.tools_declaration = build_native_tools_declaration()
|
|
412
|
+
self.submit_answer_tool = build_submit_answer_declaration()
|
|
413
|
+
|
|
414
|
+
def _init_engine(self) -> None:
|
|
415
|
+
"""Initializes model engine and decides whether to use native Gemini or Universal path."""
|
|
416
|
+
ctx = self.ctx
|
|
417
|
+
llm_config = getattr(ctx, "llm_config", None)
|
|
418
|
+
utils_cfg = getattr(llm_config, "utils", None) if llm_config else None
|
|
419
|
+
llm_cfg = getattr(utils_cfg, "video_analyzer", None) if utils_cfg else None
|
|
420
|
+
self.model_name = strip_provider_prefix(
|
|
421
|
+
llm_cfg.model if (llm_cfg and hasattr(llm_cfg, "model")) else "gemini-3.8-flash"
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
has_google_key = bool(
|
|
425
|
+
settings.GOOGLE_API_KEY and settings.GOOGLE_API_KEY.get_secret_value()
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# If client is already set on ctx or explicit Gemini configuration
|
|
429
|
+
self.client = getattr(ctx, "_genai_client", None)
|
|
430
|
+
if self.client is not None:
|
|
431
|
+
self.use_native_gemini = True
|
|
432
|
+
elif has_google_key and is_gemini_model(self.model_name):
|
|
433
|
+
try:
|
|
434
|
+
self.client = genai.Client(api_key=settings.GOOGLE_API_KEY.get_secret_value())
|
|
435
|
+
ctx._genai_client = self.client
|
|
436
|
+
self.use_native_gemini = True
|
|
437
|
+
asyncio.create_task(cleanup_abandoned_gemini_files(self.client))
|
|
438
|
+
except Exception as e:
|
|
439
|
+
logger.warning(
|
|
440
|
+
f"Failed to initialize native Gemini client: {e}. Using universal engine."
|
|
441
|
+
)
|
|
442
|
+
self.use_native_gemini = False
|
|
443
|
+
else:
|
|
444
|
+
self.use_native_gemini = False
|
|
445
|
+
self._resolve_video_processing()
|
|
446
|
+
|
|
447
|
+
def _resolve_video_processing(self) -> None:
|
|
448
|
+
"""Picks static vs agentic video understanding for the sub-agents.
|
|
449
|
+
|
|
450
|
+
Agentic processing (Gemini Interactions API) needs the native client
|
|
451
|
+
and a supporting model; the configured knob (``auto`` / ``agentic`` /
|
|
452
|
+
``static``) decides the rest. The chunk ceiling follows the mode
|
|
453
|
+
because agentic sub-agents are built to take long clips.
|
|
454
|
+
"""
|
|
455
|
+
configured = getattr(self.video_config, "processing", "auto")
|
|
456
|
+
mode = "static"
|
|
457
|
+
if self.use_native_gemini:
|
|
458
|
+
mode = resolve_video_processing(configured, self.model_name)
|
|
459
|
+
self.video_processing = mode
|
|
460
|
+
self.chunk_size_seconds = (
|
|
461
|
+
self.agentic_chunk_size_seconds if mode == "agentic" else self.static_chunk_size_seconds
|
|
462
|
+
)
|
|
463
|
+
if mode == "agentic":
|
|
464
|
+
logger.info(
|
|
465
|
+
f"Video sub-agents use agentic video understanding on {self.model_name}"
|
|
466
|
+
f" (chunk ceiling {self.chunk_size_seconds:.0f}s)"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def blackboard_ledger(self) -> str:
|
|
471
|
+
self._clean_blackboard()
|
|
472
|
+
if not self.blackboard_entries:
|
|
473
|
+
return "No video segments analyzed yet."
|
|
474
|
+
lines = []
|
|
475
|
+
for e in self.blackboard_entries:
|
|
476
|
+
lines.append(f"{e['start']}s - {e['end']}s: {e['summary']}")
|
|
477
|
+
return " | ".join(lines)
|
|
478
|
+
|
|
479
|
+
async def upload_and_poll_file(self, compressed_path: Path) -> any:
|
|
480
|
+
from artemis.agents.video_analyzer import gemini_files
|
|
481
|
+
|
|
482
|
+
return await gemini_files.upload_and_poll_file(
|
|
483
|
+
self.client, compressed_path, self.cloud_files_to_cleanup
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
def get_overlapping_warnings(self, start_time: float, end_time: float | None) -> list[dict]:
|
|
487
|
+
warnings = []
|
|
488
|
+
if not isinstance(start_time, (int, float)) or (
|
|
489
|
+
end_time is not None and not isinstance(end_time, (int, float))
|
|
490
|
+
):
|
|
491
|
+
return warnings
|
|
492
|
+
if end_time is None or (end_time - start_time) <= 0:
|
|
493
|
+
return warnings
|
|
494
|
+
req_duration = end_time - start_time
|
|
495
|
+
for entry in self.blackboard_entries:
|
|
496
|
+
if isinstance(entry, dict) and entry.get("end") != "unknown":
|
|
497
|
+
try:
|
|
498
|
+
if not isinstance(entry.get("start"), (int, float, str)) or not isinstance(
|
|
499
|
+
entry.get("end"), (int, float, str)
|
|
500
|
+
):
|
|
501
|
+
continue
|
|
502
|
+
overlap_start = max(start_time, float(entry["start"]))
|
|
503
|
+
overlap_end = min(end_time, float(entry["end"]))
|
|
504
|
+
overlap_duration = max(0.0, overlap_end - overlap_start)
|
|
505
|
+
if (overlap_duration / req_duration) > 0.8:
|
|
506
|
+
warnings.append(
|
|
507
|
+
{
|
|
508
|
+
"target": entry.get("target", ""),
|
|
509
|
+
"summary": entry.get("summary", ""),
|
|
510
|
+
}
|
|
511
|
+
)
|
|
512
|
+
except (ValueError, TypeError):
|
|
513
|
+
pass
|
|
514
|
+
return warnings
|
|
515
|
+
|
|
516
|
+
@trace(type="tool", name="extract_segment_metadata")
|
|
517
|
+
async def exec_extract_segment_metadata(
|
|
518
|
+
self, start_time: float, end_time: float | None = None
|
|
519
|
+
) -> str:
|
|
520
|
+
controller = get_controller(self.ctx)
|
|
521
|
+
async with TRANSCODE_SEMAPHORE:
|
|
522
|
+
result = await controller.extract_segment_metadata(start_time, end_time)
|
|
523
|
+
if result.success and result.video_path:
|
|
524
|
+
path = Path(result.video_path)
|
|
525
|
+
self.local_files_to_cleanup.add(path)
|
|
526
|
+
self.local_dirs_to_cleanup.add(path.parent)
|
|
527
|
+
size_mb = (
|
|
528
|
+
result.file_size_mb
|
|
529
|
+
if result.file_size_mb is not None
|
|
530
|
+
else (path.stat().st_size / (1024 * 1024) if path.exists() else 0.0)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if result.duration_seconds is not None:
|
|
534
|
+
duration = result.duration_seconds
|
|
535
|
+
elif end_time is not None:
|
|
536
|
+
duration = end_time - start_time
|
|
537
|
+
else:
|
|
538
|
+
try:
|
|
539
|
+
duration = await probe_duration(path)
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.warning(f"Failed to resolve duration via ffprobe for {path}: {e}")
|
|
542
|
+
duration = 0.0
|
|
543
|
+
if isinstance(duration, (int, float)):
|
|
544
|
+
duration = round(duration, 1)
|
|
545
|
+
|
|
546
|
+
return json.dumps(
|
|
547
|
+
{
|
|
548
|
+
"duration_seconds": duration,
|
|
549
|
+
"file_size_mb": (
|
|
550
|
+
round(size_mb, 2) if isinstance(size_mb, (int, float)) else size_mb
|
|
551
|
+
),
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
raise RuntimeError(f"Failed to get video segment: {result.message}")
|
|
555
|
+
|
|
556
|
+
async def _resolve_open_ended_interval(
|
|
557
|
+
self, requested_start: float
|
|
558
|
+
) -> tuple[float, float | None, str | None]:
|
|
559
|
+
"""Resolve an open-ended request to a stable numeric interval (or an error)."""
|
|
560
|
+
controller = get_controller(self.ctx)
|
|
561
|
+
async with TRANSCODE_SEMAPHORE:
|
|
562
|
+
metadata_result = await controller.extract_segment_metadata(requested_start, None)
|
|
563
|
+
if not metadata_result.success:
|
|
564
|
+
return (
|
|
565
|
+
requested_start,
|
|
566
|
+
None,
|
|
567
|
+
f"Error fetching segment metadata: {metadata_result.message}",
|
|
568
|
+
)
|
|
569
|
+
duration = getattr(metadata_result, "duration_seconds", None)
|
|
570
|
+
actual_start = getattr(metadata_result, "actual_start_relative_time", requested_start)
|
|
571
|
+
if not isinstance(duration, (int, float)) or duration <= 0:
|
|
572
|
+
return (
|
|
573
|
+
requested_start,
|
|
574
|
+
None,
|
|
575
|
+
"Error fetching segment metadata: resolved duration is unavailable.",
|
|
576
|
+
)
|
|
577
|
+
if not isinstance(actual_start, (int, float)):
|
|
578
|
+
actual_start = requested_start
|
|
579
|
+
requested_start = round(float(actual_start), 3)
|
|
580
|
+
requested_end = round(requested_start + float(duration), 3)
|
|
581
|
+
if metadata_result.video_path:
|
|
582
|
+
metadata_path = Path(metadata_result.video_path)
|
|
583
|
+
self.local_files_to_cleanup.add(metadata_path)
|
|
584
|
+
self.local_dirs_to_cleanup.add(metadata_path.parent)
|
|
585
|
+
return requested_start, requested_end, None
|
|
586
|
+
|
|
587
|
+
def _plan_chunks(self, intervals: list[tuple[float, float]]) -> list[tuple[float, float]]:
|
|
588
|
+
"""Cuts intervals into consecutive chunks of at most ``chunk_size_seconds``."""
|
|
589
|
+
target_chunk_size = self.chunk_size_seconds
|
|
590
|
+
chunks: list[tuple[float, float]] = []
|
|
591
|
+
for gap_start, gap_end in intervals:
|
|
592
|
+
cursor = gap_start
|
|
593
|
+
while cursor < gap_end:
|
|
594
|
+
chunk_end = min(gap_end, cursor + target_chunk_size)
|
|
595
|
+
chunks.append((round(cursor, 3), round(chunk_end, 3)))
|
|
596
|
+
cursor = chunk_end
|
|
597
|
+
return chunks
|
|
598
|
+
|
|
599
|
+
@trace(type="tool", name="spawn_sub_agent")
|
|
600
|
+
async def exec_spawn_sub_agent(
|
|
601
|
+
self, start_time: float, end_time: float | None, specific_query: str
|
|
602
|
+
) -> str:
|
|
603
|
+
if not isinstance(start_time, (int, float)):
|
|
604
|
+
return "Error: start_time must be a numeric recording-relative timestamp."
|
|
605
|
+
if not str(specific_query or "").strip():
|
|
606
|
+
return "Error: specific_query must describe the evidence to find."
|
|
607
|
+
|
|
608
|
+
requested_start = round(float(start_time), 3)
|
|
609
|
+
requested_end = round(float(end_time), 3) if isinstance(end_time, (int, float)) else None
|
|
610
|
+
if requested_end is not None and requested_end <= requested_start:
|
|
611
|
+
return "Error: end_time must be greater than start_time."
|
|
612
|
+
|
|
613
|
+
# A closed interval does not need a throw-away full-range trim merely
|
|
614
|
+
# to discover its duration. Open-ended requests are resolved once to a
|
|
615
|
+
# stable numeric end before they enter persistent coverage.
|
|
616
|
+
if requested_end is None:
|
|
617
|
+
requested_start, requested_end, error = await self._resolve_open_ended_interval(
|
|
618
|
+
requested_start
|
|
619
|
+
)
|
|
620
|
+
if error is not None:
|
|
621
|
+
return error
|
|
622
|
+
|
|
623
|
+
cached_results = self.blackboard.format_cached_segments(
|
|
624
|
+
requested_start, requested_end, specific_query
|
|
625
|
+
)
|
|
626
|
+
missing = self.blackboard.missing_intervals(requested_start, requested_end, specific_query)
|
|
627
|
+
if not missing:
|
|
628
|
+
logger.info(
|
|
629
|
+
"Reusing complete video-blackboard coverage for "
|
|
630
|
+
f"{requested_start:.1f}s-{requested_end:.1f}s"
|
|
631
|
+
)
|
|
632
|
+
return "CACHED VIDEO ANALYSIS: " + " ".join(cached_results)
|
|
633
|
+
|
|
634
|
+
chunks = self._plan_chunks(missing)
|
|
635
|
+
logger.info(
|
|
636
|
+
f"Video blackboard planned {len(chunks)} uncovered chunk(s) for "
|
|
637
|
+
f"{requested_start:.1f}s-{requested_end:.1f}s"
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
async def analyze_with_recovery(
|
|
641
|
+
cs: float, ce: float, depth: int = 0, *, retried_static: bool = False
|
|
642
|
+
) -> tuple[list[str], list[str]]:
|
|
643
|
+
try:
|
|
644
|
+
async with API_SEMAPHORE:
|
|
645
|
+
value = await self._exec_single_chunk(cs, ce, specific_query)
|
|
646
|
+
return [value], []
|
|
647
|
+
except Exception as error:
|
|
648
|
+
failure = classify_video_failure(error)
|
|
649
|
+
duration = ce - cs
|
|
650
|
+
fits_ceiling = duration <= self.chunk_size_seconds + 1e-3
|
|
651
|
+
degraded = isinstance(error, AgenticVideoDegraded)
|
|
652
|
+
if degraded and fits_ceiling and not retried_static:
|
|
653
|
+
# Retry short intervals once in static mode without splitting.
|
|
654
|
+
logger.warning(
|
|
655
|
+
f"Retrying {cs:.1f}s-{ce:.1f}s in static mode: it already fits"
|
|
656
|
+
f" the {self.chunk_size_seconds:.0f}s chunk ceiling"
|
|
657
|
+
)
|
|
658
|
+
return await analyze_with_recovery(cs, ce, depth, retried_static=True)
|
|
659
|
+
pieces = (
|
|
660
|
+
self._plan_chunks([(cs, ce)])
|
|
661
|
+
if failure.should_split and not fits_ceiling
|
|
662
|
+
else []
|
|
663
|
+
)
|
|
664
|
+
if len(pieces) > 1:
|
|
665
|
+
# Split clips planned before a downgrade at the new ceiling.
|
|
666
|
+
logger.warning(
|
|
667
|
+
f"Re-planning failed {cs:.1f}s-{ce:.1f}s chunk into {len(pieces)}"
|
|
668
|
+
f" piece(s) of at most {self.chunk_size_seconds:.0f}s"
|
|
669
|
+
f" after {failure.category.value}"
|
|
670
|
+
)
|
|
671
|
+
parts = await asyncio.gather(
|
|
672
|
+
*(analyze_with_recovery(ps, pe, depth) for ps, pe in pieces)
|
|
673
|
+
)
|
|
674
|
+
return (
|
|
675
|
+
[item for values, _ in parts for item in values],
|
|
676
|
+
[item for _, failures in parts for item in failures],
|
|
677
|
+
)
|
|
678
|
+
# Bisecting only helps genuine size/timeout failures; a
|
|
679
|
+
# degraded interval that fits the ceiling has had its one
|
|
680
|
+
# static retry and is terminal.
|
|
681
|
+
can_split = (
|
|
682
|
+
failure.should_split
|
|
683
|
+
and not degraded
|
|
684
|
+
and depth < self.max_split_depth
|
|
685
|
+
and duration >= self.min_chunk_seconds * 2
|
|
686
|
+
)
|
|
687
|
+
if can_split:
|
|
688
|
+
midpoint = round(cs + duration / 2.0, 3)
|
|
689
|
+
logger.warning(
|
|
690
|
+
f"Splitting failed {cs:.1f}s-{ce:.1f}s chunk at {midpoint:.1f}s "
|
|
691
|
+
f"after {failure.category.value}"
|
|
692
|
+
)
|
|
693
|
+
left, right = await asyncio.gather(
|
|
694
|
+
analyze_with_recovery(cs, midpoint, depth + 1),
|
|
695
|
+
analyze_with_recovery(midpoint, ce, depth + 1),
|
|
696
|
+
)
|
|
697
|
+
return left[0] + right[0], left[1] + right[1]
|
|
698
|
+
failure_text = f"{cs:.1f}s-{ce:.1f}s [{failure.category.value}]: {error}"
|
|
699
|
+
logger.error(f"Video chunk terminal failure: {failure_text}")
|
|
700
|
+
return [], [failure_text]
|
|
701
|
+
|
|
702
|
+
recovered = await asyncio.gather(*(analyze_with_recovery(cs, ce) for cs, ce in chunks))
|
|
703
|
+
fresh_results = [item for values, _ in recovered for item in values]
|
|
704
|
+
failed_chunks = [item for _, failures in recovered for item in failures]
|
|
705
|
+
|
|
706
|
+
# Reload committed chunk summaries and event evidence from the shared
|
|
707
|
+
# store before applying presentation-only conflict filtering.
|
|
708
|
+
self._refresh_blackboard()
|
|
709
|
+
self._clean_blackboard()
|
|
710
|
+
|
|
711
|
+
valid_results = cached_results + fresh_results
|
|
712
|
+
if not valid_results:
|
|
713
|
+
return "All sub-agent chunks failed. Failed intervals: " + "; ".join(failed_chunks)
|
|
714
|
+
combined = " ".join(valid_results)
|
|
715
|
+
if failed_chunks:
|
|
716
|
+
return (
|
|
717
|
+
"PARTIAL VIDEO ANALYSIS (successful chunks were persisted). "
|
|
718
|
+
f"Failed intervals: {'; '.join(failed_chunks)}. Results: {combined}"
|
|
719
|
+
)
|
|
720
|
+
self._record_reliability_metrics()
|
|
721
|
+
return combined
|
|
722
|
+
|
|
723
|
+
async def _exec_single_chunk(
|
|
724
|
+
self, start_time: float, end_time: float | None, specific_query: str
|
|
725
|
+
) -> str:
|
|
726
|
+
from artemis.agents.video_analyzer import chunk_native
|
|
727
|
+
|
|
728
|
+
return await chunk_native.exec_single_chunk(self, start_time, end_time, specific_query)
|
|
729
|
+
|
|
730
|
+
@trace(type="tool", name="analyze_audio_only")
|
|
731
|
+
async def exec_analyze_audio_only(
|
|
732
|
+
self, start_time: float, end_time: float | None, specific_query: str
|
|
733
|
+
) -> str:
|
|
734
|
+
from artemis.agents.video_analyzer import audio_native
|
|
735
|
+
|
|
736
|
+
return await audio_native.exec_analyze_audio_only(
|
|
737
|
+
self, start_time, end_time, specific_query
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
async def _exec_single_chunk_universal(
|
|
741
|
+
self,
|
|
742
|
+
compressed_path: Path,
|
|
743
|
+
raw_path: Path,
|
|
744
|
+
start_time: float,
|
|
745
|
+
end_time: float | None,
|
|
746
|
+
actual_start: float,
|
|
747
|
+
prompt_with_context: str,
|
|
748
|
+
specific_query: str,
|
|
749
|
+
force_fallback: bool = False,
|
|
750
|
+
) -> str:
|
|
751
|
+
from artemis.agents.video_analyzer import universal_engine
|
|
752
|
+
|
|
753
|
+
return await universal_engine.exec_single_chunk_universal(
|
|
754
|
+
self,
|
|
755
|
+
compressed_path=compressed_path,
|
|
756
|
+
raw_path=raw_path,
|
|
757
|
+
start_time=start_time,
|
|
758
|
+
end_time=end_time,
|
|
759
|
+
actual_start=actual_start,
|
|
760
|
+
prompt_with_context=prompt_with_context,
|
|
761
|
+
specific_query=specific_query,
|
|
762
|
+
force_fallback=force_fallback,
|
|
763
|
+
)
|
|
764
|
+
|
|
765
|
+
async def _exec_analyze_audio_universal(
|
|
766
|
+
self,
|
|
767
|
+
audio_path: Path,
|
|
768
|
+
start_time: float,
|
|
769
|
+
end_time: float | None,
|
|
770
|
+
actual_start: float,
|
|
771
|
+
prompt_with_context: str,
|
|
772
|
+
specific_query: str,
|
|
773
|
+
force_fallback: bool = False,
|
|
774
|
+
) -> str:
|
|
775
|
+
from artemis.agents.video_analyzer import universal_engine
|
|
776
|
+
|
|
777
|
+
return await universal_engine.exec_analyze_audio_universal(
|
|
778
|
+
self,
|
|
779
|
+
audio_path=audio_path,
|
|
780
|
+
start_time=start_time,
|
|
781
|
+
end_time=end_time,
|
|
782
|
+
actual_start=actual_start,
|
|
783
|
+
prompt_with_context=prompt_with_context,
|
|
784
|
+
specific_query=specific_query,
|
|
785
|
+
force_fallback=force_fallback,
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
async def _run_universal(
|
|
789
|
+
self,
|
|
790
|
+
time_description: str,
|
|
791
|
+
purpose: str,
|
|
792
|
+
system_prompt: str,
|
|
793
|
+
*,
|
|
794
|
+
force_fallback: bool = False,
|
|
795
|
+
) -> tuple[str, str]:
|
|
796
|
+
from artemis.agents.video_analyzer import universal_engine
|
|
797
|
+
|
|
798
|
+
return await universal_engine.run_universal(
|
|
799
|
+
self,
|
|
800
|
+
time_description,
|
|
801
|
+
purpose,
|
|
802
|
+
system_prompt,
|
|
803
|
+
force_fallback=force_fallback,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
@trace(type="agent", name="video_analyzer")
|
|
807
|
+
async def run(self, time_description: str, purpose: str) -> tuple[str, str]:
|
|
808
|
+
ctx = self.ctx
|
|
809
|
+
async with MAIN_AGENT_SEMAPHORE:
|
|
810
|
+
logger.info(
|
|
811
|
+
f"Video analyzer invoked with time: '{time_description}', purpose: '{purpose}'"
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
self._refresh_blackboard()
|
|
815
|
+
|
|
816
|
+
# Initialize engine & cleanup tracking
|
|
817
|
+
self._init_engine()
|
|
818
|
+
|
|
819
|
+
# Resolve model name and temperature from config
|
|
820
|
+
llm_config = getattr(ctx, "llm_config", None)
|
|
821
|
+
utils_cfg = getattr(llm_config, "utils", None) if llm_config else None
|
|
822
|
+
llm_cfg = getattr(utils_cfg, "video_analyzer", None) if utils_cfg else None
|
|
823
|
+
temperature = 0.2
|
|
824
|
+
thinking_level = None
|
|
825
|
+
if llm_cfg:
|
|
826
|
+
self.model_name = strip_provider_prefix(llm_cfg.model)
|
|
827
|
+
if getattr(llm_cfg, "temperature", None) is not None:
|
|
828
|
+
temperature = llm_cfg.temperature
|
|
829
|
+
if getattr(llm_cfg, "thinking_level", None) is not None:
|
|
830
|
+
thinking_level = llm_cfg.thinking_level
|
|
831
|
+
else:
|
|
832
|
+
self.model_name = "gemini-3.8-flash"
|
|
833
|
+
|
|
834
|
+
# Track files for cleanup
|
|
835
|
+
self.local_files_to_cleanup = set()
|
|
836
|
+
self.local_dirs_to_cleanup = set()
|
|
837
|
+
self.cloud_files_to_cleanup = set()
|
|
838
|
+
|
|
839
|
+
sub_prompt_path = Path(__file__).parent / "video_sub_agent.md"
|
|
840
|
+
self.sub_system_prompt = sub_prompt_path.read_text(encoding="utf-8")
|
|
841
|
+
|
|
842
|
+
audio_prompt_path = Path(__file__).parent / "video_sub_agent_audio.md"
|
|
843
|
+
self.audio_system_prompt = audio_prompt_path.read_text(encoding="utf-8")
|
|
844
|
+
|
|
845
|
+
prompt_path = Path(__file__).parent / "video_analyzer.md"
|
|
846
|
+
system_prompt = prompt_path.read_text(encoding="utf-8")
|
|
847
|
+
if self.enable_ledger:
|
|
848
|
+
system_prompt += (
|
|
849
|
+
"\n## Trust Protocol\nConsult Blackboard Ledger before"
|
|
850
|
+
" searching. Evaluate if new search is necessary based on"
|
|
851
|
+
" previous context. Explain reasoning before aborting or"
|
|
852
|
+
" proceeding.\n"
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
if not self.use_native_gemini:
|
|
856
|
+
try:
|
|
857
|
+
return await self._run_universal(
|
|
858
|
+
time_description=time_description,
|
|
859
|
+
purpose=purpose,
|
|
860
|
+
system_prompt=system_prompt,
|
|
861
|
+
)
|
|
862
|
+
finally:
|
|
863
|
+
await self._cleanup_universal_resources()
|
|
864
|
+
|
|
865
|
+
from artemis.agents.video_analyzer import native_coordinator
|
|
866
|
+
|
|
867
|
+
return await native_coordinator.run_native(
|
|
868
|
+
self,
|
|
869
|
+
ctx,
|
|
870
|
+
time_description,
|
|
871
|
+
purpose,
|
|
872
|
+
system_prompt,
|
|
873
|
+
temperature,
|
|
874
|
+
thinking_level,
|
|
875
|
+
)
|