openadapt-ml 0.1.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openadapt_ml/baselines/__init__.py +121 -0
- openadapt_ml/baselines/adapter.py +185 -0
- openadapt_ml/baselines/cli.py +314 -0
- openadapt_ml/baselines/config.py +448 -0
- openadapt_ml/baselines/parser.py +922 -0
- openadapt_ml/baselines/prompts.py +787 -0
- openadapt_ml/benchmarks/__init__.py +13 -107
- openadapt_ml/benchmarks/agent.py +297 -374
- openadapt_ml/benchmarks/azure.py +62 -24
- openadapt_ml/benchmarks/azure_ops_tracker.py +521 -0
- openadapt_ml/benchmarks/cli.py +1874 -751
- openadapt_ml/benchmarks/trace_export.py +631 -0
- openadapt_ml/benchmarks/viewer.py +1236 -0
- openadapt_ml/benchmarks/vm_monitor.py +1111 -0
- openadapt_ml/benchmarks/waa_deploy/Dockerfile +216 -0
- openadapt_ml/benchmarks/waa_deploy/__init__.py +10 -0
- openadapt_ml/benchmarks/waa_deploy/api_agent.py +540 -0
- openadapt_ml/benchmarks/waa_deploy/start_waa_server.bat +53 -0
- openadapt_ml/cloud/azure_inference.py +3 -5
- openadapt_ml/cloud/lambda_labs.py +722 -307
- openadapt_ml/cloud/local.py +3194 -89
- openadapt_ml/cloud/ssh_tunnel.py +595 -0
- openadapt_ml/datasets/next_action.py +125 -96
- openadapt_ml/evals/grounding.py +32 -9
- openadapt_ml/evals/plot_eval_metrics.py +15 -13
- openadapt_ml/evals/trajectory_matching.py +120 -57
- openadapt_ml/experiments/demo_prompt/__init__.py +19 -0
- openadapt_ml/experiments/demo_prompt/format_demo.py +236 -0
- openadapt_ml/experiments/demo_prompt/results/experiment_20251231_002125.json +83 -0
- openadapt_ml/experiments/demo_prompt/results/experiment_n30_20251231_165958.json +1100 -0
- openadapt_ml/experiments/demo_prompt/results/multistep_20251231_025051.json +182 -0
- openadapt_ml/experiments/demo_prompt/run_experiment.py +541 -0
- openadapt_ml/experiments/representation_shootout/__init__.py +70 -0
- openadapt_ml/experiments/representation_shootout/conditions.py +708 -0
- openadapt_ml/experiments/representation_shootout/config.py +390 -0
- openadapt_ml/experiments/representation_shootout/evaluator.py +659 -0
- openadapt_ml/experiments/representation_shootout/runner.py +687 -0
- openadapt_ml/experiments/waa_demo/__init__.py +10 -0
- openadapt_ml/experiments/waa_demo/demos.py +357 -0
- openadapt_ml/experiments/waa_demo/runner.py +732 -0
- openadapt_ml/experiments/waa_demo/tasks.py +151 -0
- openadapt_ml/export/__init__.py +9 -0
- openadapt_ml/export/__main__.py +6 -0
- openadapt_ml/export/cli.py +89 -0
- openadapt_ml/export/parquet.py +277 -0
- openadapt_ml/grounding/detector.py +18 -14
- openadapt_ml/ingest/__init__.py +11 -10
- openadapt_ml/ingest/capture.py +97 -86
- openadapt_ml/ingest/loader.py +120 -69
- openadapt_ml/ingest/synthetic.py +344 -193
- openadapt_ml/models/api_adapter.py +14 -4
- openadapt_ml/models/base_adapter.py +10 -2
- openadapt_ml/models/providers/__init__.py +288 -0
- openadapt_ml/models/providers/anthropic.py +266 -0
- openadapt_ml/models/providers/base.py +299 -0
- openadapt_ml/models/providers/google.py +376 -0
- openadapt_ml/models/providers/openai.py +342 -0
- openadapt_ml/models/qwen_vl.py +46 -19
- openadapt_ml/perception/__init__.py +35 -0
- openadapt_ml/perception/integration.py +399 -0
- openadapt_ml/retrieval/README.md +226 -0
- openadapt_ml/retrieval/USAGE.md +391 -0
- openadapt_ml/retrieval/__init__.py +91 -0
- openadapt_ml/retrieval/demo_retriever.py +843 -0
- openadapt_ml/retrieval/embeddings.py +630 -0
- openadapt_ml/retrieval/index.py +194 -0
- openadapt_ml/retrieval/retriever.py +162 -0
- openadapt_ml/runtime/__init__.py +50 -0
- openadapt_ml/runtime/policy.py +27 -14
- openadapt_ml/runtime/safety_gate.py +471 -0
- openadapt_ml/schema/__init__.py +113 -0
- openadapt_ml/schema/converters.py +588 -0
- openadapt_ml/schema/episode.py +470 -0
- openadapt_ml/scripts/capture_screenshots.py +530 -0
- openadapt_ml/scripts/compare.py +102 -61
- openadapt_ml/scripts/demo_policy.py +4 -1
- openadapt_ml/scripts/eval_policy.py +19 -14
- openadapt_ml/scripts/make_gif.py +1 -1
- openadapt_ml/scripts/prepare_synthetic.py +16 -17
- openadapt_ml/scripts/train.py +98 -75
- openadapt_ml/segmentation/README.md +920 -0
- openadapt_ml/segmentation/__init__.py +97 -0
- openadapt_ml/segmentation/adapters/__init__.py +5 -0
- openadapt_ml/segmentation/adapters/capture_adapter.py +420 -0
- openadapt_ml/segmentation/annotator.py +610 -0
- openadapt_ml/segmentation/cache.py +290 -0
- openadapt_ml/segmentation/cli.py +674 -0
- openadapt_ml/segmentation/deduplicator.py +656 -0
- openadapt_ml/segmentation/frame_describer.py +788 -0
- openadapt_ml/segmentation/pipeline.py +340 -0
- openadapt_ml/segmentation/schemas.py +622 -0
- openadapt_ml/segmentation/segment_extractor.py +634 -0
- openadapt_ml/training/azure_ops_viewer.py +1097 -0
- openadapt_ml/training/benchmark_viewer.py +3255 -19
- openadapt_ml/training/shared_ui.py +7 -7
- openadapt_ml/training/stub_provider.py +57 -35
- openadapt_ml/training/trainer.py +255 -441
- openadapt_ml/training/trl_trainer.py +403 -0
- openadapt_ml/training/viewer.py +323 -108
- openadapt_ml/training/viewer_components.py +180 -0
- {openadapt_ml-0.1.0.dist-info → openadapt_ml-0.2.1.dist-info}/METADATA +312 -69
- openadapt_ml-0.2.1.dist-info/RECORD +116 -0
- openadapt_ml/benchmarks/base.py +0 -366
- openadapt_ml/benchmarks/data_collection.py +0 -432
- openadapt_ml/benchmarks/runner.py +0 -381
- openadapt_ml/benchmarks/waa.py +0 -704
- openadapt_ml/schemas/__init__.py +0 -53
- openadapt_ml/schemas/sessions.py +0 -122
- openadapt_ml/schemas/validation.py +0 -252
- openadapt_ml-0.1.0.dist-info/RECORD +0 -55
- {openadapt_ml-0.1.0.dist-info → openadapt_ml-0.2.1.dist-info}/WHEEL +0 -0
- {openadapt_ml-0.1.0.dist-info → openadapt_ml-0.2.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Task definitions for WAA demo experiment.
|
|
2
|
+
|
|
3
|
+
10 carefully selected tasks across 4 enterprise-relevant domains.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Difficulty(Enum):
|
|
14
|
+
EASY = "easy"
|
|
15
|
+
MEDIUM = "medium"
|
|
16
|
+
HARD = "hard"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Domain(Enum):
|
|
20
|
+
BROWSER = "msedge"
|
|
21
|
+
OFFICE_CALC = "libreoffice_calc"
|
|
22
|
+
OFFICE_WRITER = "libreoffice_writer"
|
|
23
|
+
SETTINGS = "settings"
|
|
24
|
+
FILE_EXPLORER = "file_explorer"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class WATask:
|
|
29
|
+
"""A Windows Agent Arena task definition."""
|
|
30
|
+
|
|
31
|
+
task_id: str
|
|
32
|
+
instruction: str
|
|
33
|
+
domain: Domain
|
|
34
|
+
difficulty: Difficulty
|
|
35
|
+
first_action_hint: str
|
|
36
|
+
demo_method: str # "manual" or "recorded"
|
|
37
|
+
json_path: str # Path in WAA repo
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
TASKS: dict[str, WATask] = {
|
|
41
|
+
"1": WATask(
|
|
42
|
+
task_id="004587f8-6028-4656-94c1-681481abbc9c-wos",
|
|
43
|
+
instruction="Enable the 'Do Not Track' feature in Edge",
|
|
44
|
+
domain=Domain.BROWSER,
|
|
45
|
+
difficulty=Difficulty.MEDIUM,
|
|
46
|
+
first_action_hint="Click three-dot menu in Edge",
|
|
47
|
+
demo_method="manual",
|
|
48
|
+
json_path="examples/msedge/004587f8-6028-4656-94c1-681481abbc9c-wos.json",
|
|
49
|
+
),
|
|
50
|
+
"2": WATask(
|
|
51
|
+
task_id="049d3788-c979-4ea6-934d-3a35c4630faf-WOS",
|
|
52
|
+
instruction="Save this webpage to bookmarks bar",
|
|
53
|
+
domain=Domain.BROWSER,
|
|
54
|
+
difficulty=Difficulty.EASY,
|
|
55
|
+
first_action_hint="Click star/bookmark icon or Ctrl+D",
|
|
56
|
+
demo_method="manual",
|
|
57
|
+
json_path="examples/msedge/049d3788-c979-4ea6-934d-3a35c4630faf-WOS.json",
|
|
58
|
+
),
|
|
59
|
+
"3": WATask(
|
|
60
|
+
task_id="2acd62b4-a2ab-44a7-a7e3-f5227bbd8324-wos",
|
|
61
|
+
instruction="Set default font size to largest for grandmother",
|
|
62
|
+
domain=Domain.BROWSER,
|
|
63
|
+
difficulty=Difficulty.MEDIUM,
|
|
64
|
+
first_action_hint="Open Settings > Appearance",
|
|
65
|
+
demo_method="manual",
|
|
66
|
+
json_path="examples/msedge/2acd62b4-a2ab-44a7-a7e3-f5227bbd8324-wos.json",
|
|
67
|
+
),
|
|
68
|
+
"4": WATask(
|
|
69
|
+
task_id="01b269ae-2111-4a07-81fd-3fcd711993b0-WOS",
|
|
70
|
+
instruction="Fill all blank cells with value from cell above",
|
|
71
|
+
domain=Domain.OFFICE_CALC,
|
|
72
|
+
difficulty=Difficulty.HARD,
|
|
73
|
+
first_action_hint="Select cells, use Go To Special > Blanks",
|
|
74
|
+
demo_method="recorded",
|
|
75
|
+
json_path="examples/libreoffice_calc/01b269ae-2111-4a07-81fd-3fcd711993b0-WOS.json",
|
|
76
|
+
),
|
|
77
|
+
"5": WATask(
|
|
78
|
+
task_id="0a2e43bf-b26c-4631-a966-af9dfa12c9e5-WOS",
|
|
79
|
+
instruction="Calculate monthly totals and create line chart",
|
|
80
|
+
domain=Domain.OFFICE_CALC,
|
|
81
|
+
difficulty=Difficulty.HARD,
|
|
82
|
+
first_action_hint="Click cell for SUM formula",
|
|
83
|
+
demo_method="recorded",
|
|
84
|
+
json_path="examples/libreoffice_calc/0a2e43bf-b26c-4631-a966-af9dfa12c9e5-WOS.json",
|
|
85
|
+
),
|
|
86
|
+
"6": WATask(
|
|
87
|
+
task_id="3ef2b351-8a84-4ff2-8724-d86eae9b842e-WOS",
|
|
88
|
+
instruction="Center align the heading in LibreOffice Writer",
|
|
89
|
+
domain=Domain.OFFICE_WRITER,
|
|
90
|
+
difficulty=Difficulty.EASY,
|
|
91
|
+
first_action_hint="Select text, click center align button",
|
|
92
|
+
demo_method="manual",
|
|
93
|
+
json_path="examples/libreoffice_writer/3ef2b351-8a84-4ff2-8724-d86eae9b842e-WOS.json",
|
|
94
|
+
),
|
|
95
|
+
"7": WATask(
|
|
96
|
+
task_id="37e10fc4-b4c5-4b02-a65c-bfae8bc51d3f-wos",
|
|
97
|
+
instruction="Turn off notifications for system",
|
|
98
|
+
domain=Domain.SETTINGS,
|
|
99
|
+
difficulty=Difficulty.MEDIUM,
|
|
100
|
+
first_action_hint="Open Settings > System > Notifications",
|
|
101
|
+
demo_method="manual",
|
|
102
|
+
json_path="examples/settings/37e10fc4-b4c5-4b02-a65c-bfae8bc51d3f-wos.json",
|
|
103
|
+
),
|
|
104
|
+
"8": WATask(
|
|
105
|
+
task_id="46adf721-2949-4426-b069-010b7c128d8f-wos",
|
|
106
|
+
instruction="Enable Night Light: on at 7PM, off at 7AM",
|
|
107
|
+
domain=Domain.SETTINGS,
|
|
108
|
+
difficulty=Difficulty.MEDIUM,
|
|
109
|
+
first_action_hint="Open Settings > Display > Night Light",
|
|
110
|
+
demo_method="manual",
|
|
111
|
+
json_path="examples/settings/46adf721-2949-4426-b069-010b7c128d8f-wos.json",
|
|
112
|
+
),
|
|
113
|
+
"9": WATask(
|
|
114
|
+
task_id="0c9dda13-428c-492b-900b-f48562111f93-WOS",
|
|
115
|
+
instruction="Create Archive folder and move all .docx files",
|
|
116
|
+
domain=Domain.FILE_EXPLORER,
|
|
117
|
+
difficulty=Difficulty.MEDIUM,
|
|
118
|
+
first_action_hint="Right-click > New Folder, then select and move files",
|
|
119
|
+
demo_method="recorded",
|
|
120
|
+
json_path="examples/file_explorer/0c9dda13-428c-492b-900b-f48562111f93-WOS.json",
|
|
121
|
+
),
|
|
122
|
+
"10": WATask(
|
|
123
|
+
task_id="34a4fee9-e52e-4a4a-96d2-68d35091504a-WOS",
|
|
124
|
+
instruction="Change view to Details view",
|
|
125
|
+
domain=Domain.FILE_EXPLORER,
|
|
126
|
+
difficulty=Difficulty.EASY,
|
|
127
|
+
first_action_hint="Click View menu or dropdown",
|
|
128
|
+
demo_method="manual",
|
|
129
|
+
json_path="examples/file_explorer/34a4fee9-e52e-4a4a-96d2-68d35091504a-WOS.json",
|
|
130
|
+
),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_task(task_num: str | int) -> Optional[WATask]:
|
|
135
|
+
"""Get a task by its number (1-10)."""
|
|
136
|
+
return TASKS.get(str(task_num))
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def get_tasks_by_method(method: str) -> list[WATask]:
|
|
140
|
+
"""Get all tasks that use a specific demo method."""
|
|
141
|
+
return [t for t in TASKS.values() if t.demo_method == method]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_manual_tasks() -> list[WATask]:
|
|
145
|
+
"""Get tasks requiring manual demo writing."""
|
|
146
|
+
return get_tasks_by_method("manual")
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def get_recorded_tasks() -> list[WATask]:
|
|
150
|
+
"""Get tasks requiring recorded demos."""
|
|
151
|
+
return get_tasks_by_method("recorded")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Export utilities for Episode data.
|
|
2
|
+
|
|
3
|
+
This module provides tools to export Episode trajectories to various formats
|
|
4
|
+
for analytics, training, and sharing.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from openadapt_ml.export.parquet import to_parquet, from_parquet
|
|
8
|
+
|
|
9
|
+
__all__ = ["to_parquet", "from_parquet"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""CLI for export utilities."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> int:
|
|
9
|
+
"""Main entry point for export CLI."""
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
description="Export Episode data to various formats",
|
|
12
|
+
prog="python -m openadapt_ml.export",
|
|
13
|
+
)
|
|
14
|
+
subparsers = parser.add_subparsers(dest="command", help="Export format")
|
|
15
|
+
|
|
16
|
+
# Parquet subcommand
|
|
17
|
+
parquet_parser = subparsers.add_parser(
|
|
18
|
+
"parquet",
|
|
19
|
+
help="Export to Parquet format for analytics",
|
|
20
|
+
)
|
|
21
|
+
parquet_parser.add_argument(
|
|
22
|
+
"--input",
|
|
23
|
+
"-i",
|
|
24
|
+
required=True,
|
|
25
|
+
help="Directory containing Episode JSON files",
|
|
26
|
+
)
|
|
27
|
+
parquet_parser.add_argument(
|
|
28
|
+
"--output",
|
|
29
|
+
"-o",
|
|
30
|
+
required=True,
|
|
31
|
+
help="Output path for .parquet file",
|
|
32
|
+
)
|
|
33
|
+
parquet_parser.add_argument(
|
|
34
|
+
"--include-summary",
|
|
35
|
+
action="store_true",
|
|
36
|
+
help="Also generate episode-level summary table",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
if args.command == "parquet":
|
|
42
|
+
return export_parquet(args)
|
|
43
|
+
else:
|
|
44
|
+
parser.print_help()
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def export_parquet(args: argparse.Namespace) -> int:
|
|
49
|
+
"""Export Episodes to Parquet."""
|
|
50
|
+
try:
|
|
51
|
+
from openadapt_ml.export import to_parquet
|
|
52
|
+
from openadapt_ml.ingest import load_episodes
|
|
53
|
+
except ImportError as e:
|
|
54
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
55
|
+
return 1
|
|
56
|
+
|
|
57
|
+
input_path = Path(args.input)
|
|
58
|
+
if not input_path.exists():
|
|
59
|
+
print(f"Error: Input path does not exist: {input_path}", file=sys.stderr)
|
|
60
|
+
return 1
|
|
61
|
+
|
|
62
|
+
print(f"Loading episodes from: {input_path}")
|
|
63
|
+
episodes = load_episodes(str(input_path))
|
|
64
|
+
print(f"Loaded {len(episodes)} episodes")
|
|
65
|
+
|
|
66
|
+
if not episodes:
|
|
67
|
+
print("Warning: No episodes found", file=sys.stderr)
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
total_steps = sum(len(ep.steps) for ep in episodes)
|
|
71
|
+
print(f"Total steps: {total_steps}")
|
|
72
|
+
|
|
73
|
+
print(f"Exporting to: {args.output}")
|
|
74
|
+
to_parquet(
|
|
75
|
+
episodes,
|
|
76
|
+
args.output,
|
|
77
|
+
include_summary=args.include_summary,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
print("Done!")
|
|
81
|
+
if args.include_summary:
|
|
82
|
+
summary_path = args.output.replace(".parquet", "_summary.parquet")
|
|
83
|
+
print(f"Summary written to: {summary_path}")
|
|
84
|
+
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
if __name__ == "__main__":
|
|
89
|
+
sys.exit(main())
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""Parquet export utilities for Episode trajectories.
|
|
2
|
+
|
|
3
|
+
Parquet is a derived format for analytics and governance.
|
|
4
|
+
Episode JSON remains the canonical representation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from openadapt_ml.schema import Episode
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def to_parquet(
|
|
17
|
+
episodes: list[Episode],
|
|
18
|
+
output_path: str,
|
|
19
|
+
flatten_steps: bool = True,
|
|
20
|
+
include_summary: bool = False,
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Export Episodes to Parquet for analytics.
|
|
23
|
+
|
|
24
|
+
Creates a step-level Parquet file with one row per step.
|
|
25
|
+
Episode-level fields are repeated for each step.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
episodes: List of Episode objects to export.
|
|
29
|
+
output_path: Path to output .parquet file.
|
|
30
|
+
flatten_steps: If True, one row per step. If False, one row per episode
|
|
31
|
+
with steps as nested structure (not yet implemented).
|
|
32
|
+
include_summary: If True, also generate {output_path}_summary.parquet
|
|
33
|
+
with episode-level aggregations.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ImportError: If pyarrow is not installed.
|
|
37
|
+
ValueError: If flatten_steps is False (not yet implemented).
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> from openadapt_ml.ingest import load_episodes
|
|
41
|
+
>>> from openadapt_ml.export import to_parquet
|
|
42
|
+
>>> episodes = load_episodes("workflow_exports/")
|
|
43
|
+
>>> to_parquet(episodes, "episodes.parquet")
|
|
44
|
+
"""
|
|
45
|
+
try:
|
|
46
|
+
import pyarrow as pa
|
|
47
|
+
import pyarrow.parquet as pq
|
|
48
|
+
except ImportError:
|
|
49
|
+
raise ImportError(
|
|
50
|
+
"Parquet export requires pyarrow. "
|
|
51
|
+
"Install with: pip install openadapt-ml[parquet]"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if not flatten_steps:
|
|
55
|
+
raise ValueError(
|
|
56
|
+
"flatten_steps=False is not yet implemented. "
|
|
57
|
+
"Use flatten_steps=True for step-level rows."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
rows = []
|
|
61
|
+
for episode in episodes:
|
|
62
|
+
episode_metadata = None
|
|
63
|
+
if hasattr(episode, "metadata") and episode.metadata:
|
|
64
|
+
episode_metadata = json.dumps(episode.metadata)
|
|
65
|
+
|
|
66
|
+
for step in episode.steps:
|
|
67
|
+
# Extract normalized coordinates if available
|
|
68
|
+
x, y = None, None
|
|
69
|
+
if step.action and step.action.normalized_coordinates:
|
|
70
|
+
x, y = step.action.normalized_coordinates
|
|
71
|
+
|
|
72
|
+
# Extract action type value (enum -> string)
|
|
73
|
+
action_type = None
|
|
74
|
+
if step.action:
|
|
75
|
+
action_type = (
|
|
76
|
+
step.action.type.value
|
|
77
|
+
if hasattr(step.action.type, "value")
|
|
78
|
+
else step.action.type
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
row = {
|
|
82
|
+
"episode_id": episode.episode_id,
|
|
83
|
+
"instruction": episode.instruction,
|
|
84
|
+
"task_id": getattr(episode, "task_id", None),
|
|
85
|
+
"step_index": step.step_index,
|
|
86
|
+
"timestamp": step.timestamp,
|
|
87
|
+
"action_type": action_type,
|
|
88
|
+
"x": x,
|
|
89
|
+
"y": y,
|
|
90
|
+
"end_x": step.action.normalized_end[0]
|
|
91
|
+
if step.action and step.action.normalized_end
|
|
92
|
+
else None,
|
|
93
|
+
"end_y": step.action.normalized_end[1]
|
|
94
|
+
if step.action and step.action.normalized_end
|
|
95
|
+
else None,
|
|
96
|
+
"text": getattr(step.action, "text", None) if step.action else None,
|
|
97
|
+
"key": getattr(step.action, "key", None) if step.action else None,
|
|
98
|
+
"scroll_direction": (
|
|
99
|
+
getattr(step.action, "scroll_direction", None)
|
|
100
|
+
if step.action
|
|
101
|
+
else None
|
|
102
|
+
),
|
|
103
|
+
"screenshot_path": (
|
|
104
|
+
step.observation.screenshot_path if step.observation else None
|
|
105
|
+
),
|
|
106
|
+
"window_title": (
|
|
107
|
+
getattr(step.observation, "window_title", None)
|
|
108
|
+
if step.observation
|
|
109
|
+
else None
|
|
110
|
+
),
|
|
111
|
+
"app_name": (
|
|
112
|
+
None # Not in new schema at Observation level
|
|
113
|
+
),
|
|
114
|
+
"url": (
|
|
115
|
+
None # Not in new schema at Observation level
|
|
116
|
+
),
|
|
117
|
+
"reasoning": getattr(step, "reasoning", None),
|
|
118
|
+
"episode_metadata": episode_metadata,
|
|
119
|
+
}
|
|
120
|
+
rows.append(row)
|
|
121
|
+
|
|
122
|
+
table = pa.Table.from_pylist(rows)
|
|
123
|
+
pq.write_table(table, output_path)
|
|
124
|
+
|
|
125
|
+
if include_summary:
|
|
126
|
+
_write_summary(episodes, output_path)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _write_summary(episodes: list[Episode], output_path: str) -> None:
|
|
130
|
+
"""Write episode-level summary Parquet file."""
|
|
131
|
+
try:
|
|
132
|
+
import pyarrow as pa
|
|
133
|
+
import pyarrow.parquet as pq
|
|
134
|
+
except ImportError:
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
summary_rows = []
|
|
138
|
+
for episode in episodes:
|
|
139
|
+
first_t = episode.steps[0].timestamp if episode.steps else None
|
|
140
|
+
last_t = episode.steps[-1].timestamp if episode.steps else None
|
|
141
|
+
duration = (
|
|
142
|
+
(last_t - first_t) if first_t is not None and last_t is not None else None
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Extract action type values (enum -> string)
|
|
146
|
+
first_action_type = None
|
|
147
|
+
last_action_type = None
|
|
148
|
+
if episode.steps and episode.steps[0].action:
|
|
149
|
+
t = episode.steps[0].action.type
|
|
150
|
+
first_action_type = t.value if hasattr(t, "value") else t
|
|
151
|
+
if episode.steps and episode.steps[-1].action:
|
|
152
|
+
t = episode.steps[-1].action.type
|
|
153
|
+
last_action_type = t.value if hasattr(t, "value") else t
|
|
154
|
+
|
|
155
|
+
summary_rows.append(
|
|
156
|
+
{
|
|
157
|
+
"episode_id": episode.episode_id,
|
|
158
|
+
"instruction": episode.instruction,
|
|
159
|
+
"task_id": getattr(episode, "task_id", None),
|
|
160
|
+
"step_count": len(episode.steps),
|
|
161
|
+
"duration": duration,
|
|
162
|
+
"success": getattr(episode, "success", None),
|
|
163
|
+
"first_action_type": first_action_type,
|
|
164
|
+
"last_action_type": last_action_type,
|
|
165
|
+
"metadata": (
|
|
166
|
+
json.dumps(episode.metadata)
|
|
167
|
+
if hasattr(episode, "metadata") and episode.metadata
|
|
168
|
+
else None
|
|
169
|
+
),
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
summary_table = pa.Table.from_pylist(summary_rows)
|
|
174
|
+
summary_path = str(output_path).replace(".parquet", "_summary.parquet")
|
|
175
|
+
pq.write_table(summary_table, summary_path)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def from_parquet(parquet_path: str) -> list[Episode]:
|
|
179
|
+
"""Load Episodes from Parquet (inverse of to_parquet).
|
|
180
|
+
|
|
181
|
+
This is a lossy reconstruction. For full fidelity, always keep
|
|
182
|
+
Episode JSON as the source of truth.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
parquet_path: Path to the Parquet file created by to_parquet().
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
List of reconstructed Episode objects.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
ImportError: If pyarrow is not installed.
|
|
192
|
+
|
|
193
|
+
Note:
|
|
194
|
+
- Metadata fields are deserialized from JSON strings
|
|
195
|
+
- Step ordering is recovered from step_index
|
|
196
|
+
- Episode boundaries are recovered from episode_id grouping
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
import pyarrow.parquet as pq
|
|
200
|
+
except ImportError:
|
|
201
|
+
raise ImportError(
|
|
202
|
+
"Parquet import requires pyarrow. "
|
|
203
|
+
"Install with: pip install openadapt-ml[parquet]"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
from openadapt_ml.schema import Action, ActionType, Episode, Observation, Step
|
|
207
|
+
|
|
208
|
+
table = pq.read_table(parquet_path)
|
|
209
|
+
df = table.to_pandas()
|
|
210
|
+
|
|
211
|
+
episodes = []
|
|
212
|
+
for episode_id, group in df.groupby("episode_id"):
|
|
213
|
+
group = group.sort_values("step_index")
|
|
214
|
+
|
|
215
|
+
steps = []
|
|
216
|
+
for _, row in group.iterrows():
|
|
217
|
+
observation = Observation(
|
|
218
|
+
screenshot_path=row.get("screenshot_path") or row.get("image_path"),
|
|
219
|
+
window_title=row.get("window_title"),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
action = None
|
|
223
|
+
if row.get("action_type"):
|
|
224
|
+
# Convert string action type to ActionType enum
|
|
225
|
+
action_type_str = row["action_type"]
|
|
226
|
+
try:
|
|
227
|
+
action_type = ActionType(action_type_str)
|
|
228
|
+
except ValueError:
|
|
229
|
+
action_type = ActionType.CLICK # Default fallback
|
|
230
|
+
|
|
231
|
+
# Build normalized coordinates tuple if x and y are present
|
|
232
|
+
normalized_coords = None
|
|
233
|
+
if row.get("x") is not None and row.get("y") is not None:
|
|
234
|
+
normalized_coords = (float(row["x"]), float(row["y"]))
|
|
235
|
+
|
|
236
|
+
# Build normalized end coordinates for drag
|
|
237
|
+
normalized_end = None
|
|
238
|
+
if row.get("end_x") is not None and row.get("end_y") is not None:
|
|
239
|
+
normalized_end = (float(row["end_x"]), float(row["end_y"]))
|
|
240
|
+
|
|
241
|
+
action = Action(
|
|
242
|
+
type=action_type,
|
|
243
|
+
normalized_coordinates=normalized_coords,
|
|
244
|
+
normalized_end=normalized_end,
|
|
245
|
+
text=row.get("text"),
|
|
246
|
+
key=row.get("key"),
|
|
247
|
+
scroll_direction=row.get("scroll_direction"),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
step = Step(
|
|
251
|
+
step_index=int(row.get("step_index", 0)),
|
|
252
|
+
observation=observation,
|
|
253
|
+
action=action,
|
|
254
|
+
reasoning=row.get("reasoning") or row.get("thought"),
|
|
255
|
+
timestamp=row.get("timestamp"),
|
|
256
|
+
)
|
|
257
|
+
steps.append(step)
|
|
258
|
+
|
|
259
|
+
# Parse metadata if present
|
|
260
|
+
metadata = None
|
|
261
|
+
if group.iloc[0].get("episode_metadata"):
|
|
262
|
+
try:
|
|
263
|
+
metadata = json.loads(group.iloc[0]["episode_metadata"])
|
|
264
|
+
except (json.JSONDecodeError, TypeError):
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
episode = Episode(
|
|
268
|
+
episode_id=str(episode_id),
|
|
269
|
+
instruction=group.iloc[0].get("instruction")
|
|
270
|
+
or group.iloc[0].get("goal", ""),
|
|
271
|
+
steps=steps,
|
|
272
|
+
task_id=group.iloc[0].get("task_id"),
|
|
273
|
+
metadata=metadata,
|
|
274
|
+
)
|
|
275
|
+
episodes.append(episode)
|
|
276
|
+
|
|
277
|
+
return episodes
|
|
@@ -20,7 +20,7 @@ from openadapt_ml.config import settings
|
|
|
20
20
|
from openadapt_ml.grounding.base import GroundingModule, RegionCandidate
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
|
-
from PIL import Image
|
|
23
|
+
from PIL import Image
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class GeminiGrounder(GroundingModule):
|
|
@@ -104,7 +104,7 @@ class GeminiGrounder(GroundingModule):
|
|
|
104
104
|
|
|
105
105
|
# Try to parse JSON from the response
|
|
106
106
|
# Look for JSON array or object in the response
|
|
107
|
-
json_match = re.search(r
|
|
107
|
+
json_match = re.search(r"\[[\s\S]*\]|\{[\s\S]*\}", response_text)
|
|
108
108
|
if not json_match:
|
|
109
109
|
return candidates
|
|
110
110
|
|
|
@@ -340,11 +340,11 @@ Example output format:
|
|
|
340
340
|
response_text = response.text
|
|
341
341
|
|
|
342
342
|
# Try to extract JSON array from response
|
|
343
|
-
json_match = re.search(r
|
|
343
|
+
json_match = re.search(r"\[[\s\S]*\]", response_text)
|
|
344
344
|
if not json_match:
|
|
345
345
|
# Maybe it's just a plain array
|
|
346
|
-
if response_text.strip().startswith(
|
|
347
|
-
json_match = re.match(r
|
|
346
|
+
if response_text.strip().startswith("["):
|
|
347
|
+
json_match = re.match(r".*", response_text)
|
|
348
348
|
else:
|
|
349
349
|
return []
|
|
350
350
|
|
|
@@ -369,13 +369,18 @@ Example output format:
|
|
|
369
369
|
max(0, min(1, y2 / screenshot.height)),
|
|
370
370
|
]
|
|
371
371
|
|
|
372
|
-
normalized_elements.append(
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
372
|
+
normalized_elements.append(
|
|
373
|
+
{
|
|
374
|
+
"id": elem.get("id", len(normalized_elements) + 1),
|
|
375
|
+
"label": elem.get(
|
|
376
|
+
"label",
|
|
377
|
+
f"Element {elem.get('id', len(normalized_elements) + 1)}",
|
|
378
|
+
),
|
|
379
|
+
"bbox": norm_bbox,
|
|
380
|
+
"type": elem.get("type", "other"),
|
|
381
|
+
"text": elem.get("text", ""),
|
|
382
|
+
}
|
|
383
|
+
)
|
|
379
384
|
|
|
380
385
|
return normalized_elements
|
|
381
386
|
|
|
@@ -549,8 +554,7 @@ class DetectorGrounder(GroundingModule):
|
|
|
549
554
|
self._backend = GeminiGrounder(**kwargs)
|
|
550
555
|
elif backend == "omniparser":
|
|
551
556
|
raise NotImplementedError(
|
|
552
|
-
"OmniParser backend not yet implemented. "
|
|
553
|
-
"Use backend='gemini' for now."
|
|
557
|
+
"OmniParser backend not yet implemented. Use backend='gemini' for now."
|
|
554
558
|
)
|
|
555
559
|
else:
|
|
556
560
|
raise ValueError(f"Unknown backend: {backend}")
|
openadapt_ml/ingest/__init__.py
CHANGED
|
@@ -6,7 +6,6 @@ and converting them to the format used for training.
|
|
|
6
6
|
Data Model:
|
|
7
7
|
- Episode: A single task attempt (e.g., "log into the app"). Contains a sequence
|
|
8
8
|
of Steps, each with an Observation (screenshot) and Action (click/type/etc).
|
|
9
|
-
- Session: A container grouping one or more Episodes with shared metadata.
|
|
10
9
|
|
|
11
10
|
Functions:
|
|
12
11
|
- load_episodes(): Load Episodes from JSON files (primary entry point)
|
|
@@ -14,30 +13,32 @@ Functions:
|
|
|
14
13
|
- capture_to_episode(): Converts one openadapt-capture recording → one Episode
|
|
15
14
|
- capture_to_session(): Converts one recording → Session containing one Episode
|
|
16
15
|
- load_captures_as_sessions(): Loads multiple recordings → list of Sessions
|
|
17
|
-
-
|
|
16
|
+
- generate_synthetic_episodes(): Creates synthetic training data
|
|
18
17
|
"""
|
|
19
18
|
|
|
20
19
|
from openadapt_ml.ingest.loader import load_episodes, save_episodes
|
|
21
|
-
from openadapt_ml.ingest.synthetic import
|
|
20
|
+
from openadapt_ml.ingest.synthetic import generate_synthetic_episodes
|
|
22
21
|
|
|
23
22
|
__all__ = [
|
|
24
23
|
"load_episodes",
|
|
25
24
|
"save_episodes",
|
|
26
|
-
"
|
|
25
|
+
"generate_synthetic_episodes",
|
|
27
26
|
]
|
|
28
27
|
|
|
29
28
|
# Conditionally export capture functions if openadapt-capture is installed
|
|
30
29
|
try:
|
|
31
|
-
from openadapt_ml.ingest.capture import (
|
|
30
|
+
from openadapt_ml.ingest.capture import ( # noqa: F401
|
|
32
31
|
capture_to_episode,
|
|
33
32
|
capture_to_session,
|
|
34
33
|
load_captures_as_sessions,
|
|
35
34
|
)
|
|
36
35
|
|
|
37
|
-
__all__.extend(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
__all__.extend(
|
|
37
|
+
[
|
|
38
|
+
"capture_to_episode",
|
|
39
|
+
"capture_to_session",
|
|
40
|
+
"load_captures_as_sessions",
|
|
41
|
+
]
|
|
42
|
+
)
|
|
42
43
|
except ImportError:
|
|
43
44
|
pass
|