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,623 @@
|
|
|
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
|
+
import base64
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
import os
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
import subprocess
|
|
23
|
+
import threading
|
|
24
|
+
import time
|
|
25
|
+
from typing import Any
|
|
26
|
+
import urllib.parse
|
|
27
|
+
from fastapi import HTTPException
|
|
28
|
+
|
|
29
|
+
from artemis.config import IMAGES_DIR, TRACES_PATH, WORKSPACE_ROOT
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class MediaService:
|
|
35
|
+
"""Service handling media indexing, local file security, payload unwrapping and notes/plans."""
|
|
36
|
+
|
|
37
|
+
# Conversion results keyed by source path -> (mtime, size, resolved path).
|
|
38
|
+
# Failed conversions are cached too (resolved path == source path) so a broken
|
|
39
|
+
# file doesn't re-run ffmpeg (up to 45s) on every /api/sessions poll.
|
|
40
|
+
_playable_cache: dict[str, tuple[float, int, str]] = {}
|
|
41
|
+
_playable_cache_lock = threading.Lock()
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def ensure_browser_playable_video(cls, p: Path) -> Path:
|
|
45
|
+
"""Ensure the video file is in a browser-supported container (MP4).
|
|
46
|
+
|
|
47
|
+
If given an MKV, check if a converted MP4 already exists or convert it.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
if not p.exists():
|
|
51
|
+
return p
|
|
52
|
+
suffix = p.suffix.lower()
|
|
53
|
+
if suffix == ".mp4":
|
|
54
|
+
return p
|
|
55
|
+
if suffix in (".mkv", ".webm"):
|
|
56
|
+
if cls.is_live_recording(p):
|
|
57
|
+
# scrcpy is still appending to this file: converting now
|
|
58
|
+
# would freeze a few-seconds partial as recording.mp4.
|
|
59
|
+
return p
|
|
60
|
+
src_stat = p.stat()
|
|
61
|
+
cache_key = str(p)
|
|
62
|
+
cached = cls._playable_cache.get(cache_key)
|
|
63
|
+
if cached and cached[0] == src_stat.st_mtime and cached[1] == src_stat.st_size:
|
|
64
|
+
return Path(cached[2])
|
|
65
|
+
mp4_cand = p.with_suffix(".mp4")
|
|
66
|
+
if (
|
|
67
|
+
mp4_cand.exists()
|
|
68
|
+
and mp4_cand.stat().st_size > 0
|
|
69
|
+
and mp4_cand.stat().st_mtime >= src_stat.st_mtime
|
|
70
|
+
):
|
|
71
|
+
# An MP4 older than its source is a stale partial left by a
|
|
72
|
+
# mid-run scan; fall through and convert again.
|
|
73
|
+
cls._playable_cache[cache_key] = (
|
|
74
|
+
src_stat.st_mtime,
|
|
75
|
+
src_stat.st_size,
|
|
76
|
+
str(mp4_cand),
|
|
77
|
+
)
|
|
78
|
+
return mp4_cand
|
|
79
|
+
with cls._playable_cache_lock:
|
|
80
|
+
# Another thread may have converted this file while we waited.
|
|
81
|
+
cached = cls._playable_cache.get(cache_key)
|
|
82
|
+
if cached and cached[0] == src_stat.st_mtime and cached[1] == src_stat.st_size:
|
|
83
|
+
return Path(cached[2])
|
|
84
|
+
try:
|
|
85
|
+
result = cls._convert_to_mp4(p, mp4_cand)
|
|
86
|
+
except Exception:
|
|
87
|
+
result = p
|
|
88
|
+
cls._playable_cache[cache_key] = (
|
|
89
|
+
src_stat.st_mtime,
|
|
90
|
+
src_stat.st_size,
|
|
91
|
+
str(result),
|
|
92
|
+
)
|
|
93
|
+
return result
|
|
94
|
+
except OSError:
|
|
95
|
+
# Filesystem probe failed (missing/locked file): serve the
|
|
96
|
+
# original path and let the media endpoint report the error.
|
|
97
|
+
pass
|
|
98
|
+
return p
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _convert_to_mp4(p: Path, mp4_cand: Path) -> Path:
|
|
102
|
+
from artemis.utils.video import get_ffmpeg_path
|
|
103
|
+
|
|
104
|
+
ffmpeg = get_ffmpeg_path()
|
|
105
|
+
# 1. Attempt fast copy remux
|
|
106
|
+
subprocess.run(
|
|
107
|
+
[
|
|
108
|
+
ffmpeg,
|
|
109
|
+
"-y",
|
|
110
|
+
"-fflags",
|
|
111
|
+
"+genpts",
|
|
112
|
+
"-i",
|
|
113
|
+
str(p),
|
|
114
|
+
"-c",
|
|
115
|
+
"copy",
|
|
116
|
+
"-movflags",
|
|
117
|
+
"+faststart",
|
|
118
|
+
str(mp4_cand),
|
|
119
|
+
],
|
|
120
|
+
capture_output=True,
|
|
121
|
+
timeout=15,
|
|
122
|
+
)
|
|
123
|
+
if mp4_cand.exists() and mp4_cand.stat().st_size > 0:
|
|
124
|
+
return mp4_cand
|
|
125
|
+
# 2. If copy remux failed, fallback to ultrafast re-encode
|
|
126
|
+
subprocess.run(
|
|
127
|
+
[
|
|
128
|
+
ffmpeg,
|
|
129
|
+
"-y",
|
|
130
|
+
"-fflags",
|
|
131
|
+
"+genpts+discardcorrupt",
|
|
132
|
+
"-i",
|
|
133
|
+
str(p),
|
|
134
|
+
"-c:v",
|
|
135
|
+
"libx264",
|
|
136
|
+
"-preset",
|
|
137
|
+
"ultrafast",
|
|
138
|
+
"-pix_fmt",
|
|
139
|
+
"yuv420p",
|
|
140
|
+
"-movflags",
|
|
141
|
+
"+faststart",
|
|
142
|
+
str(mp4_cand),
|
|
143
|
+
],
|
|
144
|
+
capture_output=True,
|
|
145
|
+
timeout=30,
|
|
146
|
+
)
|
|
147
|
+
if mp4_cand.exists() and mp4_cand.stat().st_size > 0:
|
|
148
|
+
return mp4_cand
|
|
149
|
+
return p
|
|
150
|
+
|
|
151
|
+
LIVE_RECORDING_GRACE_SECONDS = 10.0
|
|
152
|
+
TRACE_STATUS_SUFFIXES = ("_PASS_", "_FAIL_", "_TESTFAIL_")
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def is_live_recording(cls, p: Path) -> bool:
|
|
156
|
+
"""Whether ``p`` is a raw .mkv that scrcpy is still appending to.
|
|
157
|
+
|
|
158
|
+
scrcpy touches the file continuously while recording; a raw file that
|
|
159
|
+
has not changed for a few seconds belongs to a finished (or killed)
|
|
160
|
+
recording and is safe to convert.
|
|
161
|
+
"""
|
|
162
|
+
if p.suffix.lower() != ".mkv":
|
|
163
|
+
return False
|
|
164
|
+
try:
|
|
165
|
+
return (time.time() - p.stat().st_mtime) < cls.LIVE_RECORDING_GRACE_SECONDS
|
|
166
|
+
except OSError:
|
|
167
|
+
return False
|
|
168
|
+
|
|
169
|
+
@staticmethod
|
|
170
|
+
def is_browser_playable(p: Path) -> bool:
|
|
171
|
+
return p.suffix.lower() in (".mp4", ".webm")
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def strip_trace_status_suffix(cls, folder_name: str) -> str:
|
|
175
|
+
for marker in cls.TRACE_STATUS_SUFFIXES:
|
|
176
|
+
folder_name = folder_name.split(marker)[0]
|
|
177
|
+
return folder_name
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def _finalize_trace_folder_name(cls, folder: Path, session_start_time: float | None) -> Path:
|
|
181
|
+
"""Give an unfinished temp trace folder the terminal name the worker uses."""
|
|
182
|
+
try:
|
|
183
|
+
if folder.resolve().parent != Path(TRACES_PATH).resolve():
|
|
184
|
+
return folder
|
|
185
|
+
except OSError:
|
|
186
|
+
return folder
|
|
187
|
+
if any(marker in folder.name for marker in cls.TRACE_STATUS_SUFFIXES):
|
|
188
|
+
return folder
|
|
189
|
+
try:
|
|
190
|
+
stamp = datetime.fromtimestamp(float(session_start_time or time.time())).strftime(
|
|
191
|
+
"%Y-%m-%dT%H-%M-%S"
|
|
192
|
+
)
|
|
193
|
+
except (TypeError, ValueError, OSError, OverflowError):
|
|
194
|
+
stamp = datetime.now().strftime("%Y-%m-%dT%H-%M-%S")
|
|
195
|
+
target = folder.with_name(f"{folder.name}_FAIL_{stamp}")
|
|
196
|
+
if target.exists():
|
|
197
|
+
return folder
|
|
198
|
+
try:
|
|
199
|
+
folder.rename(target)
|
|
200
|
+
except OSError as exc:
|
|
201
|
+
logger.warning("Could not rename orphaned trace folder %s: %s", folder, exc)
|
|
202
|
+
return folder
|
|
203
|
+
return target
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def recover_orphaned_recording(
|
|
207
|
+
cls,
|
|
208
|
+
local_video_path: str | Path | None,
|
|
209
|
+
session_start_time: float | None = None,
|
|
210
|
+
) -> Path | None:
|
|
211
|
+
"""Finalize the recording of a worker that died before its own stop path ran.
|
|
212
|
+
|
|
213
|
+
A hard-killed worker leaves the raw scrcpy ``recording.mkv`` behind
|
|
214
|
+
(playable, but never remuxed) plus, possibly, a few-seconds partial
|
|
215
|
+
``recording.mp4`` that a session-list scan converted mid-run. The raw
|
|
216
|
+
file is remuxed over that partial and removed on success, and the temp
|
|
217
|
+
trace folder is renamed the way the worker would have done it.
|
|
218
|
+
|
|
219
|
+
Returns the final browser-playable path, or None when nothing usable
|
|
220
|
+
exists (the caller then records a terminal recording failure).
|
|
221
|
+
"""
|
|
222
|
+
if not local_video_path:
|
|
223
|
+
return None
|
|
224
|
+
source = Path(str(local_video_path))
|
|
225
|
+
folder = source.parent
|
|
226
|
+
if not folder.is_dir():
|
|
227
|
+
return None
|
|
228
|
+
raw = source if source.suffix.lower() == ".mkv" else folder / "recording.mkv"
|
|
229
|
+
mp4 = folder / "recording.mp4"
|
|
230
|
+
|
|
231
|
+
def _non_empty(path: Path) -> bool:
|
|
232
|
+
try:
|
|
233
|
+
return path.is_file() and path.stat().st_size > 0
|
|
234
|
+
except OSError:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
if _non_empty(raw):
|
|
238
|
+
if cls.is_live_recording(raw):
|
|
239
|
+
return None
|
|
240
|
+
with cls._playable_cache_lock:
|
|
241
|
+
converted = cls._convert_to_mp4(raw, mp4)
|
|
242
|
+
cls._playable_cache.pop(str(raw), None)
|
|
243
|
+
if converted != mp4 or not _non_empty(mp4):
|
|
244
|
+
return None
|
|
245
|
+
try:
|
|
246
|
+
raw.unlink()
|
|
247
|
+
except OSError:
|
|
248
|
+
pass
|
|
249
|
+
final = mp4
|
|
250
|
+
elif _non_empty(mp4):
|
|
251
|
+
final = mp4
|
|
252
|
+
elif _non_empty(source) and cls.is_browser_playable(source):
|
|
253
|
+
final = source
|
|
254
|
+
else:
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
folder = cls._finalize_trace_folder_name(folder, session_start_time)
|
|
258
|
+
return folder / final.name
|
|
259
|
+
|
|
260
|
+
@staticmethod
|
|
261
|
+
def path_to_video_url(p: Path) -> str:
|
|
262
|
+
resolved = p.resolve()
|
|
263
|
+
try:
|
|
264
|
+
rel = resolved.relative_to(WORKSPACE_ROOT)
|
|
265
|
+
return f"/videos/{urllib.parse.quote(rel.as_posix(), safe='/')}"
|
|
266
|
+
except ValueError:
|
|
267
|
+
return f"/videos/{urllib.parse.quote(resolved.as_posix().lstrip('/'), safe='/:')}"
|
|
268
|
+
|
|
269
|
+
@classmethod
|
|
270
|
+
def build_video_index(cls) -> dict[str, str]:
|
|
271
|
+
idx = {}
|
|
272
|
+
dirs = [
|
|
273
|
+
TRACES_PATH,
|
|
274
|
+
WORKSPACE_ROOT / "artemis-traces",
|
|
275
|
+
WORKSPACE_ROOT / ".benchmarks" / "diagnoser" / "outputs" / "artifacts",
|
|
276
|
+
]
|
|
277
|
+
for d in dirs:
|
|
278
|
+
if not d.exists():
|
|
279
|
+
continue
|
|
280
|
+
try:
|
|
281
|
+
entries = list(d.iterdir())
|
|
282
|
+
except OSError as exc:
|
|
283
|
+
logger.warning("Video index scan failed under %s: %s", d, exc)
|
|
284
|
+
continue
|
|
285
|
+
for item in entries:
|
|
286
|
+
# One unreadable entry must not abort the rest of the directory.
|
|
287
|
+
try:
|
|
288
|
+
if item.is_dir():
|
|
289
|
+
for ext in [".mp4", ".mkv", ".webm"]:
|
|
290
|
+
vfile = item / f"recording{ext}"
|
|
291
|
+
if vfile.exists():
|
|
292
|
+
vfile = cls.ensure_browser_playable_video(vfile)
|
|
293
|
+
if not cls.is_browser_playable(vfile):
|
|
294
|
+
# Live (still recording) or unconvertible raw file.
|
|
295
|
+
break
|
|
296
|
+
url = cls.path_to_video_url(vfile)
|
|
297
|
+
idx[item.name] = url
|
|
298
|
+
prefix = cls.strip_trace_status_suffix(item.name)
|
|
299
|
+
idx[prefix] = url
|
|
300
|
+
break
|
|
301
|
+
elif item.suffix in [".mp4", ".mkv", ".webm"]:
|
|
302
|
+
item = cls.ensure_browser_playable_video(item)
|
|
303
|
+
if not cls.is_browser_playable(item):
|
|
304
|
+
continue
|
|
305
|
+
url = cls.path_to_video_url(item)
|
|
306
|
+
idx[item.stem] = url
|
|
307
|
+
idx[item.name] = url
|
|
308
|
+
except OSError as exc:
|
|
309
|
+
logger.warning("Skipping unindexable video entry %s: %s", item, exc)
|
|
310
|
+
return idx
|
|
311
|
+
|
|
312
|
+
@classmethod
|
|
313
|
+
def resolve_video_url(
|
|
314
|
+
cls,
|
|
315
|
+
row_dict: dict[str, Any],
|
|
316
|
+
video_rec_map: dict[str, str],
|
|
317
|
+
video_idx: dict[str, str],
|
|
318
|
+
) -> str | None:
|
|
319
|
+
raw_sid = row_dict.get("session_id")
|
|
320
|
+
s_id = (
|
|
321
|
+
str(raw_sid).strip()
|
|
322
|
+
if raw_sid is not None and str(raw_sid).strip().lower() != "none"
|
|
323
|
+
else ""
|
|
324
|
+
)
|
|
325
|
+
v_url = None
|
|
326
|
+
v_fp = row_dict.get("video_filepath")
|
|
327
|
+
if v_fp and os.path.exists(v_fp):
|
|
328
|
+
p = cls.ensure_browser_playable_video(Path(v_fp))
|
|
329
|
+
if cls.is_browser_playable(p):
|
|
330
|
+
v_url = cls.path_to_video_url(p)
|
|
331
|
+
|
|
332
|
+
orig_rec = video_rec_map.get(s_id)
|
|
333
|
+
if not v_url and orig_rec:
|
|
334
|
+
p = Path(orig_rec)
|
|
335
|
+
if p.exists():
|
|
336
|
+
p = cls.ensure_browser_playable_video(p)
|
|
337
|
+
if cls.is_browser_playable(p):
|
|
338
|
+
v_url = cls.path_to_video_url(p)
|
|
339
|
+
if not v_url:
|
|
340
|
+
folder_name = p.parent.name
|
|
341
|
+
v_url = video_idx.get(folder_name) or video_idx.get(folder_name.split("_")[0])
|
|
342
|
+
|
|
343
|
+
if not v_url:
|
|
344
|
+
v_url = video_idx.get(s_id)
|
|
345
|
+
|
|
346
|
+
if not v_url and s_id:
|
|
347
|
+
sess_dir = TRACES_PATH / s_id
|
|
348
|
+
if sess_dir.is_dir():
|
|
349
|
+
for ext in [".mp4", ".webm", ".mkv"]:
|
|
350
|
+
vfile = sess_dir / f"recording{ext}"
|
|
351
|
+
if vfile.exists():
|
|
352
|
+
vfile = cls.ensure_browser_playable_video(vfile)
|
|
353
|
+
if cls.is_browser_playable(vfile):
|
|
354
|
+
v_url = cls.path_to_video_url(vfile)
|
|
355
|
+
break
|
|
356
|
+
if not v_url:
|
|
357
|
+
try:
|
|
358
|
+
for item in sess_dir.iterdir():
|
|
359
|
+
if item.is_file() and item.suffix.lower() in [".mp4", ".webm", ".mkv"]:
|
|
360
|
+
item = cls.ensure_browser_playable_video(item)
|
|
361
|
+
if not cls.is_browser_playable(item):
|
|
362
|
+
continue
|
|
363
|
+
v_url = cls.path_to_video_url(item)
|
|
364
|
+
break
|
|
365
|
+
except OSError:
|
|
366
|
+
# Unreadable session dir: no recording to resolve.
|
|
367
|
+
pass
|
|
368
|
+
|
|
369
|
+
if not v_url and row_dict.get("initial_goal"):
|
|
370
|
+
goal_str = row_dict["initial_goal"]
|
|
371
|
+
task_hint = (
|
|
372
|
+
goal_str.split("Task: ")[1].split()[0].strip()
|
|
373
|
+
if "Task: " in goal_str
|
|
374
|
+
else goal_str.split("\n")[0].strip()
|
|
375
|
+
)
|
|
376
|
+
v_url = video_idx.get(task_hint)
|
|
377
|
+
|
|
378
|
+
return v_url
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def resolve_video_segments(cls, video_url: str | None) -> list[dict[str, Any]]:
|
|
382
|
+
"""Resolve a recording sidecar manifest into browser-safe segment URLs."""
|
|
383
|
+
if not video_url or not video_url.startswith("/videos/"):
|
|
384
|
+
return []
|
|
385
|
+
relative = urllib.parse.unquote(video_url.removeprefix("/videos/"))
|
|
386
|
+
video_path = (WORKSPACE_ROOT / relative).resolve()
|
|
387
|
+
manifest_path = video_path.parent / "recording.json"
|
|
388
|
+
if not manifest_path.is_file():
|
|
389
|
+
return []
|
|
390
|
+
try:
|
|
391
|
+
payload = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
392
|
+
segments = []
|
|
393
|
+
for item in payload.get("segments", []):
|
|
394
|
+
segment_path = (manifest_path.parent / str(item["file"])).resolve()
|
|
395
|
+
if (
|
|
396
|
+
segment_path.parent != manifest_path.parent.resolve()
|
|
397
|
+
or not segment_path.is_file()
|
|
398
|
+
):
|
|
399
|
+
continue
|
|
400
|
+
segment = {
|
|
401
|
+
"url": cls.path_to_video_url(segment_path),
|
|
402
|
+
"start": float(item.get("start", 0)),
|
|
403
|
+
"duration": float(item.get("duration", 0)),
|
|
404
|
+
"width": int(item.get("width", 0)),
|
|
405
|
+
"height": int(item.get("height", 0)),
|
|
406
|
+
}
|
|
407
|
+
# Manifest v2 carries the real session-relative timeline so the
|
|
408
|
+
# UI can seek by step time across scrcpy restart gaps.
|
|
409
|
+
if item.get("offset_ms") is not None and item.get("duration_ms") is not None:
|
|
410
|
+
segment["offset_ms"] = int(item["offset_ms"])
|
|
411
|
+
segment["duration_ms"] = int(item["duration_ms"])
|
|
412
|
+
segments.append(segment)
|
|
413
|
+
return segments
|
|
414
|
+
except (OSError, KeyError, TypeError, ValueError, json.JSONDecodeError):
|
|
415
|
+
return []
|
|
416
|
+
|
|
417
|
+
@staticmethod
|
|
418
|
+
def unwrap_payload(obj: Any) -> Any:
|
|
419
|
+
"""Unwraps trace payloads, storing base64 images as disk files and
|
|
420
|
+
truncating large dumps.
|
|
421
|
+
"""
|
|
422
|
+
if isinstance(obj, str):
|
|
423
|
+
try:
|
|
424
|
+
if (obj.startswith("{") and obj.endswith("}")) or (
|
|
425
|
+
obj.startswith("[") and obj.endswith("]")
|
|
426
|
+
):
|
|
427
|
+
return MediaService.unwrap_payload(json.loads(obj))
|
|
428
|
+
except ValueError:
|
|
429
|
+
# Looks like JSON but is not: treat it as a plain string.
|
|
430
|
+
pass
|
|
431
|
+
|
|
432
|
+
# Check if the string is a Base64 image
|
|
433
|
+
is_base64_img = False
|
|
434
|
+
base64_data = None
|
|
435
|
+
if obj.startswith("data:image/") and ";base64," in obj:
|
|
436
|
+
try:
|
|
437
|
+
_, base64_data = obj.split(";base64,", 1)
|
|
438
|
+
is_base64_img = True
|
|
439
|
+
except ValueError:
|
|
440
|
+
# Malformed data URI: fall through to plain-string handling.
|
|
441
|
+
pass
|
|
442
|
+
elif obj.startswith("iVBORw0KGgo") or obj.startswith("/9j/"):
|
|
443
|
+
is_base64_img = True
|
|
444
|
+
base64_data = obj
|
|
445
|
+
|
|
446
|
+
if is_base64_img and base64_data:
|
|
447
|
+
try:
|
|
448
|
+
image_bytes = base64.b64decode(base64_data)
|
|
449
|
+
image_hash = hashlib.sha256(image_bytes).hexdigest()
|
|
450
|
+
|
|
451
|
+
IMAGES_DIR.mkdir(parents=True, exist_ok=True)
|
|
452
|
+
image_path = IMAGES_DIR / f"{image_hash}.jpg"
|
|
453
|
+
if not image_path.exists():
|
|
454
|
+
image_path.write_bytes(image_bytes)
|
|
455
|
+
|
|
456
|
+
return f"image://{image_hash}"
|
|
457
|
+
except (ValueError, OSError) as exc:
|
|
458
|
+
# Invalid base64 or image cache write failure: fall back
|
|
459
|
+
# to the truncated raw string below.
|
|
460
|
+
logger.debug("Could not persist inline image: %s", exc)
|
|
461
|
+
|
|
462
|
+
if len(obj) > 2000:
|
|
463
|
+
preview_start = obj[:100].replace("\n", " ")
|
|
464
|
+
preview_end = obj[-100:].replace("\n", " ")
|
|
465
|
+
return (
|
|
466
|
+
f"<Massive String: {preview_start}...[truncated"
|
|
467
|
+
f" {len(obj) - 200} characters for UI performance]...{preview_end}"
|
|
468
|
+
f" (length={len(obj)})>"
|
|
469
|
+
)
|
|
470
|
+
return obj
|
|
471
|
+
elif isinstance(obj, dict):
|
|
472
|
+
return {k: MediaService.unwrap_payload(v) for k, v in obj.items()}
|
|
473
|
+
elif isinstance(obj, list):
|
|
474
|
+
if len(obj) > 20 and any(
|
|
475
|
+
isinstance(x, dict) and ("bounds" in x or "resource-id" in x) for x in obj
|
|
476
|
+
):
|
|
477
|
+
return [f"<XML UI List with {len(obj)} elements truncated for UI performance>"]
|
|
478
|
+
return [MediaService.unwrap_payload(x) for x in obj]
|
|
479
|
+
return obj
|
|
480
|
+
|
|
481
|
+
_LOCAL_FILE_MEDIA_TYPES = {
|
|
482
|
+
".jpg": "image/jpeg",
|
|
483
|
+
".jpeg": "image/jpeg",
|
|
484
|
+
".png": "image/png",
|
|
485
|
+
".gif": "image/gif",
|
|
486
|
+
".webp": "image/webp",
|
|
487
|
+
".mp4": "video/mp4",
|
|
488
|
+
".webm": "video/webm",
|
|
489
|
+
".mkv": "video/x-matroska",
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@classmethod
|
|
493
|
+
def get_safe_local_file(cls, path_str: str) -> tuple[Path, str]:
|
|
494
|
+
"""Serve trace media only: resolved path must sit inside the workspace
|
|
495
|
+
or traces tree AND carry a media extension, so this endpoint can never
|
|
496
|
+
hand out .env, databases, or source files."""
|
|
497
|
+
decoded_path = urllib.parse.unquote(path_str)
|
|
498
|
+
if decoded_path.startswith("file://"):
|
|
499
|
+
decoded_path = decoded_path[len("file://") :]
|
|
500
|
+
try:
|
|
501
|
+
p = Path(decoded_path).resolve(strict=True)
|
|
502
|
+
except (OSError, ValueError):
|
|
503
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
504
|
+
|
|
505
|
+
if not p.is_file():
|
|
506
|
+
raise HTTPException(status_code=404, detail="File not found")
|
|
507
|
+
|
|
508
|
+
allowed_roots = []
|
|
509
|
+
for root in (WORKSPACE_ROOT, TRACES_PATH):
|
|
510
|
+
try:
|
|
511
|
+
allowed_roots.append(Path(root).resolve())
|
|
512
|
+
except OSError:
|
|
513
|
+
continue
|
|
514
|
+
if not any(p.is_relative_to(root) for root in allowed_roots):
|
|
515
|
+
raise HTTPException(
|
|
516
|
+
status_code=403,
|
|
517
|
+
detail="Access denied. Path is outside workspace root.",
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
media_type = cls._LOCAL_FILE_MEDIA_TYPES.get(p.suffix.lower())
|
|
521
|
+
if media_type is None:
|
|
522
|
+
raise HTTPException(
|
|
523
|
+
status_code=403,
|
|
524
|
+
detail="Access denied. Only media files are served.",
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
return p, media_type
|
|
528
|
+
|
|
529
|
+
@staticmethod
|
|
530
|
+
def get_task_plan_content(session_id: str) -> str:
|
|
531
|
+
plan_path = TRACES_PATH / session_id / "notes" / "task_plan.md"
|
|
532
|
+
if not plan_path.exists():
|
|
533
|
+
plan_path = TRACES_PATH / "notes" / "task_plan.md"
|
|
534
|
+
|
|
535
|
+
if plan_path.exists():
|
|
536
|
+
try:
|
|
537
|
+
return plan_path.read_text(encoding="utf-8")
|
|
538
|
+
except Exception as e:
|
|
539
|
+
return f"Error reading task plan: {e}"
|
|
540
|
+
return "No task plan created yet."
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def _read_jsonl_records(path: Path, label: str, session_id: str) -> list[dict[str, Any]]:
|
|
544
|
+
records: list[dict[str, Any]] = []
|
|
545
|
+
if not path.is_file():
|
|
546
|
+
return records
|
|
547
|
+
try:
|
|
548
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
549
|
+
line = line.strip()
|
|
550
|
+
if not line:
|
|
551
|
+
continue
|
|
552
|
+
try:
|
|
553
|
+
rec = json.loads(line)
|
|
554
|
+
except ValueError:
|
|
555
|
+
continue
|
|
556
|
+
if isinstance(rec, dict):
|
|
557
|
+
records.append(rec)
|
|
558
|
+
except (OSError, ValueError) as e:
|
|
559
|
+
logger.warning(f"Failed to read {label} for {session_id}: {e}")
|
|
560
|
+
return records
|
|
561
|
+
|
|
562
|
+
@staticmethod
|
|
563
|
+
def get_session_checks(session_id: str) -> dict[str, Any]:
|
|
564
|
+
"""Checker material of one session for UI backfill: the append-only
|
|
565
|
+
verdict ledger (``check_ledger.jsonl``), the per-attempt transcripts of
|
|
566
|
+
the Checker's streamed reasoning (``check_streams.jsonl``: one record
|
|
567
|
+
per attempt with timestamped ``segments``) and the machine-readable run
|
|
568
|
+
outcome (``run_outcome.json``), all written by the Checker /
|
|
569
|
+
``artemis.graph.checkpoints``. Missing files mean "no checks ran"
|
|
570
|
+
(sessions recorded before transcripts existed simply have no streams).
|
|
571
|
+
"""
|
|
572
|
+
session_dir = TRACES_PATH / session_id
|
|
573
|
+
records = MediaService._read_jsonl_records(
|
|
574
|
+
session_dir / "check_ledger.jsonl", "check ledger", session_id
|
|
575
|
+
)
|
|
576
|
+
streams = MediaService._read_jsonl_records(
|
|
577
|
+
session_dir / "check_streams.jsonl", "check streams", session_id
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
run_outcome: dict[str, Any] | None = None
|
|
581
|
+
outcome_path = session_dir / "run_outcome.json"
|
|
582
|
+
if outcome_path.is_file():
|
|
583
|
+
try:
|
|
584
|
+
loaded = json.loads(outcome_path.read_text(encoding="utf-8"))
|
|
585
|
+
if isinstance(loaded, dict):
|
|
586
|
+
run_outcome = loaded
|
|
587
|
+
except (OSError, ValueError) as e:
|
|
588
|
+
logger.warning(f"Failed to read run outcome for {session_id}: {e}")
|
|
589
|
+
|
|
590
|
+
return {"records": records, "streams": streams, "run_outcome": run_outcome}
|
|
591
|
+
|
|
592
|
+
@staticmethod
|
|
593
|
+
def get_session_notes_content(session_id: str) -> dict[str, str]:
|
|
594
|
+
notes_dir = TRACES_PATH / session_id / "notes"
|
|
595
|
+
notes_content = {}
|
|
596
|
+
|
|
597
|
+
if notes_dir.exists() and notes_dir.is_dir():
|
|
598
|
+
for file in notes_dir.iterdir():
|
|
599
|
+
if file.is_file() and file.suffix in [".md", ".txt", ".json", ".yaml"]:
|
|
600
|
+
try:
|
|
601
|
+
notes_content[file.name] = file.read_text(encoding="utf-8")
|
|
602
|
+
except Exception as e:
|
|
603
|
+
notes_content[file.name] = f"Error reading file: {e}"
|
|
604
|
+
|
|
605
|
+
if not notes_content:
|
|
606
|
+
global_notes_dir = TRACES_PATH / "notes"
|
|
607
|
+
if global_notes_dir.exists() and global_notes_dir.is_dir():
|
|
608
|
+
for file in global_notes_dir.iterdir():
|
|
609
|
+
if file.is_file() and file.suffix in [
|
|
610
|
+
".md",
|
|
611
|
+
".txt",
|
|
612
|
+
".json",
|
|
613
|
+
".yaml",
|
|
614
|
+
]:
|
|
615
|
+
try:
|
|
616
|
+
notes_content[file.name] = file.read_text(encoding="utf-8")
|
|
617
|
+
except Exception as e:
|
|
618
|
+
notes_content[file.name] = f"Error reading file: {e}"
|
|
619
|
+
|
|
620
|
+
return notes_content
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
media_service = MediaService()
|