openadapt-ml 0.1.0__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.
Files changed (55) hide show
  1. openadapt_ml/__init__.py +0 -0
  2. openadapt_ml/benchmarks/__init__.py +125 -0
  3. openadapt_ml/benchmarks/agent.py +825 -0
  4. openadapt_ml/benchmarks/azure.py +761 -0
  5. openadapt_ml/benchmarks/base.py +366 -0
  6. openadapt_ml/benchmarks/cli.py +884 -0
  7. openadapt_ml/benchmarks/data_collection.py +432 -0
  8. openadapt_ml/benchmarks/runner.py +381 -0
  9. openadapt_ml/benchmarks/waa.py +704 -0
  10. openadapt_ml/cloud/__init__.py +5 -0
  11. openadapt_ml/cloud/azure_inference.py +441 -0
  12. openadapt_ml/cloud/lambda_labs.py +2445 -0
  13. openadapt_ml/cloud/local.py +790 -0
  14. openadapt_ml/config.py +56 -0
  15. openadapt_ml/datasets/__init__.py +0 -0
  16. openadapt_ml/datasets/next_action.py +507 -0
  17. openadapt_ml/evals/__init__.py +23 -0
  18. openadapt_ml/evals/grounding.py +241 -0
  19. openadapt_ml/evals/plot_eval_metrics.py +174 -0
  20. openadapt_ml/evals/trajectory_matching.py +486 -0
  21. openadapt_ml/grounding/__init__.py +45 -0
  22. openadapt_ml/grounding/base.py +236 -0
  23. openadapt_ml/grounding/detector.py +570 -0
  24. openadapt_ml/ingest/__init__.py +43 -0
  25. openadapt_ml/ingest/capture.py +312 -0
  26. openadapt_ml/ingest/loader.py +232 -0
  27. openadapt_ml/ingest/synthetic.py +1102 -0
  28. openadapt_ml/models/__init__.py +0 -0
  29. openadapt_ml/models/api_adapter.py +171 -0
  30. openadapt_ml/models/base_adapter.py +59 -0
  31. openadapt_ml/models/dummy_adapter.py +42 -0
  32. openadapt_ml/models/qwen_vl.py +426 -0
  33. openadapt_ml/runtime/__init__.py +0 -0
  34. openadapt_ml/runtime/policy.py +182 -0
  35. openadapt_ml/schemas/__init__.py +53 -0
  36. openadapt_ml/schemas/sessions.py +122 -0
  37. openadapt_ml/schemas/validation.py +252 -0
  38. openadapt_ml/scripts/__init__.py +0 -0
  39. openadapt_ml/scripts/compare.py +1490 -0
  40. openadapt_ml/scripts/demo_policy.py +62 -0
  41. openadapt_ml/scripts/eval_policy.py +287 -0
  42. openadapt_ml/scripts/make_gif.py +153 -0
  43. openadapt_ml/scripts/prepare_synthetic.py +43 -0
  44. openadapt_ml/scripts/run_qwen_login_benchmark.py +192 -0
  45. openadapt_ml/scripts/train.py +174 -0
  46. openadapt_ml/training/__init__.py +0 -0
  47. openadapt_ml/training/benchmark_viewer.py +1538 -0
  48. openadapt_ml/training/shared_ui.py +157 -0
  49. openadapt_ml/training/stub_provider.py +276 -0
  50. openadapt_ml/training/trainer.py +2446 -0
  51. openadapt_ml/training/viewer.py +2970 -0
  52. openadapt_ml-0.1.0.dist-info/METADATA +818 -0
  53. openadapt_ml-0.1.0.dist-info/RECORD +55 -0
  54. openadapt_ml-0.1.0.dist-info/WHEEL +4 -0
  55. openadapt_ml-0.1.0.dist-info/licenses/LICENSE +21 -0
