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,1245 @@
|
|
|
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
|
+
from collections.abc import Iterator
|
|
16
|
+
from contextlib import contextmanager
|
|
17
|
+
import difflib
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
import shutil
|
|
21
|
+
import sqlite3
|
|
22
|
+
import threading
|
|
23
|
+
from typing import Any
|
|
24
|
+
import uuid
|
|
25
|
+
from uuid import UUID
|
|
26
|
+
|
|
27
|
+
from artemis.data_engine.models import (
|
|
28
|
+
BackgroundTaskRecord,
|
|
29
|
+
FailedOutputRecord,
|
|
30
|
+
HistoryChunkRecord,
|
|
31
|
+
ImageRecord,
|
|
32
|
+
SessionMetadata,
|
|
33
|
+
StepRecord,
|
|
34
|
+
TraceRecord,
|
|
35
|
+
VideoRecordingRecord,
|
|
36
|
+
)
|
|
37
|
+
from third_party.mobile_use.utils.logger import get_logger
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _safe_uuid(val: Any) -> UUID | str:
|
|
41
|
+
if val is None:
|
|
42
|
+
return ""
|
|
43
|
+
if isinstance(val, UUID):
|
|
44
|
+
return val
|
|
45
|
+
try:
|
|
46
|
+
return UUID(str(val))
|
|
47
|
+
except (ValueError, TypeError):
|
|
48
|
+
return str(val)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SafeJSONEncoder(json.JSONEncoder):
|
|
52
|
+
def default(self, obj):
|
|
53
|
+
if isinstance(obj, bytes):
|
|
54
|
+
return f"<binary data: {len(obj)} bytes>"
|
|
55
|
+
try:
|
|
56
|
+
return super().default(obj)
|
|
57
|
+
except TypeError:
|
|
58
|
+
return str(obj)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
logger = get_logger(__name__)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class StorageManager:
|
|
65
|
+
"""Manages persistence for Data Engine using SQLite and File System."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, db_path: str | Path, base_trace_dir: str | Path, *, read_only: bool = False):
|
|
68
|
+
self.db_path = Path(db_path)
|
|
69
|
+
self.base_trace_dir = Path(base_trace_dir)
|
|
70
|
+
self._lock = threading.RLock()
|
|
71
|
+
self.read_only = read_only
|
|
72
|
+
|
|
73
|
+
if read_only:
|
|
74
|
+
# Offline readers (MCP trace inspection) never create directories,
|
|
75
|
+
# tables or WAL files: they open the existing database read-only.
|
|
76
|
+
if not self.db_path.exists():
|
|
77
|
+
raise FileNotFoundError(f"Data engine database not found: {self.db_path}")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# Ensure directories exist
|
|
81
|
+
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
self.base_trace_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
self._init_db()
|
|
85
|
+
|
|
86
|
+
@contextmanager
|
|
87
|
+
def _get_connection(self) -> Iterator[sqlite3.Connection]:
|
|
88
|
+
with self._lock:
|
|
89
|
+
if self.read_only:
|
|
90
|
+
conn = sqlite3.connect(
|
|
91
|
+
f"{self.db_path.resolve().as_uri()}?mode=ro", uri=True, timeout=30.0
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
conn = sqlite3.connect(self.db_path, timeout=30.0)
|
|
95
|
+
conn.row_factory = sqlite3.Row
|
|
96
|
+
try:
|
|
97
|
+
yield conn
|
|
98
|
+
finally:
|
|
99
|
+
conn.close()
|
|
100
|
+
|
|
101
|
+
def _init_db(self):
|
|
102
|
+
"""Initialize database tables."""
|
|
103
|
+
with self._get_connection() as conn:
|
|
104
|
+
conn.execute("PRAGMA journal_mode=WAL")
|
|
105
|
+
conn.execute("""
|
|
106
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
107
|
+
session_id TEXT PRIMARY KEY,
|
|
108
|
+
initial_goal TEXT,
|
|
109
|
+
start_time REAL,
|
|
110
|
+
end_time REAL,
|
|
111
|
+
status TEXT,
|
|
112
|
+
device_info TEXT,
|
|
113
|
+
video_filepath TEXT
|
|
114
|
+
)
|
|
115
|
+
""")
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
conn.execute("ALTER TABLE sessions ADD COLUMN video_filepath TEXT")
|
|
119
|
+
except sqlite3.OperationalError:
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
conn.execute("ALTER TABLE sessions ADD COLUMN pid INTEGER")
|
|
124
|
+
except sqlite3.OperationalError:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
conn.execute("""
|
|
128
|
+
CREATE TABLE IF NOT EXISTS images (
|
|
129
|
+
image_name TEXT PRIMARY KEY,
|
|
130
|
+
timestamp REAL,
|
|
131
|
+
ocr_result TEXT,
|
|
132
|
+
ui_tree TEXT,
|
|
133
|
+
extra_metadata TEXT
|
|
134
|
+
)
|
|
135
|
+
""")
|
|
136
|
+
|
|
137
|
+
conn.execute("""
|
|
138
|
+
CREATE TABLE IF NOT EXISTS steps (
|
|
139
|
+
step_id TEXT PRIMARY KEY,
|
|
140
|
+
session_id TEXT,
|
|
141
|
+
step_number INTEGER,
|
|
142
|
+
timestamp REAL,
|
|
143
|
+
pre_image_name TEXT,
|
|
144
|
+
post_image_name TEXT,
|
|
145
|
+
summary TEXT,
|
|
146
|
+
action_taken TEXT,
|
|
147
|
+
operator_raw_thinking TEXT,
|
|
148
|
+
operator_native_thinking TEXT,
|
|
149
|
+
last_execution_result TEXT,
|
|
150
|
+
extra_metadata TEXT,
|
|
151
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id),
|
|
152
|
+
FOREIGN KEY(pre_image_name) REFERENCES images(image_name),
|
|
153
|
+
FOREIGN KEY(post_image_name) REFERENCES images(image_name)
|
|
154
|
+
)
|
|
155
|
+
""")
|
|
156
|
+
try:
|
|
157
|
+
conn.execute("ALTER TABLE steps ADD COLUMN action_taken TEXT")
|
|
158
|
+
except sqlite3.OperationalError:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
conn.execute("ALTER TABLE steps ADD COLUMN operator_raw_thinking TEXT")
|
|
163
|
+
except sqlite3.OperationalError:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
conn.execute("ALTER TABLE steps ADD COLUMN operator_native_thinking TEXT")
|
|
168
|
+
except sqlite3.OperationalError:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
conn.execute("ALTER TABLE steps ADD COLUMN last_execution_result TEXT")
|
|
173
|
+
except sqlite3.OperationalError:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
conn.execute("""
|
|
177
|
+
CREATE TABLE IF NOT EXISTS traces (
|
|
178
|
+
trace_id TEXT PRIMARY KEY,
|
|
179
|
+
session_id TEXT,
|
|
180
|
+
step_id TEXT,
|
|
181
|
+
parent_trace_id TEXT,
|
|
182
|
+
type TEXT,
|
|
183
|
+
name TEXT,
|
|
184
|
+
timestamp REAL,
|
|
185
|
+
duration REAL,
|
|
186
|
+
status TEXT,
|
|
187
|
+
payload TEXT,
|
|
188
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id),
|
|
189
|
+
FOREIGN KEY(step_id) REFERENCES steps(step_id),
|
|
190
|
+
FOREIGN KEY(parent_trace_id) REFERENCES traces(trace_id)
|
|
191
|
+
)
|
|
192
|
+
""")
|
|
193
|
+
|
|
194
|
+
conn.execute("""
|
|
195
|
+
CREATE TABLE IF NOT EXISTS failed_outputs (
|
|
196
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
197
|
+
session_id TEXT,
|
|
198
|
+
trace_id TEXT,
|
|
199
|
+
model_name TEXT,
|
|
200
|
+
prompt TEXT,
|
|
201
|
+
raw_output TEXT,
|
|
202
|
+
error_message TEXT,
|
|
203
|
+
timestamp REAL,
|
|
204
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id),
|
|
205
|
+
FOREIGN KEY(trace_id) REFERENCES traces(trace_id)
|
|
206
|
+
)
|
|
207
|
+
""")
|
|
208
|
+
|
|
209
|
+
conn.execute("""
|
|
210
|
+
CREATE TABLE IF NOT EXISTS background_tasks (
|
|
211
|
+
task_id TEXT PRIMARY KEY,
|
|
212
|
+
session_id TEXT,
|
|
213
|
+
summary TEXT,
|
|
214
|
+
status TEXT,
|
|
215
|
+
start_time REAL,
|
|
216
|
+
end_time REAL,
|
|
217
|
+
trace_id TEXT,
|
|
218
|
+
logs TEXT,
|
|
219
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id)
|
|
220
|
+
)
|
|
221
|
+
""")
|
|
222
|
+
conn.execute("""
|
|
223
|
+
CREATE TABLE IF NOT EXISTS history_chunks (
|
|
224
|
+
chunk_id TEXT PRIMARY KEY,
|
|
225
|
+
session_id TEXT,
|
|
226
|
+
start_step_id TEXT,
|
|
227
|
+
end_step_id TEXT,
|
|
228
|
+
start_step_number INTEGER,
|
|
229
|
+
end_step_number INTEGER,
|
|
230
|
+
source_step_ids TEXT,
|
|
231
|
+
subgoal_hash TEXT,
|
|
232
|
+
version INTEGER,
|
|
233
|
+
status TEXT,
|
|
234
|
+
band1 TEXT,
|
|
235
|
+
band2 TEXT,
|
|
236
|
+
band3 TEXT,
|
|
237
|
+
rendered_text TEXT,
|
|
238
|
+
created_at REAL,
|
|
239
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id)
|
|
240
|
+
)
|
|
241
|
+
""")
|
|
242
|
+
conn.execute("""
|
|
243
|
+
CREATE TABLE IF NOT EXISTS video_recordings (
|
|
244
|
+
video_id TEXT PRIMARY KEY,
|
|
245
|
+
session_id TEXT,
|
|
246
|
+
device_id TEXT,
|
|
247
|
+
start_time REAL,
|
|
248
|
+
end_time REAL,
|
|
249
|
+
local_video_path TEXT,
|
|
250
|
+
status TEXT NOT NULL DEFAULT 'recording',
|
|
251
|
+
error TEXT,
|
|
252
|
+
FOREIGN KEY(session_id) REFERENCES sessions(session_id)
|
|
253
|
+
)
|
|
254
|
+
""")
|
|
255
|
+
video_recording_columns = {
|
|
256
|
+
row[1] for row in conn.execute("PRAGMA table_info(video_recordings)").fetchall()
|
|
257
|
+
}
|
|
258
|
+
if "status" not in video_recording_columns:
|
|
259
|
+
conn.execute(
|
|
260
|
+
"ALTER TABLE video_recordings ADD COLUMN status TEXT "
|
|
261
|
+
"NOT NULL DEFAULT 'recording'"
|
|
262
|
+
)
|
|
263
|
+
conn.execute(
|
|
264
|
+
"UPDATE video_recordings SET status = 'ready' WHERE end_time IS NOT NULL"
|
|
265
|
+
)
|
|
266
|
+
if "error" not in video_recording_columns:
|
|
267
|
+
conn.execute("ALTER TABLE video_recordings ADD COLUMN error TEXT")
|
|
268
|
+
try:
|
|
269
|
+
conn.execute("ALTER TABLE background_tasks ADD COLUMN logs TEXT")
|
|
270
|
+
except sqlite3.OperationalError:
|
|
271
|
+
pass
|
|
272
|
+
conn.commit()
|
|
273
|
+
logger.info(f"Database initialized at {self.db_path}")
|
|
274
|
+
|
|
275
|
+
def create_session(self, session: SessionMetadata):
|
|
276
|
+
"""Insert a new session record."""
|
|
277
|
+
with self._get_connection() as conn:
|
|
278
|
+
conn.execute(
|
|
279
|
+
"""
|
|
280
|
+
INSERT OR REPLACE INTO sessions (session_id, initial_goal, start_time, end_time, status, device_info, pid, video_filepath)
|
|
281
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
282
|
+
""",
|
|
283
|
+
(
|
|
284
|
+
str(session.session_id),
|
|
285
|
+
session.initial_goal,
|
|
286
|
+
session.start_time,
|
|
287
|
+
session.end_time,
|
|
288
|
+
session.status,
|
|
289
|
+
json.dumps(session.device_info),
|
|
290
|
+
session.pid,
|
|
291
|
+
session.video_filepath,
|
|
292
|
+
),
|
|
293
|
+
)
|
|
294
|
+
conn.commit()
|
|
295
|
+
|
|
296
|
+
# Create session directory
|
|
297
|
+
session_dir = self.base_trace_dir / str(session.session_id)
|
|
298
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
299
|
+
logger.info(f"Session directory created at {session_dir}")
|
|
300
|
+
|
|
301
|
+
def update_session(self, session: SessionMetadata):
|
|
302
|
+
"""Update an existing session record."""
|
|
303
|
+
with self._get_connection() as conn:
|
|
304
|
+
conn.execute(
|
|
305
|
+
"""
|
|
306
|
+
UPDATE sessions
|
|
307
|
+
SET end_time = ?, status = ?, device_info = ?, video_filepath = ?
|
|
308
|
+
WHERE session_id = ?
|
|
309
|
+
""",
|
|
310
|
+
(
|
|
311
|
+
session.end_time,
|
|
312
|
+
session.status,
|
|
313
|
+
json.dumps(session.device_info),
|
|
314
|
+
session.video_filepath,
|
|
315
|
+
str(session.session_id),
|
|
316
|
+
),
|
|
317
|
+
)
|
|
318
|
+
conn.commit()
|
|
319
|
+
|
|
320
|
+
def create_video_recording(self, record: VideoRecordingRecord):
|
|
321
|
+
"""Insert a new video recording record."""
|
|
322
|
+
with self._get_connection() as conn:
|
|
323
|
+
conn.execute(
|
|
324
|
+
"""
|
|
325
|
+
INSERT INTO video_recordings (
|
|
326
|
+
video_id, session_id, device_id, start_time, end_time,
|
|
327
|
+
local_video_path, status, error
|
|
328
|
+
)
|
|
329
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
330
|
+
""",
|
|
331
|
+
(
|
|
332
|
+
str(record.video_id),
|
|
333
|
+
str(record.session_id) if record.session_id else None,
|
|
334
|
+
record.device_id,
|
|
335
|
+
record.start_time,
|
|
336
|
+
record.end_time,
|
|
337
|
+
record.local_video_path,
|
|
338
|
+
record.status,
|
|
339
|
+
record.error,
|
|
340
|
+
),
|
|
341
|
+
)
|
|
342
|
+
conn.commit()
|
|
343
|
+
|
|
344
|
+
def update_video_recording(self, record: VideoRecordingRecord):
|
|
345
|
+
"""Update an existing video recording record and session video path."""
|
|
346
|
+
with self._get_connection() as conn:
|
|
347
|
+
conn.execute(
|
|
348
|
+
"""
|
|
349
|
+
UPDATE video_recordings
|
|
350
|
+
SET end_time = ?, local_video_path = ?, status = ?, error = ?
|
|
351
|
+
WHERE video_id = ?
|
|
352
|
+
""",
|
|
353
|
+
(
|
|
354
|
+
record.end_time,
|
|
355
|
+
record.local_video_path,
|
|
356
|
+
record.status,
|
|
357
|
+
record.error,
|
|
358
|
+
str(record.video_id),
|
|
359
|
+
),
|
|
360
|
+
)
|
|
361
|
+
if record.session_id and record.local_video_path:
|
|
362
|
+
conn.execute(
|
|
363
|
+
"""
|
|
364
|
+
UPDATE sessions
|
|
365
|
+
SET video_filepath = ?
|
|
366
|
+
WHERE session_id = ?
|
|
367
|
+
""",
|
|
368
|
+
(
|
|
369
|
+
record.local_video_path,
|
|
370
|
+
str(record.session_id),
|
|
371
|
+
),
|
|
372
|
+
)
|
|
373
|
+
conn.commit()
|
|
374
|
+
|
|
375
|
+
def update_session_video_path(self, session_id: UUID, video_path: str):
|
|
376
|
+
"""Update video filepath across session and video_recordings tables."""
|
|
377
|
+
with self._get_connection() as conn:
|
|
378
|
+
conn.execute(
|
|
379
|
+
"UPDATE sessions SET video_filepath = ? WHERE session_id = ?",
|
|
380
|
+
(str(video_path), str(session_id)),
|
|
381
|
+
)
|
|
382
|
+
conn.execute(
|
|
383
|
+
"UPDATE video_recordings SET local_video_path = ? WHERE session_id = ?",
|
|
384
|
+
(str(video_path), str(session_id)),
|
|
385
|
+
)
|
|
386
|
+
conn.commit()
|
|
387
|
+
|
|
388
|
+
def get_video_recording(self, video_id: UUID) -> VideoRecordingRecord | None:
|
|
389
|
+
"""Retrieve a video recording record by ID."""
|
|
390
|
+
with self._get_connection() as conn:
|
|
391
|
+
cursor = conn.execute(
|
|
392
|
+
"SELECT * FROM video_recordings WHERE video_id = ?",
|
|
393
|
+
(str(video_id),),
|
|
394
|
+
)
|
|
395
|
+
row = cursor.fetchone()
|
|
396
|
+
if row:
|
|
397
|
+
return VideoRecordingRecord(
|
|
398
|
+
video_id=_safe_uuid(row["video_id"]),
|
|
399
|
+
session_id=_safe_uuid(row["session_id"]) if row["session_id"] else None,
|
|
400
|
+
device_id=row["device_id"],
|
|
401
|
+
start_time=row["start_time"],
|
|
402
|
+
end_time=row["end_time"],
|
|
403
|
+
local_video_path=row["local_video_path"],
|
|
404
|
+
status=row["status"]
|
|
405
|
+
if "status" in row.keys()
|
|
406
|
+
else ("ready" if row["end_time"] is not None else "recording"),
|
|
407
|
+
error=row["error"] if "error" in row.keys() else None,
|
|
408
|
+
)
|
|
409
|
+
return None
|
|
410
|
+
|
|
411
|
+
def create_image(self, image: ImageRecord):
|
|
412
|
+
"""Insert a new image record."""
|
|
413
|
+
with self._get_connection() as conn:
|
|
414
|
+
conn.execute(
|
|
415
|
+
"""
|
|
416
|
+
INSERT OR IGNORE INTO images (image_name, timestamp, ocr_result, ui_tree, extra_metadata)
|
|
417
|
+
VALUES (?, ?, ?, ?, ?)
|
|
418
|
+
""",
|
|
419
|
+
(
|
|
420
|
+
image.image_name,
|
|
421
|
+
image.timestamp,
|
|
422
|
+
json.dumps(image.ocr_result) if image.ocr_result else None,
|
|
423
|
+
json.dumps(image.ui_tree) if image.ui_tree else None,
|
|
424
|
+
json.dumps(image.extra_metadata),
|
|
425
|
+
),
|
|
426
|
+
)
|
|
427
|
+
conn.commit()
|
|
428
|
+
|
|
429
|
+
def get_image(self, image_name: str) -> ImageRecord | None:
|
|
430
|
+
"""Retrieve an image by its name."""
|
|
431
|
+
with self._get_connection() as conn:
|
|
432
|
+
cursor = conn.execute("SELECT * FROM images WHERE image_name = ?", (image_name,))
|
|
433
|
+
row = cursor.fetchone()
|
|
434
|
+
if row:
|
|
435
|
+
return ImageRecord(
|
|
436
|
+
image_name=row["image_name"],
|
|
437
|
+
timestamp=row["timestamp"],
|
|
438
|
+
ocr_result=json.loads(row["ocr_result"]) if row["ocr_result"] else None,
|
|
439
|
+
ui_tree=json.loads(row["ui_tree"]) if row["ui_tree"] else None,
|
|
440
|
+
extra_metadata=json.loads(row["extra_metadata"]),
|
|
441
|
+
)
|
|
442
|
+
return None
|
|
443
|
+
|
|
444
|
+
def update_image_data(self, image_name: str, ocr_result: Any | None, ui_tree: Any | None):
|
|
445
|
+
"""Update image record with OCR and UI tree if they are not already set."""
|
|
446
|
+
with self._get_connection() as conn:
|
|
447
|
+
ocr_json = json.dumps(ocr_result) if ocr_result is not None else None
|
|
448
|
+
ui_tree_json = json.dumps(ui_tree) if ui_tree is not None else None
|
|
449
|
+
|
|
450
|
+
conn.execute(
|
|
451
|
+
"""
|
|
452
|
+
UPDATE images
|
|
453
|
+
SET ocr_result = COALESCE(ocr_result, ?),
|
|
454
|
+
ui_tree = COALESCE(ui_tree, ?)
|
|
455
|
+
WHERE image_name = ?
|
|
456
|
+
""",
|
|
457
|
+
(ocr_json, ui_tree_json, image_name),
|
|
458
|
+
)
|
|
459
|
+
conn.commit()
|
|
460
|
+
|
|
461
|
+
def search_ui_by_hash(self, image_name: str, query: str, threshold: float = 0.6) -> list[dict]:
|
|
462
|
+
"""Search for text in UI data (XML and OCR) retrieved by image hash."""
|
|
463
|
+
record = self.get_image(image_name)
|
|
464
|
+
if not record:
|
|
465
|
+
return []
|
|
466
|
+
|
|
467
|
+
matches = []
|
|
468
|
+
|
|
469
|
+
def check_match(text, target_query):
|
|
470
|
+
if not text:
|
|
471
|
+
return 0.0
|
|
472
|
+
return difflib.SequenceMatcher(None, target_query.lower(), text.lower()).ratio()
|
|
473
|
+
|
|
474
|
+
# Search in ui_tree (raw XML elements)
|
|
475
|
+
if record.ui_tree:
|
|
476
|
+
for node in record.ui_tree:
|
|
477
|
+
best_ratio = 0.0
|
|
478
|
+
best_target = ""
|
|
479
|
+
|
|
480
|
+
# Check text
|
|
481
|
+
text = node.get("text")
|
|
482
|
+
if text:
|
|
483
|
+
ratio = check_match(text, query)
|
|
484
|
+
if ratio > best_ratio:
|
|
485
|
+
best_ratio = ratio
|
|
486
|
+
best_target = text
|
|
487
|
+
|
|
488
|
+
# Check content-desc
|
|
489
|
+
desc = node.get("content-desc")
|
|
490
|
+
if desc:
|
|
491
|
+
ratio = check_match(desc, query)
|
|
492
|
+
if ratio > best_ratio:
|
|
493
|
+
best_ratio = ratio
|
|
494
|
+
best_target = desc
|
|
495
|
+
|
|
496
|
+
if best_ratio > threshold:
|
|
497
|
+
matches.append(
|
|
498
|
+
{
|
|
499
|
+
"score": best_ratio,
|
|
500
|
+
"type": "xml",
|
|
501
|
+
"node": node,
|
|
502
|
+
"matched_text": best_target,
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# Search in ocr_result (raw OCR results)
|
|
507
|
+
if record.ocr_result:
|
|
508
|
+
for ocr in record.ocr_result:
|
|
509
|
+
text = ocr.get("text")
|
|
510
|
+
if text:
|
|
511
|
+
ratio = check_match(text, query)
|
|
512
|
+
if ratio > threshold:
|
|
513
|
+
matches.append(
|
|
514
|
+
{
|
|
515
|
+
"score": ratio,
|
|
516
|
+
"type": "ocr",
|
|
517
|
+
"node": ocr,
|
|
518
|
+
"matched_text": text,
|
|
519
|
+
}
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
# Sort by score descending
|
|
523
|
+
matches.sort(key=lambda x: x["score"], reverse=True)
|
|
524
|
+
return matches
|
|
525
|
+
|
|
526
|
+
def create_step(self, step: StepRecord):
|
|
527
|
+
"""Insert a new step record."""
|
|
528
|
+
with self._get_connection() as conn:
|
|
529
|
+
conn.execute(
|
|
530
|
+
"""
|
|
531
|
+
INSERT INTO steps (
|
|
532
|
+
step_id, session_id, step_number, timestamp,
|
|
533
|
+
pre_image_name, post_image_name,
|
|
534
|
+
summary, action_taken, operator_raw_thinking, operator_native_thinking, last_execution_result, extra_metadata
|
|
535
|
+
)
|
|
536
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
537
|
+
""",
|
|
538
|
+
(
|
|
539
|
+
str(step.step_id),
|
|
540
|
+
str(step.session_id),
|
|
541
|
+
step.step_number,
|
|
542
|
+
step.timestamp,
|
|
543
|
+
step.pre_image_name,
|
|
544
|
+
step.post_image_name,
|
|
545
|
+
step.summary,
|
|
546
|
+
json.dumps(step.action_taken) if step.action_taken else None,
|
|
547
|
+
step.operator_raw_thinking,
|
|
548
|
+
step.operator_native_thinking,
|
|
549
|
+
json.dumps(step.last_execution_result) if step.last_execution_result else None,
|
|
550
|
+
json.dumps(step.extra_metadata),
|
|
551
|
+
),
|
|
552
|
+
)
|
|
553
|
+
conn.commit()
|
|
554
|
+
|
|
555
|
+
def update_step_action(self, step_id: UUID, action_taken: dict):
|
|
556
|
+
"""Update the action taken for a step."""
|
|
557
|
+
with self._get_connection() as conn:
|
|
558
|
+
conn.execute(
|
|
559
|
+
"""
|
|
560
|
+
UPDATE steps
|
|
561
|
+
SET action_taken = ?
|
|
562
|
+
WHERE step_id = ?
|
|
563
|
+
""",
|
|
564
|
+
(
|
|
565
|
+
json.dumps(action_taken),
|
|
566
|
+
str(step_id),
|
|
567
|
+
),
|
|
568
|
+
)
|
|
569
|
+
conn.commit()
|
|
570
|
+
|
|
571
|
+
def update_step_summary(
|
|
572
|
+
self,
|
|
573
|
+
step_id: UUID,
|
|
574
|
+
summary: str | None,
|
|
575
|
+
*,
|
|
576
|
+
source: str | None = None,
|
|
577
|
+
version: int | None = None,
|
|
578
|
+
model: str | None = None,
|
|
579
|
+
status: str = "ready",
|
|
580
|
+
) -> bool:
|
|
581
|
+
"""Versioned, status-carrying summary write (replaces the blind overwrite).
|
|
582
|
+
|
|
583
|
+
Writes ``summary_status`` / ``summary_source`` / ``summary_version`` /
|
|
584
|
+
``summary_model`` into the step's ``extra_metadata``. Concurrent writes
|
|
585
|
+
to the same step are ordered by version: an explicit ``version`` lower
|
|
586
|
+
than the stored one is dropped; ``version=None`` auto-increments the
|
|
587
|
+
stored version. Status downgrades are refused without a newer explicit
|
|
588
|
+
version: ``pending`` never overwrites ``ready``/``failed``, and
|
|
589
|
+
``failed`` never overwrites ``ready``. The ``summary`` column itself is
|
|
590
|
+
only touched by a ``ready`` write with a non-None summary.
|
|
591
|
+
|
|
592
|
+
Returns True when the write was applied, False when it was dropped as
|
|
593
|
+
stale (or the step does not exist).
|
|
594
|
+
"""
|
|
595
|
+
with self._get_connection() as conn:
|
|
596
|
+
cursor = conn.execute(
|
|
597
|
+
"SELECT extra_metadata FROM steps WHERE step_id = ?",
|
|
598
|
+
(str(step_id),),
|
|
599
|
+
)
|
|
600
|
+
row = cursor.fetchone()
|
|
601
|
+
if row is None:
|
|
602
|
+
return False
|
|
603
|
+
|
|
604
|
+
try:
|
|
605
|
+
meta = json.loads(row["extra_metadata"]) if row["extra_metadata"] else {}
|
|
606
|
+
if not isinstance(meta, dict):
|
|
607
|
+
meta = {}
|
|
608
|
+
except Exception:
|
|
609
|
+
meta = {}
|
|
610
|
+
|
|
611
|
+
current_version = meta.get("summary_version")
|
|
612
|
+
current_version = current_version if isinstance(current_version, int) else 0
|
|
613
|
+
current_status = meta.get("summary_status")
|
|
614
|
+
|
|
615
|
+
if version is not None:
|
|
616
|
+
if version < current_version:
|
|
617
|
+
return False
|
|
618
|
+
new_version = version
|
|
619
|
+
is_newer = version > current_version
|
|
620
|
+
else:
|
|
621
|
+
new_version = current_version + 1
|
|
622
|
+
is_newer = False
|
|
623
|
+
|
|
624
|
+
# Status downgrades require an explicitly newer version.
|
|
625
|
+
if not is_newer:
|
|
626
|
+
if status == "pending" and current_status in ("ready", "failed"):
|
|
627
|
+
return False
|
|
628
|
+
if status == "failed" and current_status == "ready":
|
|
629
|
+
return False
|
|
630
|
+
|
|
631
|
+
meta["summary_status"] = status
|
|
632
|
+
meta["summary_version"] = new_version
|
|
633
|
+
if source is not None:
|
|
634
|
+
meta["summary_source"] = source
|
|
635
|
+
if model is not None:
|
|
636
|
+
meta["summary_model"] = model
|
|
637
|
+
|
|
638
|
+
if status == "ready" and summary is not None:
|
|
639
|
+
conn.execute(
|
|
640
|
+
"UPDATE steps SET summary = ?, extra_metadata = ? WHERE step_id = ?",
|
|
641
|
+
(summary, json.dumps(meta), str(step_id)),
|
|
642
|
+
)
|
|
643
|
+
else:
|
|
644
|
+
conn.execute(
|
|
645
|
+
"UPDATE steps SET extra_metadata = ? WHERE step_id = ?",
|
|
646
|
+
(json.dumps(meta), str(step_id)),
|
|
647
|
+
)
|
|
648
|
+
conn.commit()
|
|
649
|
+
return True
|
|
650
|
+
|
|
651
|
+
def create_history_chunk(self, chunk: HistoryChunkRecord):
|
|
652
|
+
"""Insert one history-chunk version row (append-only; never updates)."""
|
|
653
|
+
with self._get_connection() as conn:
|
|
654
|
+
conn.execute(
|
|
655
|
+
"""
|
|
656
|
+
INSERT INTO history_chunks (
|
|
657
|
+
chunk_id, session_id, start_step_id, end_step_id,
|
|
658
|
+
start_step_number, end_step_number, source_step_ids,
|
|
659
|
+
subgoal_hash, version, status, band1, band2, band3,
|
|
660
|
+
rendered_text, created_at
|
|
661
|
+
)
|
|
662
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
663
|
+
""",
|
|
664
|
+
(
|
|
665
|
+
str(chunk.chunk_id),
|
|
666
|
+
str(chunk.session_id),
|
|
667
|
+
chunk.start_step_id,
|
|
668
|
+
chunk.end_step_id,
|
|
669
|
+
chunk.start_step_number,
|
|
670
|
+
chunk.end_step_number,
|
|
671
|
+
json.dumps(chunk.source_step_ids),
|
|
672
|
+
chunk.subgoal_hash,
|
|
673
|
+
chunk.version,
|
|
674
|
+
chunk.status,
|
|
675
|
+
json.dumps(chunk.band1, ensure_ascii=False, cls=SafeJSONEncoder),
|
|
676
|
+
chunk.band2,
|
|
677
|
+
chunk.band3,
|
|
678
|
+
chunk.rendered_text,
|
|
679
|
+
chunk.created_at,
|
|
680
|
+
),
|
|
681
|
+
)
|
|
682
|
+
conn.commit()
|
|
683
|
+
|
|
684
|
+
def get_history_chunks(
|
|
685
|
+
self, session_id: UUID | str, *, all_versions: bool = False
|
|
686
|
+
) -> list[HistoryChunkRecord]:
|
|
687
|
+
"""History chunks of a session ordered by step range.
|
|
688
|
+
|
|
689
|
+
By default only the newest version per step range is returned; pass
|
|
690
|
+
``all_versions=True`` for the full append-only trail (audit/tests).
|
|
691
|
+
"""
|
|
692
|
+
with self._get_connection() as conn:
|
|
693
|
+
cursor = conn.execute(
|
|
694
|
+
"""
|
|
695
|
+
SELECT * FROM history_chunks WHERE session_id = ?
|
|
696
|
+
ORDER BY start_step_number ASC, version ASC, created_at ASC
|
|
697
|
+
""",
|
|
698
|
+
(str(session_id),),
|
|
699
|
+
)
|
|
700
|
+
rows = cursor.fetchall()
|
|
701
|
+
|
|
702
|
+
records: list[HistoryChunkRecord] = []
|
|
703
|
+
for row in rows:
|
|
704
|
+
try:
|
|
705
|
+
source_ids = json.loads(row["source_step_ids"]) if row["source_step_ids"] else []
|
|
706
|
+
except Exception:
|
|
707
|
+
source_ids = []
|
|
708
|
+
try:
|
|
709
|
+
band1 = json.loads(row["band1"]) if row["band1"] else {}
|
|
710
|
+
if not isinstance(band1, dict):
|
|
711
|
+
band1 = {}
|
|
712
|
+
except Exception:
|
|
713
|
+
band1 = {}
|
|
714
|
+
records.append(
|
|
715
|
+
HistoryChunkRecord(
|
|
716
|
+
chunk_id=row["chunk_id"],
|
|
717
|
+
session_id=row["session_id"],
|
|
718
|
+
start_step_id=row["start_step_id"],
|
|
719
|
+
end_step_id=row["end_step_id"],
|
|
720
|
+
start_step_number=row["start_step_number"],
|
|
721
|
+
end_step_number=row["end_step_number"],
|
|
722
|
+
source_step_ids=source_ids,
|
|
723
|
+
subgoal_hash=row["subgoal_hash"],
|
|
724
|
+
version=row["version"],
|
|
725
|
+
status=row["status"],
|
|
726
|
+
band1=band1,
|
|
727
|
+
band2=row["band2"],
|
|
728
|
+
band3=row["band3"],
|
|
729
|
+
rendered_text=row["rendered_text"],
|
|
730
|
+
created_at=row["created_at"],
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
if all_versions:
|
|
734
|
+
return records
|
|
735
|
+
|
|
736
|
+
latest: dict[tuple[int, int], HistoryChunkRecord] = {}
|
|
737
|
+
for rec in records:
|
|
738
|
+
key = (rec.start_step_number, rec.end_step_number)
|
|
739
|
+
current = latest.get(key)
|
|
740
|
+
if current is None or rec.version >= current.version:
|
|
741
|
+
latest[key] = rec
|
|
742
|
+
return sorted(latest.values(), key=lambda r: r.start_step_number)
|
|
743
|
+
|
|
744
|
+
def update_step_thinking(self, step_id: UUID, operator_raw_thinking: str):
|
|
745
|
+
"""Update the raw thinking process for a step."""
|
|
746
|
+
with self._get_connection() as conn:
|
|
747
|
+
conn.execute(
|
|
748
|
+
"""
|
|
749
|
+
UPDATE steps
|
|
750
|
+
SET operator_raw_thinking = ?
|
|
751
|
+
WHERE step_id = ?
|
|
752
|
+
""",
|
|
753
|
+
(
|
|
754
|
+
operator_raw_thinking,
|
|
755
|
+
str(step_id),
|
|
756
|
+
),
|
|
757
|
+
)
|
|
758
|
+
conn.commit()
|
|
759
|
+
|
|
760
|
+
def update_step_native_thinking(self, step_id: UUID, operator_native_thinking: str):
|
|
761
|
+
"""Update the native thinking process for a step."""
|
|
762
|
+
with self._get_connection() as conn:
|
|
763
|
+
conn.execute(
|
|
764
|
+
"""
|
|
765
|
+
UPDATE steps
|
|
766
|
+
SET operator_native_thinking = ?
|
|
767
|
+
WHERE step_id = ?
|
|
768
|
+
""",
|
|
769
|
+
(
|
|
770
|
+
operator_native_thinking,
|
|
771
|
+
str(step_id),
|
|
772
|
+
),
|
|
773
|
+
)
|
|
774
|
+
conn.commit()
|
|
775
|
+
|
|
776
|
+
def update_step_execution_result(
|
|
777
|
+
self,
|
|
778
|
+
step_id: UUID,
|
|
779
|
+
last_execution_result: dict,
|
|
780
|
+
post_image_name: str | None = None,
|
|
781
|
+
):
|
|
782
|
+
"""Update the validator execution result and post_image_name for a step."""
|
|
783
|
+
with self._get_connection() as conn:
|
|
784
|
+
if post_image_name:
|
|
785
|
+
cursor = conn.execute(
|
|
786
|
+
"SELECT pre_image_name FROM steps WHERE step_id = ?",
|
|
787
|
+
(str(step_id),),
|
|
788
|
+
)
|
|
789
|
+
row = cursor.fetchone()
|
|
790
|
+
if row and row["pre_image_name"] == post_image_name:
|
|
791
|
+
post_image_name = None
|
|
792
|
+
|
|
793
|
+
if post_image_name:
|
|
794
|
+
conn.execute(
|
|
795
|
+
"""
|
|
796
|
+
UPDATE steps
|
|
797
|
+
SET last_execution_result = ?, post_image_name = ?
|
|
798
|
+
WHERE step_id = ?
|
|
799
|
+
""",
|
|
800
|
+
(
|
|
801
|
+
json.dumps(last_execution_result),
|
|
802
|
+
post_image_name,
|
|
803
|
+
str(step_id),
|
|
804
|
+
),
|
|
805
|
+
)
|
|
806
|
+
else:
|
|
807
|
+
conn.execute(
|
|
808
|
+
"""
|
|
809
|
+
UPDATE steps
|
|
810
|
+
SET last_execution_result = ?
|
|
811
|
+
WHERE step_id = ?
|
|
812
|
+
""",
|
|
813
|
+
(
|
|
814
|
+
json.dumps(last_execution_result),
|
|
815
|
+
str(step_id),
|
|
816
|
+
),
|
|
817
|
+
)
|
|
818
|
+
conn.commit()
|
|
819
|
+
|
|
820
|
+
def get_session(self, session_id: UUID | str) -> SessionMetadata | None:
|
|
821
|
+
"""Retrieve a session by ID."""
|
|
822
|
+
with self._get_connection() as conn:
|
|
823
|
+
cursor = conn.execute(
|
|
824
|
+
"SELECT * FROM sessions WHERE session_id = ?",
|
|
825
|
+
(str(session_id),),
|
|
826
|
+
)
|
|
827
|
+
row = cursor.fetchone()
|
|
828
|
+
if row:
|
|
829
|
+
row_dict = dict(row)
|
|
830
|
+
return SessionMetadata(
|
|
831
|
+
session_id=_safe_uuid(row_dict["session_id"]),
|
|
832
|
+
initial_goal=row_dict["initial_goal"],
|
|
833
|
+
start_time=row_dict["start_time"],
|
|
834
|
+
end_time=row_dict["end_time"],
|
|
835
|
+
status=row_dict["status"],
|
|
836
|
+
device_info=json.loads(row_dict["device_info"]),
|
|
837
|
+
pid=row_dict.get("pid"),
|
|
838
|
+
video_filepath=row_dict.get("video_filepath"),
|
|
839
|
+
)
|
|
840
|
+
return None
|
|
841
|
+
|
|
842
|
+
def get_steps(self, session_id: UUID | str) -> list[StepRecord]:
|
|
843
|
+
"""Retrieve all steps for a session."""
|
|
844
|
+
steps = []
|
|
845
|
+
with self._get_connection() as conn:
|
|
846
|
+
cursor = conn.execute(
|
|
847
|
+
"SELECT * FROM steps WHERE session_id = ? ORDER BY step_number ASC",
|
|
848
|
+
(str(session_id),),
|
|
849
|
+
)
|
|
850
|
+
for row in cursor.fetchall():
|
|
851
|
+
action_taken = None
|
|
852
|
+
if "action_taken" in row.keys() and row["action_taken"]:
|
|
853
|
+
try:
|
|
854
|
+
action_taken = json.loads(row["action_taken"])
|
|
855
|
+
except (ValueError, TypeError):
|
|
856
|
+
pass
|
|
857
|
+
|
|
858
|
+
operator_raw_thinking = None
|
|
859
|
+
if "operator_raw_thinking" in row.keys() and row["operator_raw_thinking"]:
|
|
860
|
+
operator_raw_thinking = row["operator_raw_thinking"]
|
|
861
|
+
|
|
862
|
+
operator_native_thinking = None
|
|
863
|
+
if "operator_native_thinking" in row.keys() and row["operator_native_thinking"]:
|
|
864
|
+
operator_native_thinking = row["operator_native_thinking"]
|
|
865
|
+
|
|
866
|
+
last_execution_result = None
|
|
867
|
+
if "last_execution_result" in row.keys() and row["last_execution_result"]:
|
|
868
|
+
try:
|
|
869
|
+
last_execution_result = json.loads(row["last_execution_result"])
|
|
870
|
+
except (ValueError, TypeError):
|
|
871
|
+
pass
|
|
872
|
+
|
|
873
|
+
steps.append(
|
|
874
|
+
StepRecord(
|
|
875
|
+
step_id=_safe_uuid(row["step_id"]),
|
|
876
|
+
session_id=_safe_uuid(row["session_id"]),
|
|
877
|
+
step_number=row["step_number"],
|
|
878
|
+
timestamp=row["timestamp"],
|
|
879
|
+
pre_image_name=row["pre_image_name"],
|
|
880
|
+
post_image_name=row["post_image_name"],
|
|
881
|
+
summary=row["summary"],
|
|
882
|
+
action_taken=action_taken,
|
|
883
|
+
operator_raw_thinking=operator_raw_thinking,
|
|
884
|
+
operator_native_thinking=operator_native_thinking,
|
|
885
|
+
last_execution_result=last_execution_result,
|
|
886
|
+
extra_metadata=json.loads(row["extra_metadata"]),
|
|
887
|
+
)
|
|
888
|
+
)
|
|
889
|
+
return steps
|
|
890
|
+
|
|
891
|
+
get_session_steps = get_steps
|
|
892
|
+
|
|
893
|
+
def get_traces_for_step(self, step_id: UUID | str) -> list[TraceRecord]:
|
|
894
|
+
"""Retrieve all traces for a step."""
|
|
895
|
+
traces = []
|
|
896
|
+
with self._get_connection() as conn:
|
|
897
|
+
cursor = conn.execute(
|
|
898
|
+
"SELECT * FROM traces WHERE step_id = ? ORDER BY timestamp ASC",
|
|
899
|
+
(str(step_id),),
|
|
900
|
+
)
|
|
901
|
+
for row in cursor.fetchall():
|
|
902
|
+
traces.append(
|
|
903
|
+
TraceRecord(
|
|
904
|
+
trace_id=_safe_uuid(row["trace_id"]),
|
|
905
|
+
session_id=_safe_uuid(row["session_id"]),
|
|
906
|
+
step_id=_safe_uuid(row["step_id"]) if row["step_id"] else None,
|
|
907
|
+
parent_trace_id=_safe_uuid(row["parent_trace_id"])
|
|
908
|
+
if row["parent_trace_id"]
|
|
909
|
+
else None,
|
|
910
|
+
type=row["type"],
|
|
911
|
+
name=row["name"],
|
|
912
|
+
timestamp=row["timestamp"],
|
|
913
|
+
duration=row["duration"],
|
|
914
|
+
status=row["status"],
|
|
915
|
+
payload=json.loads(row["payload"]) if row["payload"] else {},
|
|
916
|
+
)
|
|
917
|
+
)
|
|
918
|
+
return traces
|
|
919
|
+
|
|
920
|
+
def get_steps_with_traces(self, session_id: UUID) -> list[tuple[StepRecord, list[TraceRecord]]]:
|
|
921
|
+
"""Retrieve all steps and their associated traces for a session."""
|
|
922
|
+
steps = self.get_steps(session_id)
|
|
923
|
+
result = []
|
|
924
|
+
for step in steps:
|
|
925
|
+
traces = self.get_traces_for_step(step.step_id)
|
|
926
|
+
result.append((step, traces))
|
|
927
|
+
return result
|
|
928
|
+
|
|
929
|
+
def create_trace(self, trace: TraceRecord):
|
|
930
|
+
"""Insert or update a trace record (handles out-of-order writes and preserves start timestamps)."""
|
|
931
|
+
with self._get_connection() as conn:
|
|
932
|
+
conn.execute(
|
|
933
|
+
"""
|
|
934
|
+
INSERT INTO traces (
|
|
935
|
+
trace_id, session_id, step_id, parent_trace_id, type, name,
|
|
936
|
+
timestamp, duration, status, payload
|
|
937
|
+
)
|
|
938
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
939
|
+
ON CONFLICT(trace_id) DO UPDATE SET
|
|
940
|
+
status = CASE
|
|
941
|
+
WHEN excluded.status = 'running' AND traces.status IN ('success', 'failed') THEN traces.status
|
|
942
|
+
ELSE excluded.status
|
|
943
|
+
END,
|
|
944
|
+
duration = COALESCE(excluded.duration, traces.duration),
|
|
945
|
+
timestamp = MIN(traces.timestamp, excluded.timestamp),
|
|
946
|
+
payload = CASE
|
|
947
|
+
WHEN excluded.status = 'running' AND traces.status IN ('success', 'failed') THEN traces.payload
|
|
948
|
+
ELSE excluded.payload
|
|
949
|
+
END
|
|
950
|
+
""",
|
|
951
|
+
(
|
|
952
|
+
str(trace.trace_id),
|
|
953
|
+
str(trace.session_id),
|
|
954
|
+
str(trace.step_id) if trace.step_id else None,
|
|
955
|
+
str(trace.parent_trace_id) if trace.parent_trace_id else None,
|
|
956
|
+
trace.type,
|
|
957
|
+
trace.name,
|
|
958
|
+
trace.timestamp,
|
|
959
|
+
trace.duration,
|
|
960
|
+
trace.status,
|
|
961
|
+
json.dumps(trace.payload, cls=SafeJSONEncoder),
|
|
962
|
+
),
|
|
963
|
+
)
|
|
964
|
+
conn.commit()
|
|
965
|
+
|
|
966
|
+
def create_failed_output(self, record: FailedOutputRecord):
|
|
967
|
+
"""Insert a new failed output record (crime scene)."""
|
|
968
|
+
with self._get_connection() as conn:
|
|
969
|
+
conn.execute(
|
|
970
|
+
"""
|
|
971
|
+
INSERT INTO failed_outputs (
|
|
972
|
+
session_id, trace_id, model_name, prompt, raw_output, error_message, timestamp
|
|
973
|
+
)
|
|
974
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
975
|
+
""",
|
|
976
|
+
(
|
|
977
|
+
str(record.session_id),
|
|
978
|
+
str(record.trace_id),
|
|
979
|
+
record.model_name,
|
|
980
|
+
record.prompt,
|
|
981
|
+
record.raw_output,
|
|
982
|
+
record.error_message,
|
|
983
|
+
record.timestamp,
|
|
984
|
+
),
|
|
985
|
+
)
|
|
986
|
+
conn.commit()
|
|
987
|
+
|
|
988
|
+
def create_background_task(self, record: BackgroundTaskRecord):
|
|
989
|
+
with self._get_connection() as conn:
|
|
990
|
+
conn.execute(
|
|
991
|
+
"""
|
|
992
|
+
INSERT OR REPLACE INTO background_tasks (
|
|
993
|
+
task_id, session_id, summary, status, start_time, end_time, trace_id, logs
|
|
994
|
+
)
|
|
995
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
996
|
+
""",
|
|
997
|
+
(
|
|
998
|
+
record.task_id,
|
|
999
|
+
str(record.session_id),
|
|
1000
|
+
record.summary,
|
|
1001
|
+
record.status,
|
|
1002
|
+
record.start_time,
|
|
1003
|
+
record.end_time,
|
|
1004
|
+
record.trace_id,
|
|
1005
|
+
record.logs,
|
|
1006
|
+
),
|
|
1007
|
+
)
|
|
1008
|
+
conn.commit()
|
|
1009
|
+
|
|
1010
|
+
def update_background_task_status(self, task_id: str, status: str, end_time: float):
|
|
1011
|
+
with self._get_connection() as conn:
|
|
1012
|
+
conn.execute(
|
|
1013
|
+
"""
|
|
1014
|
+
UPDATE background_tasks
|
|
1015
|
+
SET status = ?, end_time = ?
|
|
1016
|
+
WHERE task_id = ?
|
|
1017
|
+
""",
|
|
1018
|
+
(status, end_time, task_id),
|
|
1019
|
+
)
|
|
1020
|
+
conn.commit()
|
|
1021
|
+
|
|
1022
|
+
def update_background_task_status_and_logs(
|
|
1023
|
+
self, task_id: str, status: str, end_time: float, logs: str
|
|
1024
|
+
):
|
|
1025
|
+
with self._get_connection() as conn:
|
|
1026
|
+
conn.execute(
|
|
1027
|
+
"""
|
|
1028
|
+
UPDATE background_tasks
|
|
1029
|
+
SET status = ?, end_time = ?, logs = ?
|
|
1030
|
+
WHERE task_id = ?
|
|
1031
|
+
""",
|
|
1032
|
+
(status, end_time, logs, task_id),
|
|
1033
|
+
)
|
|
1034
|
+
conn.commit()
|
|
1035
|
+
|
|
1036
|
+
def get_background_tasks(self, session_id: UUID) -> list[BackgroundTaskRecord]:
|
|
1037
|
+
tasks = []
|
|
1038
|
+
with self._get_connection() as conn:
|
|
1039
|
+
cursor = conn.execute(
|
|
1040
|
+
"SELECT * FROM background_tasks WHERE session_id = ? ORDER BY start_time ASC",
|
|
1041
|
+
(str(session_id),),
|
|
1042
|
+
)
|
|
1043
|
+
for row in cursor.fetchall():
|
|
1044
|
+
logs = row["logs"] if "logs" in row.keys() else None
|
|
1045
|
+
tasks.append(
|
|
1046
|
+
BackgroundTaskRecord(
|
|
1047
|
+
task_id=row["task_id"],
|
|
1048
|
+
session_id=_safe_uuid(row["session_id"]),
|
|
1049
|
+
summary=row["summary"],
|
|
1050
|
+
status=row["status"],
|
|
1051
|
+
start_time=row["start_time"],
|
|
1052
|
+
end_time=row["end_time"],
|
|
1053
|
+
trace_id=row["trace_id"],
|
|
1054
|
+
logs=logs,
|
|
1055
|
+
)
|
|
1056
|
+
)
|
|
1057
|
+
return tasks
|
|
1058
|
+
|
|
1059
|
+
def get_step_traces(self, step_id: UUID | str) -> list[TraceRecord]:
|
|
1060
|
+
"""Retrieve all traces for a specific step."""
|
|
1061
|
+
traces = []
|
|
1062
|
+
with self._get_connection() as conn:
|
|
1063
|
+
cursor = conn.execute(
|
|
1064
|
+
"SELECT * FROM traces WHERE step_id = ? ORDER BY timestamp ASC",
|
|
1065
|
+
(str(step_id),),
|
|
1066
|
+
)
|
|
1067
|
+
for row in cursor.fetchall():
|
|
1068
|
+
traces.append(
|
|
1069
|
+
TraceRecord(
|
|
1070
|
+
trace_id=_safe_uuid(row["trace_id"]),
|
|
1071
|
+
session_id=_safe_uuid(row["session_id"]),
|
|
1072
|
+
step_id=_safe_uuid(row["step_id"]) if row["step_id"] else None,
|
|
1073
|
+
parent_trace_id=_safe_uuid(row["parent_trace_id"])
|
|
1074
|
+
if row["parent_trace_id"]
|
|
1075
|
+
else None,
|
|
1076
|
+
type=row["type"],
|
|
1077
|
+
name=row["name"],
|
|
1078
|
+
timestamp=row["timestamp"],
|
|
1079
|
+
duration=row["duration"],
|
|
1080
|
+
status=row["status"],
|
|
1081
|
+
payload=json.loads(row["payload"]) if row["payload"] else {},
|
|
1082
|
+
)
|
|
1083
|
+
)
|
|
1084
|
+
return traces
|
|
1085
|
+
|
|
1086
|
+
def clear_all_data(self):
|
|
1087
|
+
"""Clear all data from SQLite tables and delete session directories."""
|
|
1088
|
+
with self._get_connection() as conn:
|
|
1089
|
+
conn.execute("PRAGMA foreign_keys = OFF")
|
|
1090
|
+
tables = [
|
|
1091
|
+
"video_analysis_observations",
|
|
1092
|
+
"video_analysis_segments",
|
|
1093
|
+
"failed_outputs",
|
|
1094
|
+
"traces",
|
|
1095
|
+
"background_tasks",
|
|
1096
|
+
"steps",
|
|
1097
|
+
"images",
|
|
1098
|
+
"sessions",
|
|
1099
|
+
]
|
|
1100
|
+
for table in tables:
|
|
1101
|
+
try:
|
|
1102
|
+
conn.execute(f"DELETE FROM {table}")
|
|
1103
|
+
except sqlite3.OperationalError as e:
|
|
1104
|
+
logger.warning(f"Failed to clear table {table}: {e}")
|
|
1105
|
+
conn.commit()
|
|
1106
|
+
conn.execute("VACUUM")
|
|
1107
|
+
conn.commit()
|
|
1108
|
+
logger.info("Database tables cleared and vacuumed.")
|
|
1109
|
+
|
|
1110
|
+
# Delete session directories
|
|
1111
|
+
if self.base_trace_dir.exists():
|
|
1112
|
+
for path in self.base_trace_dir.iterdir():
|
|
1113
|
+
if path.is_dir():
|
|
1114
|
+
if path.name == "images":
|
|
1115
|
+
# Clear images
|
|
1116
|
+
for img_path in path.iterdir():
|
|
1117
|
+
if img_path.is_file():
|
|
1118
|
+
try:
|
|
1119
|
+
img_path.unlink()
|
|
1120
|
+
except Exception as e:
|
|
1121
|
+
logger.error(f"Failed to delete image {img_path}: {e}")
|
|
1122
|
+
elif path.name.startswith("web_") or self._is_uuid(path.name):
|
|
1123
|
+
try:
|
|
1124
|
+
shutil.rmtree(path)
|
|
1125
|
+
except Exception as e:
|
|
1126
|
+
logger.error(f"Failed to delete directory {path}: {e}")
|
|
1127
|
+
elif path.is_file() and path.name == "mcp_server.log":
|
|
1128
|
+
try:
|
|
1129
|
+
path.unlink()
|
|
1130
|
+
except Exception as e:
|
|
1131
|
+
logger.error(f"Failed to delete log file {path}: {e}")
|
|
1132
|
+
|
|
1133
|
+
def _is_uuid(self, val: str) -> bool:
|
|
1134
|
+
# Support both pure UUID and UUID with suffix (e.g., UUID_PASS_...)
|
|
1135
|
+
parts = val.split("_")
|
|
1136
|
+
target = parts[0] if parts else val
|
|
1137
|
+
try:
|
|
1138
|
+
uuid.UUID(target)
|
|
1139
|
+
return True
|
|
1140
|
+
except ValueError:
|
|
1141
|
+
return False
|
|
1142
|
+
|
|
1143
|
+
def delete_session(self, session_id: UUID):
|
|
1144
|
+
"""Delete all data associated with a session, including files on disk."""
|
|
1145
|
+
session_id_str = str(session_id)
|
|
1146
|
+
|
|
1147
|
+
# 1. Get video paths before deleting from DB
|
|
1148
|
+
video_paths = []
|
|
1149
|
+
video_ids: list[str] = []
|
|
1150
|
+
try:
|
|
1151
|
+
with self._get_connection() as conn:
|
|
1152
|
+
cursor = conn.execute(
|
|
1153
|
+
"SELECT video_id, local_video_path FROM video_recordings WHERE session_id = ?",
|
|
1154
|
+
(session_id_str,),
|
|
1155
|
+
)
|
|
1156
|
+
for row in cursor.fetchall():
|
|
1157
|
+
if row["video_id"]:
|
|
1158
|
+
video_ids.append(str(row["video_id"]))
|
|
1159
|
+
if row["local_video_path"]:
|
|
1160
|
+
video_paths.append(Path(row["local_video_path"]))
|
|
1161
|
+
except sqlite3.OperationalError as e:
|
|
1162
|
+
logger.warning(f"Could not query video_recordings: {e}")
|
|
1163
|
+
|
|
1164
|
+
# 2. Delete from DB tables
|
|
1165
|
+
with self._get_connection() as conn:
|
|
1166
|
+
conn.execute("PRAGMA foreign_keys = OFF")
|
|
1167
|
+
board_keys = [f"session:{session_id_str}"] + [
|
|
1168
|
+
f"video:{video_id}" for video_id in video_ids
|
|
1169
|
+
]
|
|
1170
|
+
for table in (
|
|
1171
|
+
"video_analysis_observations",
|
|
1172
|
+
"video_analysis_segments",
|
|
1173
|
+
):
|
|
1174
|
+
try:
|
|
1175
|
+
conn.executemany(
|
|
1176
|
+
f"DELETE FROM {table} WHERE board_key = ?",
|
|
1177
|
+
[(key,) for key in board_keys],
|
|
1178
|
+
)
|
|
1179
|
+
except sqlite3.OperationalError as e:
|
|
1180
|
+
logger.warning(f"Failed to delete video memory from {table}: {e}")
|
|
1181
|
+
tables = [
|
|
1182
|
+
"failed_outputs",
|
|
1183
|
+
"traces",
|
|
1184
|
+
"background_tasks",
|
|
1185
|
+
"video_recordings",
|
|
1186
|
+
"steps",
|
|
1187
|
+
"sessions",
|
|
1188
|
+
]
|
|
1189
|
+
for table in tables:
|
|
1190
|
+
try:
|
|
1191
|
+
conn.execute(
|
|
1192
|
+
f"DELETE FROM {table} WHERE session_id = ?",
|
|
1193
|
+
(session_id_str,),
|
|
1194
|
+
)
|
|
1195
|
+
except sqlite3.OperationalError as e:
|
|
1196
|
+
logger.warning(f"Failed to delete from table {table}: {e}")
|
|
1197
|
+
conn.commit()
|
|
1198
|
+
conn.execute("VACUUM")
|
|
1199
|
+
conn.commit()
|
|
1200
|
+
|
|
1201
|
+
logger.info(f"Database records for session {session_id} cleared.")
|
|
1202
|
+
|
|
1203
|
+
# 3. Delete session directory (notes, etc.)
|
|
1204
|
+
session_dir = self.base_trace_dir / session_id_str
|
|
1205
|
+
if session_dir.exists() and session_dir.is_dir():
|
|
1206
|
+
try:
|
|
1207
|
+
shutil.rmtree(session_dir)
|
|
1208
|
+
logger.info(f"Deleted session directory: {session_dir}")
|
|
1209
|
+
except Exception as e:
|
|
1210
|
+
logger.error(f"Failed to delete session directory {session_dir}: {e}")
|
|
1211
|
+
|
|
1212
|
+
# 4. Delete video files and their containing folders (which might have been renamed)
|
|
1213
|
+
for video_path in video_paths:
|
|
1214
|
+
# video_path is likely: /path/to/traces/task_name/recording.mp4
|
|
1215
|
+
# If it was renamed, it might be: /path/to/traces/task_name_PASS_timestamp/recording.mp4
|
|
1216
|
+
|
|
1217
|
+
task_name = video_path.parent.name
|
|
1218
|
+
|
|
1219
|
+
if task_name and task_name != "traces" and task_name != "..":
|
|
1220
|
+
# Search for directories starting with task_name in base_trace_dir
|
|
1221
|
+
try:
|
|
1222
|
+
for path in self.base_trace_dir.iterdir():
|
|
1223
|
+
if path.is_dir() and path.name.startswith(task_name):
|
|
1224
|
+
try:
|
|
1225
|
+
shutil.rmtree(path)
|
|
1226
|
+
logger.info(f"Deleted trace/video directory: {path}")
|
|
1227
|
+
except Exception as e:
|
|
1228
|
+
logger.error(f"Failed to delete directory {path}: {e}")
|
|
1229
|
+
elif path.is_file() and path.name.startswith(task_name):
|
|
1230
|
+
try:
|
|
1231
|
+
path.unlink()
|
|
1232
|
+
logger.info(f"Deleted trace/video file: {path}")
|
|
1233
|
+
except Exception as e:
|
|
1234
|
+
logger.error(f"Failed to delete file {path}: {e}")
|
|
1235
|
+
except Exception as e:
|
|
1236
|
+
logger.error(f"Error iterating base_trace_dir for cleanup: {e}")
|
|
1237
|
+
|
|
1238
|
+
if video_path.exists():
|
|
1239
|
+
try:
|
|
1240
|
+
video_path.unlink()
|
|
1241
|
+
logger.info(f"Deleted video file directly: {video_path}")
|
|
1242
|
+
if video_path.parent.exists() and not any(video_path.parent.iterdir()):
|
|
1243
|
+
video_path.parent.rmdir()
|
|
1244
|
+
except Exception as e:
|
|
1245
|
+
logger.error(f"Failed to delete video file directly {video_path}: {e}")
|