droidrun 0.3.9__py3-none-any.whl → 0.3.10.dev2__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.
- droidrun/__init__.py +2 -3
- droidrun/__main__.py +1 -1
- droidrun/agent/__init__.py +1 -1
- droidrun/agent/codeact/__init__.py +1 -4
- droidrun/agent/codeact/codeact_agent.py +66 -40
- droidrun/agent/codeact/events.py +6 -3
- droidrun/agent/codeact/prompts.py +2 -2
- droidrun/agent/common/events.py +4 -2
- droidrun/agent/context/__init__.py +1 -3
- droidrun/agent/context/agent_persona.py +2 -1
- droidrun/agent/context/context_injection_manager.py +6 -6
- droidrun/agent/context/episodic_memory.py +5 -3
- droidrun/agent/context/personas/__init__.py +3 -3
- droidrun/agent/context/personas/app_starter.py +3 -3
- droidrun/agent/context/personas/big_agent.py +3 -3
- droidrun/agent/context/personas/default.py +3 -3
- droidrun/agent/context/personas/ui_expert.py +5 -5
- droidrun/agent/context/task_manager.py +15 -17
- droidrun/agent/droid/__init__.py +1 -1
- droidrun/agent/droid/droid_agent.py +327 -180
- droidrun/agent/droid/events.py +91 -9
- droidrun/agent/executor/__init__.py +13 -0
- droidrun/agent/executor/events.py +24 -0
- droidrun/agent/executor/executor_agent.py +327 -0
- droidrun/agent/executor/prompts.py +136 -0
- droidrun/agent/manager/__init__.py +18 -0
- droidrun/agent/manager/events.py +20 -0
- droidrun/agent/manager/manager_agent.py +459 -0
- droidrun/agent/manager/prompts.py +223 -0
- droidrun/agent/oneflows/app_starter_workflow.py +118 -0
- droidrun/agent/oneflows/text_manipulator.py +204 -0
- droidrun/agent/planner/__init__.py +3 -3
- droidrun/agent/planner/events.py +6 -3
- droidrun/agent/planner/planner_agent.py +27 -42
- droidrun/agent/planner/prompts.py +2 -2
- droidrun/agent/usage.py +11 -11
- droidrun/agent/utils/__init__.py +11 -1
- droidrun/agent/utils/async_utils.py +2 -1
- droidrun/agent/utils/chat_utils.py +48 -60
- droidrun/agent/utils/device_state_formatter.py +177 -0
- droidrun/agent/utils/executer.py +12 -11
- droidrun/agent/utils/inference.py +114 -0
- droidrun/agent/utils/llm_picker.py +2 -0
- droidrun/agent/utils/message_utils.py +85 -0
- droidrun/agent/utils/tools.py +220 -0
- droidrun/agent/utils/trajectory.py +8 -7
- droidrun/cli/__init__.py +1 -1
- droidrun/cli/logs.py +29 -28
- droidrun/cli/main.py +279 -143
- droidrun/config_manager/__init__.py +25 -0
- droidrun/config_manager/config_manager.py +583 -0
- droidrun/macro/__init__.py +2 -2
- droidrun/macro/__main__.py +1 -1
- droidrun/macro/cli.py +36 -34
- droidrun/macro/replay.py +7 -9
- droidrun/portal.py +1 -1
- droidrun/telemetry/__init__.py +2 -2
- droidrun/telemetry/events.py +3 -4
- droidrun/telemetry/phoenix.py +173 -0
- droidrun/telemetry/tracker.py +7 -5
- droidrun/tools/__init__.py +1 -1
- droidrun/tools/adb.py +210 -82
- droidrun/tools/ios.py +7 -5
- droidrun/tools/tools.py +25 -8
- {droidrun-0.3.9.dist-info → droidrun-0.3.10.dev2.dist-info}/METADATA +6 -3
- droidrun-0.3.10.dev2.dist-info/RECORD +70 -0
- droidrun/agent/common/default.py +0 -5
- droidrun/agent/context/reflection.py +0 -20
- droidrun/agent/oneflows/reflector.py +0 -265
- droidrun-0.3.9.dist-info/RECORD +0 -56
- {droidrun-0.3.9.dist-info → droidrun-0.3.10.dev2.dist-info}/WHEEL +0 -0
- {droidrun-0.3.9.dist-info → droidrun-0.3.10.dev2.dist-info}/entry_points.txt +0 -0
- {droidrun-0.3.9.dist-info → droidrun-0.3.10.dev2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,583 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import os
|
4
|
+
import threading
|
5
|
+
import yaml
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Any, Callable, Optional, Dict
|
8
|
+
from dataclasses import dataclass, field, asdict
|
9
|
+
|
10
|
+
# ---------- Helpers / defaults ----------
|
11
|
+
def _default_config_text() -> str:
|
12
|
+
"""Generate default config.yaml content with all settings."""
|
13
|
+
return """# DroidRun Configuration File
|
14
|
+
# This file is auto-generated. Edit values as needed.
|
15
|
+
|
16
|
+
# === Agent Settings ===
|
17
|
+
agent:
|
18
|
+
# Maximum number of steps per task
|
19
|
+
max_steps: 15
|
20
|
+
# Enable vision capabilities per agent (screenshots)
|
21
|
+
vision:
|
22
|
+
manager: false
|
23
|
+
executor: false
|
24
|
+
codeact: false
|
25
|
+
# Enable planning with reasoning mode
|
26
|
+
reasoning: false
|
27
|
+
|
28
|
+
# === LLM Profiles ===
|
29
|
+
# Define LLM configurations for each agent type
|
30
|
+
llm_profiles:
|
31
|
+
# Manager: Plans and reasons about task progress
|
32
|
+
manager:
|
33
|
+
provider: GoogleGenAI
|
34
|
+
model: models/gemini-2.5-pro
|
35
|
+
temperature: 0.2
|
36
|
+
kwargs:
|
37
|
+
max_tokens: 8192
|
38
|
+
|
39
|
+
# Executor: Selects and executes atomic actions
|
40
|
+
executor:
|
41
|
+
provider: GoogleGenAI
|
42
|
+
model: models/gemini-2.5-pro
|
43
|
+
temperature: 0.1
|
44
|
+
kwargs:
|
45
|
+
max_tokens: 4096
|
46
|
+
|
47
|
+
# CodeAct: Generates and executes code actions
|
48
|
+
codeact:
|
49
|
+
provider: GoogleGenAI
|
50
|
+
model: models/gemini-2.5-pro
|
51
|
+
temperature: 0.2
|
52
|
+
kwargs:
|
53
|
+
max_tokens: 8192
|
54
|
+
|
55
|
+
# Text Manipulator: Edits text in input fields
|
56
|
+
text_manipulator:
|
57
|
+
provider: GoogleGenAI
|
58
|
+
model: models/gemini-2.5-pro
|
59
|
+
temperature: 0.3
|
60
|
+
kwargs:
|
61
|
+
max_tokens: 4096
|
62
|
+
|
63
|
+
# App Opener: Opens apps by name/description
|
64
|
+
app_opener:
|
65
|
+
provider: OpenAI
|
66
|
+
model: gpt-4o-mini
|
67
|
+
temperature: 0.0
|
68
|
+
base_url: null
|
69
|
+
api_base: null
|
70
|
+
kwargs:
|
71
|
+
max_tokens: 512
|
72
|
+
api_key: YOUR_API_KEY
|
73
|
+
|
74
|
+
# === Device Settings ===
|
75
|
+
device:
|
76
|
+
# Default device serial (null = auto-detect)
|
77
|
+
serial: null
|
78
|
+
# Use TCP communication instead of content provider
|
79
|
+
use_tcp: false
|
80
|
+
# Sleep duration after each action (seconds)
|
81
|
+
after_sleep_action: 1.0
|
82
|
+
|
83
|
+
# === Telemetry Settings ===
|
84
|
+
telemetry:
|
85
|
+
# Enable anonymous telemetry
|
86
|
+
enabled: true
|
87
|
+
|
88
|
+
# === Tracing Settings ===
|
89
|
+
tracing:
|
90
|
+
# Enable Arize Phoenix tracing
|
91
|
+
enabled: false
|
92
|
+
|
93
|
+
# === Logging Settings ===
|
94
|
+
logging:
|
95
|
+
# Enable debug logging
|
96
|
+
debug: false
|
97
|
+
# Trajectory saving level (none, step, action)
|
98
|
+
save_trajectory: none
|
99
|
+
|
100
|
+
# === Tool Settings ===
|
101
|
+
tools:
|
102
|
+
# Enable drag tool
|
103
|
+
allow_drag: false
|
104
|
+
"""
|
105
|
+
|
106
|
+
def _default_project_config_path() -> Path:
|
107
|
+
"""
|
108
|
+
Use module-relative resolution: two parents above this file -> project root.
|
109
|
+
"""
|
110
|
+
return Path(__file__).resolve().parents[2] / "config.yaml"
|
111
|
+
|
112
|
+
|
113
|
+
# ---------- Config Schema ----------
|
114
|
+
@dataclass
|
115
|
+
class LLMProfile:
|
116
|
+
"""LLM profile configuration."""
|
117
|
+
provider: str = "GoogleGenAI"
|
118
|
+
model: str = "models/gemini-2.0-flash-exp"
|
119
|
+
temperature: float = 0.2
|
120
|
+
base_url: Optional[str] = None
|
121
|
+
api_base: Optional[str] = None
|
122
|
+
kwargs: Dict[str, Any] = field(default_factory=dict)
|
123
|
+
|
124
|
+
def to_load_llm_kwargs(self) -> Dict[str, Any]:
|
125
|
+
"""Convert profile to kwargs for load_llm function."""
|
126
|
+
result = {
|
127
|
+
"model": self.model,
|
128
|
+
"temperature": self.temperature,
|
129
|
+
}
|
130
|
+
# Add optional URL parameters
|
131
|
+
if self.base_url:
|
132
|
+
result["base_url"] = self.base_url
|
133
|
+
if self.api_base:
|
134
|
+
result["api_base"] = self.api_base
|
135
|
+
# Merge additional kwargs
|
136
|
+
result.update(self.kwargs)
|
137
|
+
return result
|
138
|
+
|
139
|
+
|
140
|
+
@dataclass
|
141
|
+
class VisionConfig:
|
142
|
+
"""Per-agent vision settings."""
|
143
|
+
manager: bool = False
|
144
|
+
executor: bool = False
|
145
|
+
codeact: bool = False
|
146
|
+
|
147
|
+
def to_dict(self) -> Dict[str, bool]:
|
148
|
+
return {"manager": self.manager, "executor": self.executor, "codeact": self.codeact}
|
149
|
+
|
150
|
+
@classmethod
|
151
|
+
def from_dict(cls, data: Dict[str, Any]) -> "VisionConfig":
|
152
|
+
"""Create VisionConfig from dictionary or bool."""
|
153
|
+
if isinstance(data, bool):
|
154
|
+
# Support single bool → apply to all agents
|
155
|
+
return cls(manager=data, executor=data, codeact=data)
|
156
|
+
return cls(
|
157
|
+
manager=data.get("manager", False),
|
158
|
+
executor=data.get("executor", False),
|
159
|
+
codeact=data.get("codeact", False),
|
160
|
+
)
|
161
|
+
|
162
|
+
|
163
|
+
@dataclass
|
164
|
+
class AgentConfig:
|
165
|
+
"""Agent-related configuration."""
|
166
|
+
max_steps: int = 15
|
167
|
+
vision: VisionConfig = field(default_factory=VisionConfig)
|
168
|
+
reasoning: bool = False
|
169
|
+
after_sleep_action: float = 1.0
|
170
|
+
wait_for_stable_ui: float = 0.3
|
171
|
+
|
172
|
+
|
173
|
+
@dataclass
|
174
|
+
class DeviceConfig:
|
175
|
+
"""Device-related configuration."""
|
176
|
+
serial: Optional[str] = None
|
177
|
+
use_tcp: bool = False
|
178
|
+
|
179
|
+
|
180
|
+
@dataclass
|
181
|
+
class TelemetryConfig:
|
182
|
+
"""Telemetry configuration."""
|
183
|
+
enabled: bool = True
|
184
|
+
|
185
|
+
|
186
|
+
@dataclass
|
187
|
+
class TracingConfig:
|
188
|
+
"""Tracing configuration."""
|
189
|
+
enabled: bool = False
|
190
|
+
|
191
|
+
|
192
|
+
@dataclass
|
193
|
+
class LoggingConfig:
|
194
|
+
"""Logging configuration."""
|
195
|
+
debug: bool = False
|
196
|
+
save_trajectory: str = "none"
|
197
|
+
|
198
|
+
|
199
|
+
@dataclass
|
200
|
+
class ToolsConfig:
|
201
|
+
"""Tools configuration."""
|
202
|
+
allow_drag: bool = False
|
203
|
+
|
204
|
+
|
205
|
+
@dataclass
|
206
|
+
class DroidRunConfig:
|
207
|
+
"""Complete DroidRun configuration schema."""
|
208
|
+
agent: AgentConfig = field(default_factory=AgentConfig)
|
209
|
+
llm_profiles: Dict[str, LLMProfile] = field(default_factory=dict)
|
210
|
+
device: DeviceConfig = field(default_factory=DeviceConfig)
|
211
|
+
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
|
212
|
+
tracing: TracingConfig = field(default_factory=TracingConfig)
|
213
|
+
logging: LoggingConfig = field(default_factory=LoggingConfig)
|
214
|
+
tools: ToolsConfig = field(default_factory=ToolsConfig)
|
215
|
+
|
216
|
+
def __post_init__(self):
|
217
|
+
"""Ensure default profiles exist."""
|
218
|
+
if not self.llm_profiles:
|
219
|
+
self.llm_profiles = self._default_profiles()
|
220
|
+
|
221
|
+
@staticmethod
|
222
|
+
def _default_profiles() -> Dict[str, LLMProfile]:
|
223
|
+
"""Get default agent specific LLM profiles."""
|
224
|
+
return {
|
225
|
+
"manager": LLMProfile(
|
226
|
+
provider="GoogleGenAI",
|
227
|
+
model="models/gemini-2.5-pro",
|
228
|
+
temperature=0.2,
|
229
|
+
kwargs={}
|
230
|
+
),
|
231
|
+
"executor": LLMProfile(
|
232
|
+
provider="GoogleGenAI",
|
233
|
+
model="models/gemini-2.5-pro",
|
234
|
+
temperature=0.1,
|
235
|
+
kwargs={}
|
236
|
+
),
|
237
|
+
"codeact": LLMProfile(
|
238
|
+
provider="GoogleGenAI",
|
239
|
+
model="models/gemini-2.5-pro",
|
240
|
+
temperature=0.2,
|
241
|
+
kwargs={"max_tokens": 8192 }
|
242
|
+
),
|
243
|
+
"text_manipulator": LLMProfile(
|
244
|
+
provider="GoogleGenAI",
|
245
|
+
model="models/gemini-2.5-pro",
|
246
|
+
temperature=0.3,
|
247
|
+
kwargs={}
|
248
|
+
),
|
249
|
+
"app_opener": LLMProfile(
|
250
|
+
provider="OpenAI",
|
251
|
+
model="models/gemini-2.5-pro",
|
252
|
+
temperature=0.0,
|
253
|
+
kwargs={}
|
254
|
+
),
|
255
|
+
}
|
256
|
+
|
257
|
+
def to_dict(self) -> Dict[str, Any]:
|
258
|
+
"""Convert config to dictionary."""
|
259
|
+
result = asdict(self)
|
260
|
+
# Convert LLMProfile objects to dicts
|
261
|
+
result["llm_profiles"] = {
|
262
|
+
name: asdict(profile) for name, profile in self.llm_profiles.items()
|
263
|
+
}
|
264
|
+
# Convert VisionConfig to dict
|
265
|
+
if isinstance(result["agent"]["vision"], dict):
|
266
|
+
pass # Already a dict from asdict
|
267
|
+
else:
|
268
|
+
result["agent"]["vision"] = self.agent.vision.to_dict()
|
269
|
+
return result
|
270
|
+
|
271
|
+
@classmethod
|
272
|
+
def from_dict(cls, data: Dict[str, Any]) -> "DroidRunConfig":
|
273
|
+
"""Create config from dictionary."""
|
274
|
+
# Parse LLM profiles
|
275
|
+
llm_profiles = {}
|
276
|
+
for name, profile_data in data.get("llm_profiles", {}).items():
|
277
|
+
llm_profiles[name] = LLMProfile(**profile_data)
|
278
|
+
|
279
|
+
# Parse agent config with vision
|
280
|
+
agent_data = data.get("agent", {})
|
281
|
+
vision_data = agent_data.get("vision", {})
|
282
|
+
vision_config = VisionConfig.from_dict(vision_data)
|
283
|
+
|
284
|
+
agent_config = AgentConfig(
|
285
|
+
max_steps=agent_data.get("max_steps", 15),
|
286
|
+
vision=vision_config,
|
287
|
+
reasoning=agent_data.get("reasoning", False),
|
288
|
+
)
|
289
|
+
|
290
|
+
return cls(
|
291
|
+
agent=agent_config,
|
292
|
+
llm_profiles=llm_profiles,
|
293
|
+
device=DeviceConfig(**data.get("device", {})),
|
294
|
+
telemetry=TelemetryConfig(**data.get("telemetry", {})),
|
295
|
+
tracing=TracingConfig(**data.get("tracing", {})),
|
296
|
+
logging=LoggingConfig(**data.get("logging", {})),
|
297
|
+
tools=ToolsConfig(**data.get("tools", {})),
|
298
|
+
)
|
299
|
+
|
300
|
+
|
301
|
+
# ---------- ConfigManager ----------
|
302
|
+
class ConfigManager:
|
303
|
+
"""
|
304
|
+
Thread-safe singleton ConfigManager with typed configuration schema.
|
305
|
+
|
306
|
+
Usage:
|
307
|
+
from droidrun.config_manager import config
|
308
|
+
|
309
|
+
# Access typed config objects
|
310
|
+
print(config.agent.max_steps)
|
311
|
+
|
312
|
+
# Load all 3 LLMs
|
313
|
+
llms = config.load_all_llms()
|
314
|
+
fast_llm = llms['fast']
|
315
|
+
mid_llm = llms['mid']
|
316
|
+
smart_llm = llms['smart']
|
317
|
+
|
318
|
+
# Modify and save
|
319
|
+
config.save()
|
320
|
+
"""
|
321
|
+
_instance: Optional["ConfigManager"] = None
|
322
|
+
_instance_lock = threading.Lock()
|
323
|
+
|
324
|
+
def __new__(cls, path: Optional[str] = None):
|
325
|
+
# ensure singleton
|
326
|
+
with cls._instance_lock:
|
327
|
+
if cls._instance is None:
|
328
|
+
cls._instance = super().__new__(cls)
|
329
|
+
cls._instance._initialized = False
|
330
|
+
return cls._instance
|
331
|
+
|
332
|
+
def __init__(self, path: Optional[str] = None):
|
333
|
+
if getattr(self, "_initialized", False):
|
334
|
+
return
|
335
|
+
|
336
|
+
self._lock = threading.RLock()
|
337
|
+
|
338
|
+
# resolution order:
|
339
|
+
# 1) explicit path arg
|
340
|
+
# 2) DROIDRUN_CONFIG env var
|
341
|
+
# 3) module-relative project_root/config.yaml (two parents up)
|
342
|
+
if path:
|
343
|
+
self.path = Path(path).expanduser().resolve()
|
344
|
+
else:
|
345
|
+
env = os.environ.get("DROIDRUN_CONFIG")
|
346
|
+
if env:
|
347
|
+
self.path = Path(env).expanduser().resolve()
|
348
|
+
else:
|
349
|
+
self.path = _default_project_config_path().resolve()
|
350
|
+
|
351
|
+
# Initialize with default config
|
352
|
+
self._config = DroidRunConfig()
|
353
|
+
self.validate_fn: Optional[Callable[[DroidRunConfig], None]] = None
|
354
|
+
|
355
|
+
self._ensure_file_exists()
|
356
|
+
self.load_config()
|
357
|
+
|
358
|
+
self._initialized = True
|
359
|
+
|
360
|
+
# ---------------- Typed property access ----------------
|
361
|
+
@property
|
362
|
+
def agent(self) -> AgentConfig:
|
363
|
+
"""Access agent configuration."""
|
364
|
+
with self._lock:
|
365
|
+
return self._config.agent
|
366
|
+
|
367
|
+
@property
|
368
|
+
def device(self) -> DeviceConfig:
|
369
|
+
"""Access device configuration."""
|
370
|
+
with self._lock:
|
371
|
+
return self._config.device
|
372
|
+
|
373
|
+
@property
|
374
|
+
def telemetry(self) -> TelemetryConfig:
|
375
|
+
"""Access telemetry configuration."""
|
376
|
+
with self._lock:
|
377
|
+
return self._config.telemetry
|
378
|
+
|
379
|
+
@property
|
380
|
+
def tracing(self) -> TracingConfig:
|
381
|
+
"""Access tracing configuration."""
|
382
|
+
with self._lock:
|
383
|
+
return self._config.tracing
|
384
|
+
|
385
|
+
@property
|
386
|
+
def logging(self) -> LoggingConfig:
|
387
|
+
"""Access logging configuration."""
|
388
|
+
with self._lock:
|
389
|
+
return self._config.logging
|
390
|
+
|
391
|
+
@property
|
392
|
+
def tools(self) -> ToolsConfig:
|
393
|
+
"""Access tools configuration."""
|
394
|
+
with self._lock:
|
395
|
+
return self._config.tools
|
396
|
+
|
397
|
+
@property
|
398
|
+
def llm_profiles(self) -> Dict[str, LLMProfile]:
|
399
|
+
"""Access LLM profiles."""
|
400
|
+
with self._lock:
|
401
|
+
return self._config.llm_profiles
|
402
|
+
|
403
|
+
# ---------------- LLM Profile Helpers ----------------
|
404
|
+
def get_llm_profile(self, profile_name: str) -> LLMProfile:
|
405
|
+
"""
|
406
|
+
Get an LLM profile by name.
|
407
|
+
|
408
|
+
Args:
|
409
|
+
profile_name: Name of the profile (fast, mid, smart, custom, etc.)
|
410
|
+
|
411
|
+
Returns:
|
412
|
+
LLMProfile object
|
413
|
+
|
414
|
+
Raises:
|
415
|
+
KeyError: If profile_name doesn't exist
|
416
|
+
"""
|
417
|
+
with self._lock:
|
418
|
+
if profile_name not in self._config.llm_profiles:
|
419
|
+
raise KeyError(
|
420
|
+
f"LLM profile '{profile_name}' not found. "
|
421
|
+
f"Available profiles: {list(self._config.llm_profiles.keys())}"
|
422
|
+
)
|
423
|
+
|
424
|
+
return self._config.llm_profiles[profile_name]
|
425
|
+
|
426
|
+
def load_llm_from_profile(self, profile_name: str, **override_kwargs):
|
427
|
+
"""
|
428
|
+
Load an LLM using a profile configuration.
|
429
|
+
|
430
|
+
Args:
|
431
|
+
profile_name: Name of the profile to use (fast, mid, smart, custom)
|
432
|
+
**override_kwargs: Additional kwargs to override profile settings
|
433
|
+
|
434
|
+
Returns:
|
435
|
+
Initialized LLM instance
|
436
|
+
|
437
|
+
Example:
|
438
|
+
# Use specific profile
|
439
|
+
llm = config.load_llm_from_profile("smart")
|
440
|
+
|
441
|
+
# Override specific settings
|
442
|
+
llm = config.load_llm_from_profile("fast", temperature=0.5)
|
443
|
+
"""
|
444
|
+
from droidrun.agent.utils.llm_picker import load_llm
|
445
|
+
|
446
|
+
profile = self.get_llm_profile(profile_name)
|
447
|
+
|
448
|
+
# Get kwargs from profile
|
449
|
+
kwargs = profile.to_load_llm_kwargs()
|
450
|
+
|
451
|
+
# Override with any provided kwargs
|
452
|
+
kwargs.update(override_kwargs)
|
453
|
+
|
454
|
+
# Load the LLM
|
455
|
+
return load_llm(provider_name=profile.provider, **kwargs)
|
456
|
+
|
457
|
+
def load_all_llms(self, profile_names: Optional[list[str]] = None, **override_kwargs_per_profile):
|
458
|
+
"""
|
459
|
+
Load multiple LLMs from profiles for different use cases.
|
460
|
+
|
461
|
+
Args:
|
462
|
+
profile_names: List of profile names to load. If None, loads agent-specific profiles
|
463
|
+
**override_kwargs_per_profile: Dict of profile-specific overrides
|
464
|
+
Example: manager={'temperature': 0.1}, executor={'max_tokens': 8000}
|
465
|
+
|
466
|
+
Returns:
|
467
|
+
Dict mapping profile names to initialized LLM instances
|
468
|
+
|
469
|
+
Example:
|
470
|
+
# Load all agent-specific profiles
|
471
|
+
llms = config.load_all_llms()
|
472
|
+
manager_llm = llms['manager']
|
473
|
+
executor_llm = llms['executor']
|
474
|
+
codeact_llm = llms['codeact']
|
475
|
+
|
476
|
+
# Load specific profiles
|
477
|
+
llms = config.load_all_llms(['manager', 'executor'])
|
478
|
+
|
479
|
+
# Load with overrides
|
480
|
+
llms = config.load_all_llms(
|
481
|
+
manager={'temperature': 0.1},
|
482
|
+
executor={'max_tokens': 8000}
|
483
|
+
)
|
484
|
+
"""
|
485
|
+
from droidrun.agent.utils.llm_picker import load_llm
|
486
|
+
|
487
|
+
if profile_names is None:
|
488
|
+
profile_names = ["manager", "executor", "codeact", "text_manipulator", "app_opener"]
|
489
|
+
|
490
|
+
llms = {}
|
491
|
+
for profile_name in profile_names:
|
492
|
+
profile = self.get_llm_profile(profile_name)
|
493
|
+
|
494
|
+
# Get kwargs from profile
|
495
|
+
kwargs = profile.to_load_llm_kwargs()
|
496
|
+
|
497
|
+
# Apply profile-specific overrides if provided
|
498
|
+
if profile_name in override_kwargs_per_profile:
|
499
|
+
kwargs.update(override_kwargs_per_profile[profile_name])
|
500
|
+
|
501
|
+
# Load the LLM
|
502
|
+
llms[profile_name] = load_llm(provider_name=profile.provider, **kwargs)
|
503
|
+
|
504
|
+
return llms
|
505
|
+
|
506
|
+
# ---------------- I/O ----------------
|
507
|
+
def _ensure_file_exists(self) -> None:
|
508
|
+
parent = self.path.parent
|
509
|
+
parent.mkdir(parents=True, exist_ok=True)
|
510
|
+
if not self.path.exists():
|
511
|
+
with open(self.path, "w", encoding="utf-8") as f:
|
512
|
+
f.write(_default_config_text())
|
513
|
+
|
514
|
+
def load_config(self) -> None:
|
515
|
+
"""Load YAML from file into memory. Runs validator if registered."""
|
516
|
+
with self._lock:
|
517
|
+
if not self.path.exists():
|
518
|
+
# create starter file and set default config
|
519
|
+
self._ensure_file_exists()
|
520
|
+
self._config = DroidRunConfig()
|
521
|
+
return
|
522
|
+
|
523
|
+
with open(self.path, "r", encoding="utf-8") as f:
|
524
|
+
data = yaml.safe_load(f)
|
525
|
+
if data:
|
526
|
+
try:
|
527
|
+
self._config = DroidRunConfig.from_dict(data)
|
528
|
+
except Exception as e:
|
529
|
+
# If parsing fails, use defaults and log warning
|
530
|
+
import logging
|
531
|
+
logger = logging.getLogger("droidrun")
|
532
|
+
logger.warning(f"Failed to parse config, using defaults: {e}")
|
533
|
+
self._config = DroidRunConfig()
|
534
|
+
else:
|
535
|
+
self._config = DroidRunConfig()
|
536
|
+
self._run_validation()
|
537
|
+
|
538
|
+
def save(self) -> None:
|
539
|
+
"""Persist current in-memory config to YAML file."""
|
540
|
+
with self._lock:
|
541
|
+
with open(self.path, "w", encoding="utf-8") as f:
|
542
|
+
yaml.dump(self._config.to_dict(), f, sort_keys=False, default_flow_style=False)
|
543
|
+
|
544
|
+
def reload(self) -> None:
|
545
|
+
"""Reload config from disk (useful when edited externally or via UI)."""
|
546
|
+
self.load_config()
|
547
|
+
|
548
|
+
# ---------------- Validation ----------------
|
549
|
+
def register_validator(self, fn: Callable[[DroidRunConfig], None]) -> None:
|
550
|
+
"""
|
551
|
+
Register a validation function that takes the config object and raises
|
552
|
+
an exception if invalid. The validator is run immediately on registration.
|
553
|
+
"""
|
554
|
+
with self._lock:
|
555
|
+
self.validate_fn = fn
|
556
|
+
self._run_validation()
|
557
|
+
|
558
|
+
def _run_validation(self) -> None:
|
559
|
+
if self.validate_fn is None:
|
560
|
+
return
|
561
|
+
try:
|
562
|
+
self.validate_fn(self._config)
|
563
|
+
except Exception as exc:
|
564
|
+
raise Exception(f"Validation failed: {exc}") from exc
|
565
|
+
|
566
|
+
def as_dict(self) -> Dict[str, Any]:
|
567
|
+
"""Return a deep copy of the config dict to avoid accidental mutation."""
|
568
|
+
with self._lock:
|
569
|
+
import copy
|
570
|
+
return copy.deepcopy(self._config.to_dict())
|
571
|
+
|
572
|
+
# useful for tests to reset singleton state
|
573
|
+
@classmethod
|
574
|
+
def _reset_instance_for_testing(cls) -> None:
|
575
|
+
with cls._instance_lock:
|
576
|
+
cls._instance = None
|
577
|
+
|
578
|
+
def __repr__(self) -> str:
|
579
|
+
return f"<ConfigManager path={self.path!s}>"
|
580
|
+
|
581
|
+
|
582
|
+
# ---------- global singleton ----------
|
583
|
+
config = ConfigManager()
|
droidrun/macro/__init__.py
CHANGED
droidrun/macro/__main__.py
CHANGED