File without changes
@@ -0,0 +1,171 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ import base64
6
+ import os
7
+
8
+ import torch
9
+
10
+ from openadapt_ml.config import settings
11
+ from openadapt_ml.models.base_adapter import BaseVLMAdapter, get_default_device
12
+
13
+
14
+ class ApiVLMAdapter(BaseVLMAdapter):
15
+ """Inference-only adapter for hosted VLM APIs (Anthropic, OpenAI).
16
+
17
+ This adapter implements `generate` only; `prepare_inputs` and
18
+ `compute_loss` are not supported and will raise NotImplementedError.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ provider: str,
24
+ device: Optional[torch.device] = None,
25
+ api_key: Optional[str] = None,
26
+ ) -> None:
27
+ """Initialize an API-backed adapter.
28
+
29
+ Parameters
30
+ ----------
31
+ provider:
32
+ "anthropic" or "openai".
33
+ device:
34
+ Unused for remote APIs but kept for BaseVLMAdapter compatibility.
35
+ api_key:
36
+ Optional API key override. If not provided, keys are loaded from:
37
+ 1. Settings (.env file)
38
+ 2. Environment variables (ANTHROPIC_API_KEY / OPENAI_API_KEY)
39
+ 3. Error if not found
40
+ """
41
+
42
+ self.provider = provider
43
+
44
+ if provider == "anthropic":
45
+ try:
46
+ from anthropic import Anthropic # type: ignore[import]
47
+ except Exception as exc: # pragma: no cover - import-time failure
48
+ raise RuntimeError(
49
+ "anthropic package is required for provider='anthropic'. "
50
+ "Install with `uv sync --extra api`."
51
+ ) from exc
52
+
53
+ key = api_key or settings.anthropic_api_key or os.getenv("ANTHROPIC_API_KEY")
54
+ if not key:
55
+ raise RuntimeError(
56
+ "ANTHROPIC_API_KEY is required but not found. "
57
+ "Please set it in .env file, environment variable, or pass api_key parameter."
58
+ )
59
+ client = Anthropic(api_key=key)
60
+ elif provider == "openai":
61
+ try:
62
+ from openai import OpenAI # type: ignore[import]
63
+ except Exception as exc: # pragma: no cover - import-time failure
64
+ raise RuntimeError(
65
+ "openai package is required for provider='openai'. "
66
+ "Install with `uv sync --extra api`."
67
+ ) from exc
68
+
69
+ key = api_key or settings.openai_api_key or os.getenv("OPENAI_API_KEY")
70
+ if not key:
71
+ raise RuntimeError(
72
+ "OPENAI_API_KEY is required but not found. "
73
+ "Please set it in .env file, environment variable, or pass api_key parameter."
74
+ )
75
+ client = OpenAI(api_key=key)
76
+ else:
77
+ raise ValueError(f"Unsupported provider: {provider}")
78
+
79
+ if device is None:
80
+ device = get_default_device()
81
+
82
+ # Store client separately; BaseVLMAdapter expects a model + processor, so
83
+ # we pass a tiny dummy module and the client as the "processor".
84
+ self._client = client
85
+ model = torch.nn.Identity()
86
+ processor: Any = client
87
+ super().__init__(model=model, processor=processor, device=device)
88
+
89
+ def prepare_inputs(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: # type: ignore[override]
90
+ raise NotImplementedError("ApiVLMAdapter does not support training (prepare_inputs)")
91
+
92
+ def compute_loss(self, inputs: Dict[str, Any]) -> torch.Tensor: # type: ignore[override]
93
+ raise NotImplementedError("ApiVLMAdapter does not support training (compute_loss)")
94
+
95
+ def generate(self, sample: Dict[str, Any], max_new_tokens: int = 64) -> str: # type: ignore[override]
96
+ images = sample.get("images", [])
97
+ if not images:
98
+ raise ValueError("Sample is missing image paths")
99
+ image_path = images[0]
100
+
101
+ messages = sample.get("messages", [])
102
+ system_text = ""
103
+ user_text = ""
104
+ for m in messages:
105
+ role = m.get("role")
106
+ if role == "system":
107
+ system_text = m.get("content", "")
108
+ elif role == "user":
109
+ user_text = m.get("content", "")
110
+
111
+ with open(image_path, "rb") as f:
112
+ image_bytes = f.read()
113
+
114
+ if self.provider == "anthropic":
115
+ client: Any = self._client
116
+ image_b64 = base64.b64encode(image_bytes).decode("utf-8")
117
+
118
+ content: List[Dict[str, Any]] = []
119
+ if user_text:
120
+ content.append({"type": "text", "text": user_text})
121
+ content.append(
122
+ {
123
+ "type": "image",
124
+ "source": {
125
+ "type": "base64",
126
+ "media_type": "image/png",
127
+ "data": image_b64,
128
+ },
129
+ }
130
+ )
131
+
132
+ resp = client.messages.create(
133
+ model="claude-sonnet-4-5-20250929",
134
+ max_tokens=max_new_tokens,
135
+ system=system_text or None,
136
+ messages=[{"role": "user", "content": content}],
137
+ )
138
+
139
+ # Anthropic messages API returns a list of content blocks.
140
+ parts = getattr(resp, "content", [])
141
+ texts = [getattr(p, "text", "") for p in parts if getattr(p, "type", "") == "text"]
142
+ return "\n".join([t for t in texts if t]).strip()
143
+
144
+ if self.provider == "openai":
145
+ client: Any = self._client
146
+ base64_image = base64.b64encode(image_bytes).decode("utf-8")
147
+
148
+ messages_payload: List[Dict[str, Any]] = []
149
+ if system_text:
150
+ messages_payload.append({"role": "system", "content": system_text})
151
+
152
+ user_content: List[Dict[str, Any]] = []
153
+ if user_text:
154
+ user_content.append({"type": "text", "text": user_text})
155
+ user_content.append(
156
+ {
157
+ "type": "image_url",
158
+ "image_url": {"url": f"data:image/png;base64,{base64_image}"},
159
+ }
160
+ )
161
+ messages_payload.append({"role": "user", "content": user_content})
162
+
163
+ resp = client.chat.completions.create(
164
+ model="gpt-5.1",
165
+ messages=messages_payload,
166
+ max_completion_tokens=max_new_tokens,
167
+ )
168
+ return resp.choices[0].message.content or ""
169
+
170
+ # Should be unreachable because provider is validated in __init__.
171
+ raise ValueError(f"Unsupported provider: {self.provider}")
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Dict, List, Optional
5
+
6
+ import torch
7
+
8
+
9
+ def get_default_device() -> torch.device:
10
+ """Select cuda, then mps, then cpu.
11
+
12
+ This is used as a fallback when no explicit device is provided.
13
+ """
14
+
15
+ if torch.cuda.is_available():
16
+ return torch.device("cuda")
17
+ if getattr(torch.backends, "mps", None) is not None and torch.backends.mps.is_available(): # type: ignore[attr-defined]
18
+ return torch.device("mps")
19
+ return torch.device("cpu")
20
+
21
+
22
+ class BaseVLMAdapter(ABC):
23
+ """Abstract wrapper around a vision-language model + processor.
24
+
25
+ Concrete implementations are responsible for:
26
+ - converting SFT-style samples into model inputs (tokenization, image processing)
27
+ - computing supervised training loss
28
+ - generating assistant text given a single sample at inference time
29
+ """
30
+
31
+ def __init__(self, model: torch.nn.Module, processor: Any, device: Optional[torch.device] = None) -> None:
32
+ self.model = model
33
+ self.processor = processor
34
+ self.device = device or get_default_device()
35
+ self.model.to(self.device)
36
+
37
+ @abstractmethod
38
+ def prepare_inputs(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]:
39
+ """Convert a batch of SFT samples into model inputs.
40
+
41
+ The batch is a list of samples of the form produced by
42
+ `build_next_action_sft_samples` (images + messages).
43
+ Implementations should return a dict suitable for passing to the
44
+ underlying HF model, including `labels` for supervised loss.
45
+ """
46
+
47
+ @abstractmethod
48
+ def compute_loss(self, inputs: Dict[str, Any]) -> torch.Tensor:
49
+ """Run the model forward and return a scalar loss tensor."""
50
+
51
+ @abstractmethod
52
+ def generate(self, sample: Dict[str, Any], max_new_tokens: int = 64) -> str:
53
+ """Generate assistant text for a single SFT-style sample."""
54
+
55
+ def train(self) -> None:
56
+ self.model.train()
57
+
58
+ def eval(self) -> None:
59
+ self.model.eval()
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Optional
4
+
5
+ import torch
6
+ from torch import nn
7
+
8
+ from openadapt_ml.models.base_adapter import BaseVLMAdapter, get_default_device
9
+
10
+
11
+ class DummyAdapter(BaseVLMAdapter):
12
+ """Minimal adapter used to validate the training loop.
13
+
14
+ - Ignores images/messages content.
15
+ - Uses a tiny linear model and returns a simple MSE loss.
16
+ - generate() returns a fixed string.
17
+ """
18
+
19
+ def __init__(self, device: Optional[torch.device] = None) -> None:
20
+ if device is None:
21
+ device = get_default_device()
22
+ # Tiny dummy model with a few parameters
23
+ model = nn.Linear(4, 1)
24
+ processor: Any = None
25
+ super().__init__(model=model, processor=processor, device=device)
26
+
27
+ def prepare_inputs(self, batch: List[Dict[str, Any]]) -> Dict[str, Any]: # type: ignore[override]
28
+ batch_size = len(batch)
29
+ # Create a dummy input tensor; real adapters will encode images + text.
30
+ x = torch.zeros(batch_size, 4, device=self.device)
31
+ # Target is a constant zero tensor; loss will be ||model(x)||^2.
32
+ y = torch.zeros(batch_size, 1, device=self.device)
33
+ return {"inputs": x, "targets": y}
34
+
35
+ def compute_loss(self, inputs: Dict[str, Any]) -> torch.Tensor: # type: ignore[override]
36
+ x = inputs["inputs"]
37
+ y = inputs["targets"]
38
+ preds = self.model(x)
39
+ return torch.mean((preds - y) ** 2)
40
+
41
+ def generate(self, sample: Dict[str, Any], max_new_tokens: int = 64) -> str: # type: ignore[override]
42
+ return "DONE()"