android-midscene-automation 0.2.4 → 1.1.0
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 +7 -0
- package/README.md +35 -0
- 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,162 @@
|
|
|
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
|
+
"""Gemini File API helpers: upload/poll lifecycle and abandoned-file cleanup."""
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
from datetime import UTC
|
|
26
|
+
except ImportError:
|
|
27
|
+
UTC = UTC
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
_LAST_CLEANUP_TIME = 0.0
|
|
32
|
+
_CLEANUP_LOCK = asyncio.Lock()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def cleanup_abandoned_gemini_files(client) -> None:
|
|
36
|
+
"""Scan and delete remaining cloud video files whose creation time has elapsed standard TTL."""
|
|
37
|
+
global _LAST_CLEANUP_TIME
|
|
38
|
+
if _CLEANUP_LOCK.locked():
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
async with _CLEANUP_LOCK:
|
|
42
|
+
now_ts = time.time()
|
|
43
|
+
if now_ts - _LAST_CLEANUP_TIME < 3600:
|
|
44
|
+
return
|
|
45
|
+
_LAST_CLEANUP_TIME = now_ts
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
pager = await asyncio.wait_for(client.aio.files.list(), timeout=30)
|
|
49
|
+
files = []
|
|
50
|
+
async for f in pager:
|
|
51
|
+
files.append(f)
|
|
52
|
+
now = datetime.now(UTC)
|
|
53
|
+
files_to_delete = []
|
|
54
|
+
|
|
55
|
+
for f in files:
|
|
56
|
+
display_name = getattr(f, "display_name", "") or ""
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
display_name.startswith("compressed_")
|
|
60
|
+
or display_name.startswith("audio_")
|
|
61
|
+
or "artemis" in display_name
|
|
62
|
+
):
|
|
63
|
+
create_time = getattr(f, "created_at", None) or getattr(f, "create_time", None)
|
|
64
|
+
if not create_time:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
if isinstance(create_time, str):
|
|
68
|
+
try:
|
|
69
|
+
parsed_time = datetime.fromisoformat(create_time.replace("Z", "+00:00"))
|
|
70
|
+
except Exception:
|
|
71
|
+
continue
|
|
72
|
+
elif isinstance(create_time, datetime):
|
|
73
|
+
parsed_time = create_time
|
|
74
|
+
else:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
if parsed_time.tzinfo is None:
|
|
78
|
+
parsed_time = parsed_time.replace(tzinfo=UTC)
|
|
79
|
+
|
|
80
|
+
age_seconds = (now - parsed_time).total_seconds()
|
|
81
|
+
|
|
82
|
+
if age_seconds > 7200:
|
|
83
|
+
files_to_delete.append(f)
|
|
84
|
+
logger.info(
|
|
85
|
+
f"Marked expired cloud asset for deletion: {f.name}"
|
|
86
|
+
f" ({display_name}), Age: {age_seconds / 3600:.1f}h"
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if files_to_delete:
|
|
90
|
+
logger.info(f"Purging {len(files_to_delete)} expired cloud assets in parallel...")
|
|
91
|
+
tasks = [
|
|
92
|
+
asyncio.wait_for(client.aio.files.delete(name=f.name), timeout=30)
|
|
93
|
+
for f in files_to_delete
|
|
94
|
+
]
|
|
95
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
96
|
+
except Exception as e:
|
|
97
|
+
logger.warning(f"Routine cloud maintenance skipped: {e}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def delete_cloud_file(
|
|
101
|
+
client, file_name: str, cloud_files_to_cleanup: set, *, timeout: float = 30.0
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Best-effort deletion of an uploaded file; failures are logged, never raised."""
|
|
104
|
+
logger.info(f"Cleaning up cloud file {file_name}...")
|
|
105
|
+
try:
|
|
106
|
+
await asyncio.wait_for(client.aio.files.delete(name=file_name), timeout=timeout)
|
|
107
|
+
cloud_files_to_cleanup.discard(file_name)
|
|
108
|
+
except Exception as ce:
|
|
109
|
+
logger.error(f"Failed to delete cloud file {file_name}: {ce}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def upload_and_poll_file(client, compressed_path: Path, cloud_files_to_cleanup: set) -> any:
|
|
113
|
+
"""Upload a media file to the Gemini File API and poll until it is ACTIVE."""
|
|
114
|
+
logger.info(f"Uploading {compressed_path} to Gemini File API...")
|
|
115
|
+
file_size_mb = compressed_path.stat().st_size / (1024 * 1024)
|
|
116
|
+
upload_timeout = max(30.0, min(120.0, file_size_mb * 2.0))
|
|
117
|
+
|
|
118
|
+
file = await asyncio.wait_for(
|
|
119
|
+
client.aio.files.upload(file=compressed_path),
|
|
120
|
+
timeout=upload_timeout,
|
|
121
|
+
)
|
|
122
|
+
cloud_files_to_cleanup.add(file.name)
|
|
123
|
+
|
|
124
|
+
max_wait = max(60, min(180, 60 + int(file_size_mb * 2)))
|
|
125
|
+
|
|
126
|
+
wait_interval = 0.5
|
|
127
|
+
start_wait = time.time()
|
|
128
|
+
retry_count = 0
|
|
129
|
+
max_retries = 3
|
|
130
|
+
|
|
131
|
+
while True:
|
|
132
|
+
try:
|
|
133
|
+
f_state = await asyncio.wait_for(client.aio.files.get(name=file.name), timeout=20)
|
|
134
|
+
retry_count = 0
|
|
135
|
+
except Exception as poll_error:
|
|
136
|
+
retry_count += 1
|
|
137
|
+
logger.warning(
|
|
138
|
+
f"Temporary issue polling file {file.name} (attempt"
|
|
139
|
+
f" {retry_count}/{max_retries}): {poll_error}"
|
|
140
|
+
)
|
|
141
|
+
if retry_count > max_retries:
|
|
142
|
+
raise RuntimeError(
|
|
143
|
+
f"Failed to poll Gemini File API after {max_retries} attempts: {poll_error}"
|
|
144
|
+
)
|
|
145
|
+
await asyncio.sleep(2.0)
|
|
146
|
+
continue
|
|
147
|
+
|
|
148
|
+
if f_state.state.name == "ACTIVE":
|
|
149
|
+
break
|
|
150
|
+
elif f_state.state.name == "FAILED":
|
|
151
|
+
raise RuntimeError(f"Gemini File API processing failed for {file.name}")
|
|
152
|
+
|
|
153
|
+
if time.time() - start_wait > max_wait:
|
|
154
|
+
raise TimeoutError(
|
|
155
|
+
f"Gemini File API processing timeout for {file.name}"
|
|
156
|
+
f" (Waited {max_wait}s for size {file_size_mb:.1f}MB)"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
logger.info(f"File {file.name} is {f_state.state.name}, waiting {wait_interval}s...")
|
|
160
|
+
await asyncio.sleep(wait_interval)
|
|
161
|
+
wait_interval = min(3.0, wait_interval * 1.5)
|
|
162
|
+
return file
|
|
@@ -0,0 +1,541 @@
|
|
|
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
|
+
"""Native Gemini coordinator loop for the video analyzer main agent.
|
|
16
|
+
|
|
17
|
+
Extracted from ``video_analyzer.py`` as a pure structural split. Patchable
|
|
18
|
+
collaborators are looked up late through the facade module so
|
|
19
|
+
``mock.patch("artemis.agents.video_analyzer.video_analyzer.<name>")`` targets
|
|
20
|
+
keep working.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import asyncio
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
import shutil
|
|
27
|
+
import tempfile
|
|
28
|
+
|
|
29
|
+
from google.genai import types
|
|
30
|
+
|
|
31
|
+
from artemis.agents.video_analyzer import video_analyzer as _va
|
|
32
|
+
from artemis.agents.video_analyzer.reliability import classify_video_failure
|
|
33
|
+
from artemis.constants import SAFETY_SETTINGS_BLOCK_NONE
|
|
34
|
+
from artemis.data_engine.trace import CURRENT_TRACE_ID, TraceSpan
|
|
35
|
+
from artemis.llm.google import normalize_usage
|
|
36
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
37
|
+
|
|
38
|
+
logger = get_logger(__name__)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _DummyResponse:
|
|
42
|
+
"""Aggregated view over a consumed stream, shaped like a full response."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, text, function_calls, parts):
|
|
45
|
+
self.text = text
|
|
46
|
+
self.function_calls = function_calls
|
|
47
|
+
self.candidates = [
|
|
48
|
+
types.Candidate(
|
|
49
|
+
content=types.Content(
|
|
50
|
+
role="model",
|
|
51
|
+
parts=parts if parts else ([types.Part.from_text(text=text)] if text else []),
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
async def run_native(
|
|
58
|
+
analyzer,
|
|
59
|
+
ctx,
|
|
60
|
+
time_description: str,
|
|
61
|
+
purpose: str,
|
|
62
|
+
system_prompt: str,
|
|
63
|
+
temperature: float,
|
|
64
|
+
thinking_level,
|
|
65
|
+
) -> tuple[str, str]:
|
|
66
|
+
"""Drives the native Gemini coordinator loop to a final answer."""
|
|
67
|
+
tools_declaration = analyzer.tools_declaration
|
|
68
|
+
|
|
69
|
+
contents = [
|
|
70
|
+
types.Content(
|
|
71
|
+
role="user",
|
|
72
|
+
parts=[
|
|
73
|
+
types.Part.from_text(
|
|
74
|
+
text=(f"Time description: {time_description}\nPurpose: {purpose}")
|
|
75
|
+
)
|
|
76
|
+
],
|
|
77
|
+
)
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
agent_outcome = ""
|
|
81
|
+
status = "success"
|
|
82
|
+
max_iterations = 10
|
|
83
|
+
iterations = 0
|
|
84
|
+
last_ledger_index = 0
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
while iterations < max_iterations:
|
|
88
|
+
iterations += 1
|
|
89
|
+
logger.info(f"Iteration {iterations}: Invoking Native Gemini SDK...")
|
|
90
|
+
|
|
91
|
+
analyzer._clean_blackboard()
|
|
92
|
+
|
|
93
|
+
last_ledger_index = _append_ledger_diff(analyzer, contents, last_ledger_index)
|
|
94
|
+
|
|
95
|
+
is_final_turn = iterations == max_iterations
|
|
96
|
+
_append_turn_guidance(contents, iterations, max_iterations, is_final_turn)
|
|
97
|
+
|
|
98
|
+
response = await _stream_main_turn(
|
|
99
|
+
analyzer,
|
|
100
|
+
ctx,
|
|
101
|
+
contents,
|
|
102
|
+
system_prompt,
|
|
103
|
+
temperature,
|
|
104
|
+
thinking_level,
|
|
105
|
+
tools_declaration,
|
|
106
|
+
is_final_turn,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
function_calls = response.function_calls
|
|
110
|
+
if not function_calls:
|
|
111
|
+
agent_outcome = response.text
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
if response.candidates and response.candidates[0].content:
|
|
115
|
+
contents.append(response.candidates[0].content)
|
|
116
|
+
else:
|
|
117
|
+
contents.append(
|
|
118
|
+
types.Content(
|
|
119
|
+
role="model",
|
|
120
|
+
parts=[types.Part(function_call=fc) for fc in function_calls],
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
tool_response_parts = await _execute_native_tool_calls(analyzer, function_calls)
|
|
125
|
+
|
|
126
|
+
if tool_response_parts:
|
|
127
|
+
contents.append(types.Content(role="user", parts=tool_response_parts))
|
|
128
|
+
|
|
129
|
+
if iterations >= max_iterations and not agent_outcome:
|
|
130
|
+
agent_outcome = (
|
|
131
|
+
"Error: Video analyzer agent reached maximum iterations"
|
|
132
|
+
" without settling on a final answer."
|
|
133
|
+
)
|
|
134
|
+
status = "error"
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.error(f"Video analyzer failed: {e}")
|
|
138
|
+
failure = classify_video_failure(e)
|
|
139
|
+
if failure.should_fallback:
|
|
140
|
+
logger.warning(
|
|
141
|
+
f"Native coordinator failed with {failure.category.value}; "
|
|
142
|
+
"continuing through universal fallback"
|
|
143
|
+
)
|
|
144
|
+
agent_outcome, status = await analyzer._run_universal(
|
|
145
|
+
time_description,
|
|
146
|
+
purpose,
|
|
147
|
+
system_prompt,
|
|
148
|
+
force_fallback=True,
|
|
149
|
+
)
|
|
150
|
+
else:
|
|
151
|
+
agent_outcome = f"Video analysis failed: {e}"
|
|
152
|
+
status = "error"
|
|
153
|
+
finally:
|
|
154
|
+
analyzer._record_reliability_metrics()
|
|
155
|
+
await _cleanup_native_run(analyzer)
|
|
156
|
+
|
|
157
|
+
return agent_outcome, status
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _append_ledger_diff(analyzer, contents: list, last_ledger_index: int) -> int:
|
|
161
|
+
"""Appends new blackboard entries (and proofs) since the last coordinator turn."""
|
|
162
|
+
new_entries = analyzer.blackboard_entries[last_ledger_index:]
|
|
163
|
+
if analyzer.enable_ledger and new_entries:
|
|
164
|
+
lines = []
|
|
165
|
+
image_parts = []
|
|
166
|
+
for e in new_entries:
|
|
167
|
+
line = f"[{e['start']}s - {e['end']}s] SUMMARY: {e['summary']}"
|
|
168
|
+
screenshot_file = e.get("screenshot") or e.get("screenshot_path")
|
|
169
|
+
if screenshot_file and Path(screenshot_file).exists():
|
|
170
|
+
line += f" PROOF: {Path(screenshot_file).name}"
|
|
171
|
+
image_parts.append(
|
|
172
|
+
types.Part.from_text(text=f"PROOF: {Path(screenshot_file).name}")
|
|
173
|
+
)
|
|
174
|
+
image_parts.append(
|
|
175
|
+
types.Part.from_bytes(
|
|
176
|
+
data=Path(screenshot_file).read_bytes(),
|
|
177
|
+
mime_type="image/jpeg",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
lines.append(line)
|
|
181
|
+
diff_text = "NEW BLACKBOARD ENTRIES ADDED: " + " | ".join(lines)
|
|
182
|
+
contents.append(
|
|
183
|
+
types.Content(
|
|
184
|
+
role="user",
|
|
185
|
+
parts=[types.Part.from_text(text=diff_text)],
|
|
186
|
+
)
|
|
187
|
+
)
|
|
188
|
+
if image_parts:
|
|
189
|
+
contents.append(types.Content(role="user", parts=image_parts))
|
|
190
|
+
last_ledger_index = len(analyzer.blackboard_entries)
|
|
191
|
+
return last_ledger_index
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _append_turn_guidance(
|
|
195
|
+
contents: list, iterations: int, max_iterations: int, is_final_turn: bool
|
|
196
|
+
) -> None:
|
|
197
|
+
"""Appends per-turn steering messages (final-turn notice or progress nudge)."""
|
|
198
|
+
if is_final_turn:
|
|
199
|
+
contents.append(
|
|
200
|
+
types.Content(
|
|
201
|
+
role="user",
|
|
202
|
+
parts=[
|
|
203
|
+
types.Part.from_text(
|
|
204
|
+
text=(
|
|
205
|
+
"This is your final iteration; all"
|
|
206
|
+
" tools are stripped, and you must"
|
|
207
|
+
" provide your final answer directly."
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
],
|
|
211
|
+
)
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
if iterations > 1:
|
|
215
|
+
contents.append(
|
|
216
|
+
types.Content(
|
|
217
|
+
role="user",
|
|
218
|
+
parts=[
|
|
219
|
+
types.Part.from_text(
|
|
220
|
+
text=(
|
|
221
|
+
"You have not completed the video"
|
|
222
|
+
" analysis yet (iteration"
|
|
223
|
+
f" {iterations} of"
|
|
224
|
+
f" {max_iterations})."
|
|
225
|
+
)
|
|
226
|
+
)
|
|
227
|
+
],
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _record_main_llm_trace(
|
|
233
|
+
ctx, trace_id, contents: list, system_prompt: str, tools_declaration, is_final_turn: bool
|
|
234
|
+
) -> None:
|
|
235
|
+
"""Records the sanitized coordinator LLM call into the trace store."""
|
|
236
|
+
serialized_contents = []
|
|
237
|
+
for c in contents:
|
|
238
|
+
if hasattr(c, "model_dump"):
|
|
239
|
+
dumped = c.model_dump()
|
|
240
|
+
if isinstance(dumped, dict) and "parts" in dumped and dumped["parts"]:
|
|
241
|
+
for p in dumped["parts"]:
|
|
242
|
+
if (
|
|
243
|
+
isinstance(p, dict)
|
|
244
|
+
and "inline_data" in p
|
|
245
|
+
and p["inline_data"]
|
|
246
|
+
and "data" in p["inline_data"]
|
|
247
|
+
):
|
|
248
|
+
p["inline_data"]["data"] = "<image_bytes_sanitized>"
|
|
249
|
+
serialized_contents.append(dumped)
|
|
250
|
+
else:
|
|
251
|
+
serialized_contents.append(str(c))
|
|
252
|
+
|
|
253
|
+
ctx.data_engine.record_trace(
|
|
254
|
+
type="llm_call",
|
|
255
|
+
name="video_analyzer_main",
|
|
256
|
+
payload={
|
|
257
|
+
"contents": serialized_contents,
|
|
258
|
+
"system_instruction": system_prompt,
|
|
259
|
+
"tools": (
|
|
260
|
+
[t.name for t in tools_declaration]
|
|
261
|
+
if (tools_declaration and not is_final_turn)
|
|
262
|
+
else []
|
|
263
|
+
),
|
|
264
|
+
},
|
|
265
|
+
parent_trace_id=trace_id,
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
async def _stream_main_turn(
|
|
270
|
+
analyzer,
|
|
271
|
+
ctx,
|
|
272
|
+
contents: list,
|
|
273
|
+
system_prompt: str,
|
|
274
|
+
temperature: float,
|
|
275
|
+
thinking_level,
|
|
276
|
+
tools_declaration,
|
|
277
|
+
is_final_turn: bool,
|
|
278
|
+
):
|
|
279
|
+
"""Runs one traced, retry-wrapped streaming turn of the main coordinator."""
|
|
280
|
+
with TraceSpan(name="gemini_main_agent_call") as span:
|
|
281
|
+
trace_id = CURRENT_TRACE_ID.get()
|
|
282
|
+
|
|
283
|
+
if ctx.data_engine and trace_id:
|
|
284
|
+
_record_main_llm_trace(
|
|
285
|
+
ctx, trace_id, contents, system_prompt, tools_declaration, is_final_turn
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
async def run_stream():
|
|
289
|
+
return await _consume_main_stream(
|
|
290
|
+
analyzer,
|
|
291
|
+
ctx,
|
|
292
|
+
span,
|
|
293
|
+
trace_id,
|
|
294
|
+
contents,
|
|
295
|
+
system_prompt,
|
|
296
|
+
temperature,
|
|
297
|
+
thinking_level,
|
|
298
|
+
tools_declaration,
|
|
299
|
+
is_final_turn,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
response = await _va._invoke_with_retry(
|
|
303
|
+
lambda: asyncio.wait_for(run_stream(), timeout=300),
|
|
304
|
+
"Native Gemini video coordinator",
|
|
305
|
+
)
|
|
306
|
+
span.result = (
|
|
307
|
+
f"Function calls: {len(response.function_calls)}"
|
|
308
|
+
if response.function_calls
|
|
309
|
+
else "Final answer"
|
|
310
|
+
)
|
|
311
|
+
return response
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
async def _consume_main_stream(
|
|
315
|
+
analyzer,
|
|
316
|
+
ctx,
|
|
317
|
+
span,
|
|
318
|
+
trace_id,
|
|
319
|
+
contents: list,
|
|
320
|
+
system_prompt: str,
|
|
321
|
+
temperature: float,
|
|
322
|
+
thinking_level,
|
|
323
|
+
tools_declaration,
|
|
324
|
+
is_final_turn: bool,
|
|
325
|
+
) -> _DummyResponse:
|
|
326
|
+
"""Starts the coordinator stream and aggregates it into a response object."""
|
|
327
|
+
stream = await asyncio.wait_for(
|
|
328
|
+
analyzer.client.aio.models.generate_content_stream(
|
|
329
|
+
model=analyzer.model_name,
|
|
330
|
+
contents=contents,
|
|
331
|
+
config=types.GenerateContentConfig(
|
|
332
|
+
system_instruction=system_prompt,
|
|
333
|
+
temperature=temperature,
|
|
334
|
+
tools=[]
|
|
335
|
+
if is_final_turn
|
|
336
|
+
else [types.Tool(function_declarations=tools_declaration)],
|
|
337
|
+
safety_settings=SAFETY_SETTINGS_BLOCK_NONE,
|
|
338
|
+
**(
|
|
339
|
+
{"thinking_config": types.ThinkingConfig(thinking_level=thinking_level)}
|
|
340
|
+
if thinking_level
|
|
341
|
+
else {}
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
),
|
|
345
|
+
timeout=60,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
text = ""
|
|
349
|
+
accumulated_parts = []
|
|
350
|
+
function_calls = []
|
|
351
|
+
|
|
352
|
+
async for chunk in stream:
|
|
353
|
+
usage = normalize_usage(getattr(chunk, "usage_metadata", None))
|
|
354
|
+
if usage:
|
|
355
|
+
span.payload["usage_metadata"] = usage
|
|
356
|
+
|
|
357
|
+
chunk_text = chunk.text or ""
|
|
358
|
+
text += chunk_text
|
|
359
|
+
if ctx.data_engine and trace_id and chunk_text:
|
|
360
|
+
ctx.data_engine.stream_output(trace_id, chunk_text, is_thinking=False)
|
|
361
|
+
|
|
362
|
+
if chunk.function_calls:
|
|
363
|
+
function_calls.extend(chunk.function_calls)
|
|
364
|
+
|
|
365
|
+
candidates = getattr(chunk, "candidates", None)
|
|
366
|
+
if candidates and isinstance(candidates, list) and len(candidates) > 0:
|
|
367
|
+
content = getattr(candidates[0], "content", None)
|
|
368
|
+
parts = getattr(content, "parts", None) if content else None
|
|
369
|
+
if parts:
|
|
370
|
+
for part in parts:
|
|
371
|
+
accumulated_parts.append(part)
|
|
372
|
+
if getattr(part, "thought", False) and part.text:
|
|
373
|
+
if ctx.data_engine and trace_id:
|
|
374
|
+
ctx.data_engine.stream_output(
|
|
375
|
+
trace_id,
|
|
376
|
+
part.text,
|
|
377
|
+
is_thinking=True,
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
if chunk.function_calls:
|
|
381
|
+
for fc in chunk.function_calls:
|
|
382
|
+
accumulated_parts.append(types.Part(function_call=fc))
|
|
383
|
+
|
|
384
|
+
return _DummyResponse(text, function_calls, accumulated_parts)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
async def _execute_native_tool_calls(analyzer, function_calls: list) -> list:
|
|
388
|
+
"""Executes coordinator tool calls (parallel where safe) and collects responses."""
|
|
389
|
+
sub_agent_calls = [
|
|
390
|
+
fc
|
|
391
|
+
for fc in function_calls
|
|
392
|
+
if (fc.name.split(":")[-1] if ":" in fc.name else fc.name) == "spawn_sub_agent"
|
|
393
|
+
]
|
|
394
|
+
audio_only_calls = [
|
|
395
|
+
fc
|
|
396
|
+
for fc in function_calls
|
|
397
|
+
if (fc.name.split(":")[-1] if ":" in fc.name else fc.name) == "analyze_audio_only"
|
|
398
|
+
]
|
|
399
|
+
other_calls = [
|
|
400
|
+
fc
|
|
401
|
+
for fc in function_calls
|
|
402
|
+
if (fc.name.split(":")[-1] if ":" in fc.name else fc.name)
|
|
403
|
+
not in ["spawn_sub_agent", "analyze_audio_only"]
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
tool_response_parts = []
|
|
407
|
+
|
|
408
|
+
if sub_agent_calls:
|
|
409
|
+
await _run_spawn_calls(analyzer, sub_agent_calls, tool_response_parts)
|
|
410
|
+
|
|
411
|
+
if audio_only_calls:
|
|
412
|
+
await _run_audio_calls(analyzer, audio_only_calls, tool_response_parts)
|
|
413
|
+
|
|
414
|
+
await _run_other_calls(analyzer, other_calls, tool_response_parts)
|
|
415
|
+
|
|
416
|
+
return tool_response_parts
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
async def _run_spawn_calls(analyzer, sub_agent_calls: list, tool_response_parts: list) -> None:
|
|
420
|
+
"""Runs spawn_sub_agent calls in parallel and appends their responses."""
|
|
421
|
+
logger.info(f"Executing {len(sub_agent_calls)} sub-agent calls in parallel...")
|
|
422
|
+
|
|
423
|
+
async def bounded_spawn(fc):
|
|
424
|
+
# exec_spawn_sub_agent limits each actual model leaf.
|
|
425
|
+
# Holding the same semaphore around the coordinator
|
|
426
|
+
# would deadlock when all slots wait for child slots.
|
|
427
|
+
return await analyzer.exec_spawn_sub_agent(
|
|
428
|
+
fc.args.get("start_time"),
|
|
429
|
+
fc.args.get("end_time"),
|
|
430
|
+
fc.args.get("specific_query"),
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
tasks = [bounded_spawn(fc) for fc in sub_agent_calls]
|
|
434
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
435
|
+
|
|
436
|
+
for fc, result in zip(sub_agent_calls, results):
|
|
437
|
+
if isinstance(result, Exception):
|
|
438
|
+
error_msg = f"Sub-agent failed. Error: {result}"
|
|
439
|
+
logger.error(error_msg)
|
|
440
|
+
tool_response_parts.append(
|
|
441
|
+
types.Part.from_function_response(name=fc.name, response={"error": error_msg})
|
|
442
|
+
)
|
|
443
|
+
else:
|
|
444
|
+
tool_response_parts.append(
|
|
445
|
+
types.Part.from_function_response(name=fc.name, response={"result": result})
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
async def _run_audio_calls(analyzer, audio_only_calls: list, tool_response_parts: list) -> None:
|
|
450
|
+
"""Runs analyze_audio_only calls in parallel and appends their responses."""
|
|
451
|
+
logger.info(f"Executing {len(audio_only_calls)} analyze_audio_only calls in parallel...")
|
|
452
|
+
|
|
453
|
+
async def bounded_audio_spawn(fc):
|
|
454
|
+
async with _va.API_SEMAPHORE:
|
|
455
|
+
return await analyzer.exec_analyze_audio_only(
|
|
456
|
+
fc.args.get("start_time"),
|
|
457
|
+
fc.args.get("end_time"),
|
|
458
|
+
fc.args.get("specific_query"),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
tasks = [bounded_audio_spawn(fc) for fc in audio_only_calls]
|
|
462
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
463
|
+
|
|
464
|
+
for fc, result in zip(audio_only_calls, results):
|
|
465
|
+
if isinstance(result, Exception):
|
|
466
|
+
error_msg = f"Audio analysis failed. Error: {result}"
|
|
467
|
+
logger.error(error_msg)
|
|
468
|
+
tool_response_parts.append(
|
|
469
|
+
types.Part.from_function_response(name=fc.name, response={"error": error_msg})
|
|
470
|
+
)
|
|
471
|
+
else:
|
|
472
|
+
tool_response_parts.append(
|
|
473
|
+
types.Part.from_function_response(name=fc.name, response={"result": result})
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
async def _run_other_calls(analyzer, other_calls: list, tool_response_parts: list) -> None:
|
|
478
|
+
"""Runs remaining coordinator tool calls sequentially."""
|
|
479
|
+
for fc in other_calls:
|
|
480
|
+
name = fc.name.split(":")[-1] if ":" in fc.name else fc.name
|
|
481
|
+
args = fc.args
|
|
482
|
+
logger.info(f"Executing tool '{name}' sequentially...")
|
|
483
|
+
|
|
484
|
+
if name == "extract_segment_metadata":
|
|
485
|
+
try:
|
|
486
|
+
res = await analyzer.exec_extract_segment_metadata(
|
|
487
|
+
args.get("start_time"), args.get("end_time")
|
|
488
|
+
)
|
|
489
|
+
tool_response_parts.append(
|
|
490
|
+
types.Part.from_function_response(name=name, response={"result": res})
|
|
491
|
+
)
|
|
492
|
+
except Exception as e:
|
|
493
|
+
tool_response_parts.append(
|
|
494
|
+
types.Part.from_function_response(name=name, response={"error": str(e)})
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
async def _cleanup_native_run(analyzer) -> None:
|
|
499
|
+
"""Releases remaining cloud files and temporary local media after a run."""
|
|
500
|
+
if analyzer.cloud_files_to_cleanup:
|
|
501
|
+
logger.info(
|
|
502
|
+
f"Cleaning up {len(analyzer.cloud_files_to_cleanup)} remaining"
|
|
503
|
+
" cloud files in parallel..."
|
|
504
|
+
)
|
|
505
|
+
tasks = [
|
|
506
|
+
asyncio.wait_for(analyzer.client.aio.files.delete(name=name), timeout=30)
|
|
507
|
+
for name in list(analyzer.cloud_files_to_cleanup)
|
|
508
|
+
]
|
|
509
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
510
|
+
analyzer.cloud_files_to_cleanup.clear()
|
|
511
|
+
|
|
512
|
+
if os.environ.get("KEEP_VIDEOS") or os.environ.get("ARTEMIS_DEBUG"):
|
|
513
|
+
logger.info("Skipping cleanup of local video files and directories due to debug mode.")
|
|
514
|
+
else:
|
|
515
|
+
for path in list(analyzer.local_files_to_cleanup):
|
|
516
|
+
try:
|
|
517
|
+
if path.exists():
|
|
518
|
+
logger.info(f"Cleaning up local file {path}...")
|
|
519
|
+
path.unlink()
|
|
520
|
+
except Exception as e:
|
|
521
|
+
logger.error(f"Failed to delete local file {path}: {e}")
|
|
522
|
+
|
|
523
|
+
for dir_path in list(analyzer.local_dirs_to_cleanup):
|
|
524
|
+
try:
|
|
525
|
+
resolved = dir_path.resolve()
|
|
526
|
+
system_dirs = {
|
|
527
|
+
Path("/").resolve(),
|
|
528
|
+
Path("/tmp").resolve(),
|
|
529
|
+
Path("/var/tmp").resolve(),
|
|
530
|
+
Path.home().resolve(),
|
|
531
|
+
Path(tempfile.gettempdir()).resolve(),
|
|
532
|
+
}
|
|
533
|
+
if resolved in system_dirs:
|
|
534
|
+
logger.debug(f"Skipping deletion of root/system directory: {dir_path}")
|
|
535
|
+
continue
|
|
536
|
+
|
|
537
|
+
if dir_path.exists():
|
|
538
|
+
logger.info(f"Cleaning up local directory {dir_path}...")
|
|
539
|
+
shutil.rmtree(dir_path, ignore_errors=True)
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.error(f"Failed to delete local directory {dir_path}: {e}")
|