autodevloop 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.
- autodevloop/__init__.py +5 -0
- autodevloop/__main__.py +6 -0
- autodevloop/cli.py +233 -0
- autodevloop/config.py +165 -0
- autodevloop/engine.py +750 -0
- autodevloop/llm.py +259 -0
- autodevloop/prompts.py +342 -0
- autodevloop/py.typed +0 -0
- autodevloop/registry.py +37 -0
- autodevloop/reporting.py +127 -0
- autodevloop/testing.py +119 -0
- autodevloop/util.py +250 -0
- autodevloop/vcs.py +74 -0
- autodevloop/webapp.py +1184 -0
- autodevloop/yaml_compat.py +192 -0
- autodevloop-0.1.0.dist-info/METADATA +332 -0
- autodevloop-0.1.0.dist-info/RECORD +21 -0
- autodevloop-0.1.0.dist-info/WHEEL +5 -0
- autodevloop-0.1.0.dist-info/entry_points.txt +2 -0
- autodevloop-0.1.0.dist-info/licenses/LICENSE +21 -0
- autodevloop-0.1.0.dist-info/top_level.txt +1 -0
autodevloop/__init__.py
ADDED
autodevloop/__main__.py
ADDED
autodevloop/cli.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""Command-line interface for AutoDevLoop."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .config import deep_get, deep_merge, load_config, save_config
|
|
12
|
+
from .engine import AutoDevLoop
|
|
13
|
+
from .util import APP_DIR, STATE_FILE, STOP_FILE, load_json, now_text, write_text
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve_project_dir(raw: str | None, base_dir: Path | None = None) -> Path:
|
|
17
|
+
base = (base_dir or Path.cwd()).resolve()
|
|
18
|
+
if not raw:
|
|
19
|
+
return base
|
|
20
|
+
path = Path(raw).expanduser()
|
|
21
|
+
return path.resolve() if path.is_absolute() else (base / path).resolve()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _prompt(label: str, default: str = "") -> str:
|
|
25
|
+
suffix = f" [{default}]" if default else ""
|
|
26
|
+
return input(f"{label}{suffix}: ").strip() or default
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _prompt_int(label: str, default: int, minimum: int = 1) -> int:
|
|
30
|
+
while True:
|
|
31
|
+
raw = _prompt(label, str(default))
|
|
32
|
+
try:
|
|
33
|
+
value = int(raw)
|
|
34
|
+
except ValueError:
|
|
35
|
+
print(f"Please enter an integer >= {minimum}.")
|
|
36
|
+
continue
|
|
37
|
+
if value >= minimum:
|
|
38
|
+
return value
|
|
39
|
+
print(f"Please enter an integer >= {minimum}.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _prompt_multiline(label: str) -> str:
|
|
43
|
+
print(label)
|
|
44
|
+
print("Enter the requirement text. Finish with a single line containing only END.")
|
|
45
|
+
lines: list[str] = []
|
|
46
|
+
while True:
|
|
47
|
+
line = input()
|
|
48
|
+
if line.strip().upper() == "END":
|
|
49
|
+
break
|
|
50
|
+
lines.append(line)
|
|
51
|
+
return "\n".join(lines).strip()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _overrides_from_args(args: argparse.Namespace) -> dict[str, Any]:
|
|
55
|
+
"""Build a config-shaped override dict from any explicitly-set CLI flags."""
|
|
56
|
+
o: dict[str, Any] = {"project": {}, "provider": {}, "pipeline": {}, "agents": {}, "review": {}, "fix": {}, "tests": {}, "vcs": {}}
|
|
57
|
+
if getattr(args, "project_name", ""):
|
|
58
|
+
o["project"]["name"] = args.project_name
|
|
59
|
+
if getattr(args, "arch_hint", ""):
|
|
60
|
+
o["project"]["arch_hint"] = args.arch_hint
|
|
61
|
+
if getattr(args, "mode", ""):
|
|
62
|
+
o["pipeline"]["mode"] = args.mode
|
|
63
|
+
if getattr(args, "provider", ""):
|
|
64
|
+
o["provider"]["name"] = args.provider
|
|
65
|
+
if getattr(args, "provider_command", ""):
|
|
66
|
+
o["provider"]["command"] = args.provider_command
|
|
67
|
+
if getattr(args, "model", ""):
|
|
68
|
+
o["provider"]["model"] = args.model
|
|
69
|
+
if getattr(args, "agent_timeout", None):
|
|
70
|
+
o["agents"]["timeout"] = args.agent_timeout
|
|
71
|
+
if getattr(args, "max_parallel_agents", None):
|
|
72
|
+
o["agents"]["max_parallel"] = args.max_parallel_agents
|
|
73
|
+
if getattr(args, "no_parallel", False):
|
|
74
|
+
o["agents"]["allow_parallel"] = False
|
|
75
|
+
if getattr(args, "review_threshold", None):
|
|
76
|
+
o["review"]["threshold"] = args.review_threshold
|
|
77
|
+
if getattr(args, "fix_retries", None) is not None:
|
|
78
|
+
o["fix"]["retries"] = args.fix_retries
|
|
79
|
+
if getattr(args, "test_command", ""):
|
|
80
|
+
o["tests"]["command"] = args.test_command
|
|
81
|
+
if getattr(args, "test_timeout", None):
|
|
82
|
+
o["tests"]["timeout"] = args.test_timeout
|
|
83
|
+
if getattr(args, "no_git", False):
|
|
84
|
+
o["vcs"]["git"] = False
|
|
85
|
+
return {k: v for k, v in o.items() if v}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def cmd_run(args: argparse.Namespace) -> None:
|
|
89
|
+
execution_dir = Path.cwd().resolve()
|
|
90
|
+
if not args.non_interactive and not args.project_dir:
|
|
91
|
+
print("[AutoDevLoop] Project directory setup.")
|
|
92
|
+
print(f"Absolute example: {execution_dir}")
|
|
93
|
+
print("Relative example: . or demo-project")
|
|
94
|
+
args.project_dir = _prompt("Project directory", str(execution_dir))
|
|
95
|
+
|
|
96
|
+
root = resolve_project_dir(args.project_dir, execution_dir)
|
|
97
|
+
config = deep_merge(load_config(root), _overrides_from_args(args))
|
|
98
|
+
|
|
99
|
+
goal = args.goal or deep_get(config, "project.goal", "")
|
|
100
|
+
project_name = deep_get(config, "project.name", "") or root.name
|
|
101
|
+
max_versions = args.max_versions or int(deep_get(config, "project.max_versions", 5))
|
|
102
|
+
|
|
103
|
+
if not args.non_interactive:
|
|
104
|
+
print("[AutoDevLoop] Interactive setup. Press Enter to accept defaults.")
|
|
105
|
+
if not project_name:
|
|
106
|
+
project_name = _prompt("Project name", root.name)
|
|
107
|
+
if not goal:
|
|
108
|
+
goal = _prompt_multiline("Goal / user requirement")
|
|
109
|
+
max_versions = _prompt_int("Max versions", max_versions, minimum=1)
|
|
110
|
+
mode = _prompt("Mode (simple/advanced)", deep_get(config, "pipeline.mode", "advanced"))
|
|
111
|
+
config = deep_merge(config, {"pipeline": {"mode": mode}, "project": {"name": project_name, "max_versions": max_versions}})
|
|
112
|
+
|
|
113
|
+
if not goal and not (root / APP_DIR / STATE_FILE).exists():
|
|
114
|
+
raise SystemExit("A goal is required. Use --goal \"...\" or run interactively.")
|
|
115
|
+
|
|
116
|
+
# Persist resolved config so the web UI and resumes see the same settings.
|
|
117
|
+
save_config(root, deep_merge(config, {"project": {"name": project_name, "goal": goal, "max_versions": max_versions}}))
|
|
118
|
+
|
|
119
|
+
print(f"[AutoDevLoop] Stop with: autodevloop stop --project-dir {root}")
|
|
120
|
+
AutoDevLoop(root, config).run(
|
|
121
|
+
reset=args.reset, goal=goal, project_name=project_name, max_versions=max_versions,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_stop(args: argparse.Namespace) -> None:
|
|
126
|
+
root = resolve_project_dir(args.project_dir)
|
|
127
|
+
app_dir = root / APP_DIR
|
|
128
|
+
app_dir.mkdir(parents=True, exist_ok=True)
|
|
129
|
+
write_text(app_dir / STOP_FILE, f"stop requested at {now_text()}\n")
|
|
130
|
+
print(f"[AutoDevLoop] Stop requested: {app_dir / STOP_FILE}")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def cmd_status(args: argparse.Namespace) -> None:
|
|
134
|
+
root = resolve_project_dir(args.project_dir)
|
|
135
|
+
state = load_json(root / APP_DIR / STATE_FILE, {})
|
|
136
|
+
if not state:
|
|
137
|
+
print("[AutoDevLoop] No state found. Run `autodevloop run` first.")
|
|
138
|
+
return
|
|
139
|
+
versions = state.get("versions", [])
|
|
140
|
+
scores = [v.get("review_score", 0) for v in versions if isinstance(v.get("review_score", 0), int)]
|
|
141
|
+
cost = state.get("cost", {})
|
|
142
|
+
print()
|
|
143
|
+
print(f" Project : {state.get('project_name')}")
|
|
144
|
+
print(f" Status : {state.get('status')} (phase: {state.get('phase')})")
|
|
145
|
+
print(f" Version : v{state.get('current_version')} / {state.get('max_versions')}")
|
|
146
|
+
print(f" Goal : {state.get('goal_progress', 0)}% met"
|
|
147
|
+
+ (f" (completed at v{state.get('goal_completed_version')})" if state.get('goal_completed_version') else ""))
|
|
148
|
+
if scores:
|
|
149
|
+
print(f" Scores : {scores} (avg {round(sum(scores)/len(scores))})")
|
|
150
|
+
print(f" Cost : ${cost.get('cost_usd_total', 0):.4f} | "
|
|
151
|
+
f"in {cost.get('input_tokens', 0)} / out {cost.get('output_tokens', 0)} tokens")
|
|
152
|
+
print(f" Updated : {state.get('updated_at')}")
|
|
153
|
+
print()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def cmd_web(args: argparse.Namespace) -> None:
|
|
157
|
+
from .webapp import serve
|
|
158
|
+
serve(host=args.host, port=args.port)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def cmd_version(_args: argparse.Namespace) -> None:
|
|
162
|
+
print(f"AutoDevLoop {__version__}")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _add_project_dir(parser: argparse.ArgumentParser) -> None:
|
|
166
|
+
parser.add_argument("--project-dir", default=None, help="Project directory (default: current dir).")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
170
|
+
parser = argparse.ArgumentParser(prog="autodevloop", description="AI-driven autonomous development iteration loop.")
|
|
171
|
+
parser.add_argument("--version", action="version", version=f"AutoDevLoop {__version__}")
|
|
172
|
+
_add_project_dir(parser)
|
|
173
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
174
|
+
|
|
175
|
+
run = sub.add_parser("run", help="Start or resume an autonomous run.")
|
|
176
|
+
_add_project_dir(run)
|
|
177
|
+
run.add_argument("--project-name", default="")
|
|
178
|
+
run.add_argument("--goal", default="", help="User requirement / project goal.")
|
|
179
|
+
run.add_argument("--max-versions", type=int, default=0, help="Number of versions to generate.")
|
|
180
|
+
run.add_argument("--mode", choices=["simple", "advanced"], default="", help="Pipeline mode.")
|
|
181
|
+
run.add_argument("--arch-hint", default="", help="Optional architecture / stack hint for AgentARCH.")
|
|
182
|
+
run.add_argument("--provider", choices=["claude", "codex", "gemini"], default="", help="Provider profile.")
|
|
183
|
+
run.add_argument("--provider-command", default="", help="Override the CLI command (e.g. a wrapper).")
|
|
184
|
+
run.add_argument("--model", default="", help="Optional model alias/name passed to the provider.")
|
|
185
|
+
run.add_argument("--test-command", default="", help="Override test command (otherwise auto-detected).")
|
|
186
|
+
run.add_argument("--test-timeout", type=int, default=0)
|
|
187
|
+
run.add_argument("--agent-timeout", type=int, default=0)
|
|
188
|
+
run.add_argument("--review-threshold", type=int, default=0)
|
|
189
|
+
run.add_argument("--fix-retries", type=int, default=None)
|
|
190
|
+
run.add_argument("--max-parallel-agents", type=int, default=0)
|
|
191
|
+
run.add_argument("--no-parallel", action="store_true")
|
|
192
|
+
run.add_argument("--no-git", action="store_true", help="Disable git commits/tags in current/.")
|
|
193
|
+
run.add_argument("--reset", action="store_true", help="Start fresh (wipe .autodev, versions, current).")
|
|
194
|
+
run.add_argument("--non-interactive", action="store_true", help="Never prompt; fail if goal missing.")
|
|
195
|
+
run.set_defaults(func=cmd_run)
|
|
196
|
+
|
|
197
|
+
stop = sub.add_parser("stop", help="Request a running loop to stop.")
|
|
198
|
+
_add_project_dir(stop)
|
|
199
|
+
stop.set_defaults(func=cmd_stop)
|
|
200
|
+
|
|
201
|
+
status = sub.add_parser("status", help="Print run state summary.")
|
|
202
|
+
_add_project_dir(status)
|
|
203
|
+
status.set_defaults(func=cmd_status)
|
|
204
|
+
|
|
205
|
+
web = sub.add_parser("web", help="Launch the local web dashboard.")
|
|
206
|
+
web.add_argument("--host", default="127.0.0.1")
|
|
207
|
+
web.add_argument("--port", type=int, default=8787)
|
|
208
|
+
web.set_defaults(func=cmd_web)
|
|
209
|
+
|
|
210
|
+
ver = sub.add_parser("version", help="Print version.")
|
|
211
|
+
ver.set_defaults(func=cmd_version)
|
|
212
|
+
return parser
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _make_stdout_safe() -> None:
|
|
216
|
+
"""Avoid UnicodeEncodeError on legacy Windows code pages (cp932/gbk/etc.)."""
|
|
217
|
+
for stream in (sys.stdout, sys.stderr):
|
|
218
|
+
try:
|
|
219
|
+
stream.reconfigure(encoding="utf-8", errors="replace") # type: ignore[union-attr]
|
|
220
|
+
except (AttributeError, ValueError):
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def main(argv: list[str] | None = None) -> int:
|
|
225
|
+
_make_stdout_safe()
|
|
226
|
+
parser = build_parser()
|
|
227
|
+
args = parser.parse_args(argv)
|
|
228
|
+
args.func(args)
|
|
229
|
+
return 0
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
raise SystemExit(main())
|
autodevloop/config.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Configuration defaults, loading, merging, and persistence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from . import yaml_compat
|
|
10
|
+
from .util import CONFIG_FILE, read_text, write_text
|
|
11
|
+
|
|
12
|
+
DEFAULT_AGENT_TIMEOUT = 1800
|
|
13
|
+
DEFAULT_MAX_VERSIONS = 5
|
|
14
|
+
DEFAULT_REVIEW_THRESHOLD = 80
|
|
15
|
+
DEFAULT_FIX_RETRIES = 2
|
|
16
|
+
DEFAULT_MAX_PARALLEL_AGENTS = 3
|
|
17
|
+
DEFAULT_TEST_TIMEOUT = 120
|
|
18
|
+
DEFAULT_VALUE_THRESHOLD = 65
|
|
19
|
+
|
|
20
|
+
# Provider profiles. Only the CLI *command* (and optional args/model) differ;
|
|
21
|
+
# no API keys are ever requested. Users pre-configure their CLI locally.
|
|
22
|
+
PROVIDER_PROFILES: dict[str, dict[str, Any]] = {
|
|
23
|
+
"claude": {
|
|
24
|
+
"command": "claude",
|
|
25
|
+
"args": [
|
|
26
|
+
"--print", "--input-format", "text", "--output-format", "json",
|
|
27
|
+
"--no-session-persistence", "--permission-mode", "acceptEdits",
|
|
28
|
+
"--allowedTools", "Read,Write,Edit,MultiEdit,LS,Glob,Grep",
|
|
29
|
+
],
|
|
30
|
+
"model_flag": "--model",
|
|
31
|
+
"prompt_via": "stdin",
|
|
32
|
+
"output": "claude-json",
|
|
33
|
+
},
|
|
34
|
+
"codex": {
|
|
35
|
+
"command": "codex",
|
|
36
|
+
"args": ["exec", "--skip-git-repo-check", "--dangerously-bypass-approvals-and-sandbox"],
|
|
37
|
+
"model_flag": "--model",
|
|
38
|
+
"prompt_via": "stdin",
|
|
39
|
+
"output": "text",
|
|
40
|
+
},
|
|
41
|
+
"gemini": {
|
|
42
|
+
"command": "gemini",
|
|
43
|
+
"args": ["-y"],
|
|
44
|
+
"model_flag": "--model",
|
|
45
|
+
"prompt_via": "stdin",
|
|
46
|
+
"output": "text",
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Steps enabled per mode. Web/CLI may override individual steps.
|
|
51
|
+
SIMPLE_STEPS = {
|
|
52
|
+
"arch": True,
|
|
53
|
+
"goal_check": False, # folded into review in simple mode
|
|
54
|
+
"test_agent": False, # built-in test detection only (no extra LLM call)
|
|
55
|
+
"doc": False,
|
|
56
|
+
"scout": False, # no周边-feature scouting
|
|
57
|
+
"evaluate": False,
|
|
58
|
+
"features_doc": True,
|
|
59
|
+
}
|
|
60
|
+
ADVANCED_STEPS = {
|
|
61
|
+
"arch": True,
|
|
62
|
+
"goal_check": True,
|
|
63
|
+
"test_agent": True,
|
|
64
|
+
"doc": True,
|
|
65
|
+
"scout": True,
|
|
66
|
+
"evaluate": True,
|
|
67
|
+
"features_doc": True,
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def default_config() -> dict[str, Any]:
|
|
72
|
+
return {
|
|
73
|
+
"project": {
|
|
74
|
+
"name": "",
|
|
75
|
+
"goal": "",
|
|
76
|
+
"max_versions": DEFAULT_MAX_VERSIONS,
|
|
77
|
+
"arch_hint": "",
|
|
78
|
+
},
|
|
79
|
+
"provider": {
|
|
80
|
+
"name": "claude",
|
|
81
|
+
"command": "", # blank -> use profile command
|
|
82
|
+
"model": "",
|
|
83
|
+
"extra_args": [],
|
|
84
|
+
},
|
|
85
|
+
"pipeline": {
|
|
86
|
+
"mode": "advanced", # "simple" | "advanced"
|
|
87
|
+
"steps": {}, # per-step overrides on top of mode defaults
|
|
88
|
+
},
|
|
89
|
+
"agents": {
|
|
90
|
+
"timeout": DEFAULT_AGENT_TIMEOUT,
|
|
91
|
+
"allow_parallel": True,
|
|
92
|
+
"max_parallel": DEFAULT_MAX_PARALLEL_AGENTS,
|
|
93
|
+
"retries": 3,
|
|
94
|
+
"backoff_seconds": 5,
|
|
95
|
+
},
|
|
96
|
+
"review": {
|
|
97
|
+
"threshold": DEFAULT_REVIEW_THRESHOLD,
|
|
98
|
+
},
|
|
99
|
+
"value": {
|
|
100
|
+
"threshold": DEFAULT_VALUE_THRESHOLD,
|
|
101
|
+
},
|
|
102
|
+
"fix": {
|
|
103
|
+
"retries": DEFAULT_FIX_RETRIES,
|
|
104
|
+
},
|
|
105
|
+
"tests": {
|
|
106
|
+
"timeout": DEFAULT_TEST_TIMEOUT,
|
|
107
|
+
"command": "",
|
|
108
|
+
},
|
|
109
|
+
"vcs": {
|
|
110
|
+
"git": True,
|
|
111
|
+
},
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
|
116
|
+
result = copy.deepcopy(base)
|
|
117
|
+
for key, value in (override or {}).items():
|
|
118
|
+
if isinstance(value, dict) and isinstance(result.get(key), dict):
|
|
119
|
+
result[key] = deep_merge(result[key], value)
|
|
120
|
+
else:
|
|
121
|
+
result[key] = copy.deepcopy(value)
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def load_config(root: Path) -> dict[str, Any]:
|
|
126
|
+
raw = yaml_compat.load(read_text(root / CONFIG_FILE))
|
|
127
|
+
return deep_merge(default_config(), raw if isinstance(raw, dict) else {})
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def save_config(root: Path, config: dict[str, Any]) -> None:
|
|
131
|
+
merged = deep_merge(default_config(), config)
|
|
132
|
+
write_text(root / CONFIG_FILE, yaml_compat.dump(merged))
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def resolved_steps(config: dict[str, Any]) -> dict[str, bool]:
|
|
136
|
+
mode = str(deep_get(config, "pipeline.mode", "advanced")).lower()
|
|
137
|
+
base = ADVANCED_STEPS if mode == "advanced" else SIMPLE_STEPS
|
|
138
|
+
steps = dict(base)
|
|
139
|
+
overrides = deep_get(config, "pipeline.steps", {}) or {}
|
|
140
|
+
for key, value in overrides.items():
|
|
141
|
+
if key in steps and isinstance(value, bool):
|
|
142
|
+
steps[key] = value
|
|
143
|
+
return steps
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def provider_invocation(config: dict[str, Any]) -> dict[str, Any]:
|
|
147
|
+
name = str(deep_get(config, "provider.name", "claude")).lower()
|
|
148
|
+
profile = copy.deepcopy(PROVIDER_PROFILES.get(name, PROVIDER_PROFILES["claude"]))
|
|
149
|
+
command = deep_get(config, "provider.command", "") or profile["command"]
|
|
150
|
+
extra = deep_get(config, "provider.extra_args", []) or []
|
|
151
|
+
model = deep_get(config, "provider.model", "")
|
|
152
|
+
profile["command"] = command
|
|
153
|
+
profile["extra_args"] = list(extra)
|
|
154
|
+
profile["model"] = model
|
|
155
|
+
profile["name"] = name
|
|
156
|
+
return profile
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def deep_get(data: dict[str, Any], dotted: str, default: Any = None) -> Any:
|
|
160
|
+
current: Any = data
|
|
161
|
+
for part in dotted.split("."):
|
|
162
|
+
if not isinstance(current, dict) or part not in current:
|
|
163
|
+
return default
|
|
164
|
+
current = current[part]
|
|
165
|
+
return current
|