machineconfig 2.8__py3-none-any.whl → 2.94__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.
Potentially problematic release.
This version of machineconfig might be problematic. Click here for more details.
- machineconfig/cluster/sessions_managers/zellij_local_manager.py +4 -20
- machineconfig/jobs/python/python_ve_symlink.py +1 -1
- machineconfig/scripts/python/__pycache__/devops.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/devops_update_repos.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/fire_agents.cpython-313.pyc +0 -0
- machineconfig/scripts/python/__pycache__/repos_helper_update.cpython-313.pyc +0 -0
- machineconfig/scripts/python/ai/mcinit.py +2 -14
- machineconfig/scripts/python/devops.py +18 -64
- machineconfig/scripts/python/devops_add_identity.py +6 -2
- machineconfig/scripts/python/devops_add_ssh_key.py +5 -2
- machineconfig/scripts/python/devops_backup_retrieve.py +3 -15
- machineconfig/scripts/python/devops_devapps_install.py +8 -6
- machineconfig/scripts/python/devops_update_repos.py +122 -226
- machineconfig/scripts/python/fire_agents.py +111 -198
- machineconfig/scripts/python/fire_agents_help_launch.py +142 -0
- machineconfig/scripts/python/fire_agents_help_search.py +82 -0
- machineconfig/scripts/python/fire_agents_load_balancer.py +52 -0
- machineconfig/scripts/python/fire_jobs.py +2 -1
- machineconfig/scripts/python/helpers/cloud_helpers.py +2 -5
- machineconfig/scripts/python/repos.py +1 -1
- machineconfig/scripts/python/repos_helper_update.py +265 -0
- machineconfig/utils/installer_utils/installer_class.py +3 -3
- machineconfig/utils/notifications.py +24 -4
- machineconfig/utils/path.py +2 -1
- machineconfig/utils/procs.py +7 -7
- machineconfig/utils/schemas/fire_agents/fire_agents_input.py +70 -0
- machineconfig/utils/schemas/layouts/layout_types.py +0 -1
- machineconfig/utils/source_of_truth.py +2 -0
- machineconfig/utils/ve.py +9 -5
- {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/METADATA +7 -10
- {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/RECORD +34 -27
- {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/WHEEL +0 -0
- {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/entry_points.txt +0 -0
- {machineconfig-2.8.dist-info → machineconfig-2.94.dist-info}/top_level.txt +0 -0
|
@@ -7,63 +7,19 @@ Improved design notes:
|
|
|
7
7
|
* Preserves original core behavior & command generation for each agent type.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
10
|
from pathlib import Path
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
from typing import cast, get_args, Iterable, TypeAlias, Literal
|
|
12
|
+
import json
|
|
13
|
+
import sys
|
|
16
14
|
|
|
15
|
+
from machineconfig.scripts.python.fire_agents_help_launch import prep_agent_launch, get_agents_launch_layout, AGENTS
|
|
16
|
+
from machineconfig.scripts.python.fire_agents_help_search import search_files_by_pattern, search_python_files
|
|
17
|
+
from machineconfig.scripts.python.fire_agents_load_balancer import chunk_prompts, SPLITTING_STRATEGY, DEFAULT_AGENT_CAP
|
|
17
18
|
from machineconfig.cluster.sessions_managers.zellij_local_manager import ZellijLocalManager
|
|
18
|
-
from machineconfig.utils.
|
|
19
|
-
from machineconfig.utils.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
AGENTS: TypeAlias = Literal[
|
|
24
|
-
"cursor-agent", "gemini", "crush", "q", "onlyPrepPromptFiles"
|
|
25
|
-
# warp terminal
|
|
26
|
-
]
|
|
27
|
-
DEFAULT_AGENT_CAP = 6
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def get_gemini_api_keys() -> list[str]:
|
|
31
|
-
from machineconfig.utils.utils2 import read_ini
|
|
32
|
-
|
|
33
|
-
config = read_ini(Path.home().joinpath("dotfiles/creds/llm/gemini/api_keys.ini"))
|
|
34
|
-
res: list[str] = []
|
|
35
|
-
for a_section_name in list(config.sections()):
|
|
36
|
-
a_section = config[a_section_name]
|
|
37
|
-
if "api_key" in a_section:
|
|
38
|
-
api_key = a_section["api_key"].strip()
|
|
39
|
-
if api_key:
|
|
40
|
-
res.append(api_key)
|
|
41
|
-
# res = [v for k, v in config.items("api_keys") if k.startswith("key") and v.strip() != ""]
|
|
42
|
-
print(f"Found {len(res)} Gemini API keys configured.")
|
|
43
|
-
return res
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def _search_python_files(repo_root: Path, keyword: str) -> list[Path]:
|
|
47
|
-
"""Return all Python files under repo_root whose text contains keyword.
|
|
48
|
-
|
|
49
|
-
Notes:
|
|
50
|
-
- Skips any paths that reside under a directory named ".venv" at any depth.
|
|
51
|
-
- Errors reading individual files are ignored (decoded with 'ignore').
|
|
52
|
-
"""
|
|
53
|
-
py_files = list(repo_root.rglob("*.py"))
|
|
54
|
-
keyword_lower = keyword.lower()
|
|
55
|
-
matches: list[Path] = []
|
|
56
|
-
for f in py_files:
|
|
57
|
-
# Skip anything under a .venv directory anywhere in the path
|
|
58
|
-
if any(part == ".venv" for part in f.parts):
|
|
59
|
-
continue
|
|
60
|
-
try:
|
|
61
|
-
if keyword_lower in f.read_text(encoding="utf-8", errors="ignore").lower():
|
|
62
|
-
matches.append(f)
|
|
63
|
-
except OSError:
|
|
64
|
-
# Skip unreadable file
|
|
65
|
-
continue
|
|
66
|
-
return matches
|
|
19
|
+
from machineconfig.utils.options import choose_one_option
|
|
20
|
+
from machineconfig.utils.ve import get_repo_root
|
|
21
|
+
|
|
22
|
+
SEARCH_STRATEGIES: TypeAlias = Literal["file_path", "keyword_search", "filename_pattern"]
|
|
67
23
|
|
|
68
24
|
|
|
69
25
|
def _write_list_file(target: Path, files: Iterable[Path]) -> None:
|
|
@@ -71,164 +27,121 @@ def _write_list_file(target: Path, files: Iterable[Path]) -> None:
|
|
|
71
27
|
target.write_text("\n".join(str(f) for f in files), encoding="utf-8")
|
|
72
28
|
|
|
73
29
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _confirm(message: str, default_no: bool = True) -> bool:
|
|
87
|
-
suffix = "[y/N]" if default_no else "[Y/n]"
|
|
88
|
-
answer = input(f"{message} {suffix} ").strip().lower()
|
|
89
|
-
if answer in {"y", "yes"}:
|
|
90
|
-
return True
|
|
91
|
-
if not default_no and answer == "":
|
|
92
|
-
return True
|
|
93
|
-
return False
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def launch_agents(repo_root: Path, prompts: list[str], agent: AGENTS, *, max_agents: int = DEFAULT_AGENT_CAP) -> list[TabConfig]:
|
|
97
|
-
"""Create tab configuration for a set of agent prompts.
|
|
98
|
-
|
|
99
|
-
If number of prompts exceeds max_agents, ask user for confirmation.
|
|
100
|
-
(Original behavior raised an error; now interactive override.)
|
|
101
|
-
"""
|
|
102
|
-
if not prompts:
|
|
103
|
-
raise ValueError("No prompts provided")
|
|
104
|
-
|
|
105
|
-
if len(prompts) > max_agents:
|
|
106
|
-
proceed = _confirm(message=(f"You are about to launch {len(prompts)} agents which exceeds the cap ({max_agents}). Proceed?"))
|
|
107
|
-
if not proceed:
|
|
108
|
-
print("Aborting per user choice.")
|
|
109
|
-
return []
|
|
110
|
-
|
|
111
|
-
tab_config: list[TabConfig] = []
|
|
112
|
-
tmp_dir = repo_root / ".ai" / f"tmp_prompts/{randstr()}"
|
|
113
|
-
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
114
|
-
|
|
115
|
-
for idx, a_prompt in enumerate(prompts):
|
|
116
|
-
prompt_path = tmp_dir / f"agent{idx}_prompt.txt"
|
|
117
|
-
prompt_path.write_text(a_prompt, encoding="utf-8")
|
|
118
|
-
cmd_path = tmp_dir / f"agent{idx}_cmd.sh"
|
|
119
|
-
match agent:
|
|
120
|
-
case "gemini":
|
|
121
|
-
# model = "gemini-2.5-pro"
|
|
122
|
-
# model = "gemini-2.5-flash-lite"
|
|
123
|
-
model = None # auto-select
|
|
124
|
-
if model is None:
|
|
125
|
-
model_arg = ""
|
|
126
|
-
else:
|
|
127
|
-
model_arg = f"--model {shlex.quote(model)}"
|
|
128
|
-
# Need a real shell for the pipeline; otherwise '| gemini ...' is passed as args to 'cat'
|
|
129
|
-
safe_path = shlex.quote(str(prompt_path))
|
|
130
|
-
api_keys = get_gemini_api_keys()
|
|
131
|
-
api_key = api_keys[idx % len(api_keys)] if api_keys else ""
|
|
132
|
-
# Export the environment variable so it's available to subshells
|
|
133
|
-
cmd = f"""
|
|
134
|
-
export GEMINI_API_KEY={shlex.quote(api_key)}
|
|
135
|
-
echo "Using Gemini API key $GEMINI_API_KEY"
|
|
136
|
-
cat {prompt_path}
|
|
137
|
-
GEMINI_API_KEY={shlex.quote(api_key)} bash -lc 'cat {safe_path} | gemini {model_arg} --yolo --prompt'
|
|
138
|
-
"""
|
|
139
|
-
case "cursor-agent":
|
|
140
|
-
# As originally implemented
|
|
141
|
-
cmd = f"""
|
|
142
|
-
|
|
143
|
-
cursor-agent --print --output-format text < {prompt_path}
|
|
144
|
-
|
|
145
|
-
"""
|
|
146
|
-
case "crush":
|
|
147
|
-
cmd = f"""
|
|
148
|
-
# cat {prompt_path} | crush run
|
|
149
|
-
crush run {prompt_path}
|
|
150
|
-
"""
|
|
151
|
-
case "q":
|
|
152
|
-
cmd = f"""
|
|
153
|
-
q chat --no-interactive --trust-all-tools {prompt_path}
|
|
154
|
-
"""
|
|
155
|
-
case "onlyPrepPromptFiles":
|
|
156
|
-
cmd = f"""
|
|
157
|
-
echo "Prepared prompt file at {prompt_path}"
|
|
158
|
-
"""
|
|
159
|
-
case _:
|
|
160
|
-
raise ValueError(f"Unsupported agent type: {agent}")
|
|
161
|
-
random_sleep_time = random.uniform(0, 5)
|
|
162
|
-
cmd_prefix = f"""
|
|
163
|
-
echo "Sleeping for {random_sleep_time:.2f} seconds to stagger agent startups..."
|
|
164
|
-
sleep {random_sleep_time:.2f}
|
|
165
|
-
echo "Launching `{agent}` with prompt from {prompt_path}"
|
|
166
|
-
echo "Launching `{agent}` with command from {cmd_path}"
|
|
167
|
-
echo "--------START OF AGENT OUTPUT--------"
|
|
168
|
-
sleep 0.1
|
|
169
|
-
"""
|
|
170
|
-
cmd_postfix = """
|
|
171
|
-
sleep 0.1
|
|
172
|
-
echo "---------END OF AGENT OUTPUT---------"
|
|
173
|
-
"""
|
|
174
|
-
cmd_path.write_text(cmd_prefix + cmd + cmd_postfix, encoding="utf-8")
|
|
175
|
-
fire_cmd = f"bash {shlex.quote(str(cmd_path))}"
|
|
176
|
-
tab_config.append(TabConfig(tabName=f"Agent{idx}", startDir=str(repo_root), command=fire_cmd))
|
|
177
|
-
|
|
178
|
-
print(f"Launching a template with #{len(tab_config)} agents")
|
|
179
|
-
return tab_config
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
def main(): # noqa: C901 - (complexity acceptable for CLI glue)
|
|
183
|
-
repo_root = Path.cwd()
|
|
184
|
-
print(f"Operating @ {repo_root}")
|
|
185
|
-
|
|
186
|
-
file_path_input = input("Enter path to target file [press Enter to generate it from searching]: ").strip()
|
|
187
|
-
if file_path_input == "":
|
|
30
|
+
def get_prompt_material(search_strategy: SEARCH_STRATEGIES, repo_root: Path) -> tuple[Path, str]:
|
|
31
|
+
if search_strategy == "file_path":
|
|
32
|
+
file_path_input = input("Enter path to target file: ").strip()
|
|
33
|
+
if not file_path_input:
|
|
34
|
+
print("No file path provided. Exiting.")
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
target_file_path = Path(file_path_input).expanduser().resolve()
|
|
37
|
+
if not target_file_path.exists() or not target_file_path.is_file():
|
|
38
|
+
print(f"Invalid file path: {target_file_path}")
|
|
39
|
+
sys.exit(1)
|
|
40
|
+
separator = input("Enter separator [\\n]: ").strip() or "\n"
|
|
41
|
+
elif search_strategy == "keyword_search":
|
|
188
42
|
keyword = input("Enter keyword to search recursively for all .py files containing it: ").strip()
|
|
189
43
|
if not keyword:
|
|
190
44
|
print("No keyword supplied. Exiting.")
|
|
191
|
-
|
|
192
|
-
matching_files =
|
|
45
|
+
sys.exit(1)
|
|
46
|
+
matching_files = search_python_files(repo_root, keyword)
|
|
193
47
|
if not matching_files:
|
|
194
48
|
print(f"💥 No .py files found containing keyword: {keyword}")
|
|
195
|
-
|
|
49
|
+
sys.exit(1)
|
|
196
50
|
for idx, mf in enumerate(matching_files):
|
|
197
51
|
print(f"{idx:>3}: {mf}")
|
|
198
52
|
print(f"\nFound {len(matching_files)} .py files containing keyword: {keyword}")
|
|
199
|
-
|
|
200
|
-
_write_list_file(
|
|
53
|
+
target_file_path = repo_root / ".ai" / "target_file.txt"
|
|
54
|
+
_write_list_file(target_file_path, matching_files)
|
|
55
|
+
separator = "\n"
|
|
56
|
+
elif search_strategy == "filename_pattern":
|
|
57
|
+
pattern = input("Enter filename pattern (e.g., '*.py', '*test*', 'config.*'): ").strip()
|
|
58
|
+
if not pattern:
|
|
59
|
+
print("No pattern supplied. Exiting.")
|
|
60
|
+
sys.exit(1)
|
|
61
|
+
matching_files = search_files_by_pattern(repo_root, pattern)
|
|
62
|
+
if not matching_files:
|
|
63
|
+
print(f"💥 No files found matching pattern: {pattern}")
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
for idx, mf in enumerate(matching_files):
|
|
66
|
+
print(f"{idx:>3}: {mf}")
|
|
67
|
+
print(f"\nFound {len(matching_files)} files matching pattern: {pattern}")
|
|
68
|
+
target_file_path = repo_root / ".ai" / "target_file.txt"
|
|
69
|
+
_write_list_file(target_file_path, matching_files)
|
|
201
70
|
separator = "\n"
|
|
202
|
-
source_text = target_list_file.read_text(encoding="utf-8", errors="ignore")
|
|
203
71
|
else:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
print(f"Invalid file path: {target_file_path}")
|
|
207
|
-
return
|
|
208
|
-
separator = input("Enter separator [\\n]: ").strip() or "\n"
|
|
209
|
-
if not target_file_path.exists():
|
|
210
|
-
print(f"File does not exist: {target_file_path}")
|
|
211
|
-
return
|
|
212
|
-
source_text = target_file_path.read_text(encoding="utf-8", errors="ignore")
|
|
213
|
-
|
|
214
|
-
raw_prompts = source_text.split(separator)
|
|
215
|
-
print(f"Loaded {len(raw_prompts)} raw prompts from source.")
|
|
216
|
-
prefix = input("Enter prefix prompt: ")
|
|
217
|
-
combined_prompts = _chunk_prompts(raw_prompts, DEFAULT_AGENT_CAP)
|
|
218
|
-
combined_prompts = [prefix + "\n" + p for p in combined_prompts]
|
|
72
|
+
raise ValueError(f"Unknown search strategy: {search_strategy}")
|
|
73
|
+
return target_file_path, separator
|
|
219
74
|
|
|
220
|
-
from machineconfig.utils.options import choose_one_option
|
|
221
75
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
76
|
+
def main(): # noqa: C901 - (complexity acceptable for CLI glue)
|
|
77
|
+
repo_root = get_repo_root(Path.cwd())
|
|
78
|
+
if repo_root is None:
|
|
79
|
+
print("💥 Could not determine the repository root. Please run this script from within a git repository.")
|
|
80
|
+
sys.exit(1)
|
|
81
|
+
print(f"Operating @ {repo_root}")
|
|
227
82
|
|
|
228
|
-
|
|
83
|
+
search_strategy = cast(SEARCH_STRATEGIES, choose_one_option(header="Choose search strategy:", options=get_args(SEARCH_STRATEGIES)))
|
|
84
|
+
splitting_strategy = cast(SPLITTING_STRATEGY, choose_one_option(header="Choose prompt splitting strategy:", options=get_args(SPLITTING_STRATEGY)))
|
|
85
|
+
agent_selected = cast(AGENTS, choose_one_option(header="Select agent type", options=get_args(AGENTS)))
|
|
86
|
+
print("Enter prefix prompt (end with Ctrl-D / Ctrl-Z):")
|
|
87
|
+
prompt_prefix = "\n".join(sys.stdin.readlines())
|
|
88
|
+
job_name = input("Enter job name [AI_AGENTS]: ") or "AI_Agents"
|
|
89
|
+
keep_material_in_separate_file_input = input("Keep prompt material in separate file? [y/N]: ").strip().lower() == "y"
|
|
90
|
+
|
|
91
|
+
prompt_material_path, separator = get_prompt_material(search_strategy=search_strategy, repo_root=repo_root)
|
|
92
|
+
match splitting_strategy:
|
|
93
|
+
case "agent_cap":
|
|
94
|
+
agent_cap_input = input(f"Enter maximum number of agents/splits [default: {DEFAULT_AGENT_CAP}]: ").strip()
|
|
95
|
+
agent_cap = int(agent_cap_input) if agent_cap_input else DEFAULT_AGENT_CAP
|
|
96
|
+
task_rows = None
|
|
97
|
+
case "task_rows":
|
|
98
|
+
task_rows_input: str = input("Enter number of rows/tasks per agent [13]: ").strip() or "13"
|
|
99
|
+
task_rows = int(task_rows_input)
|
|
100
|
+
agent_cap = None
|
|
101
|
+
prompt_material_re_splitted = chunk_prompts(prompt_material_path, splitting_strategy, agent_cap=agent_cap, task_rows=task_rows, joiner=separator)
|
|
102
|
+
|
|
103
|
+
agents_dir = prep_agent_launch(repo_root=repo_root, prompts_material=prompt_material_re_splitted, keep_material_in_separate_file=keep_material_in_separate_file_input, prompt_prefix=prompt_prefix, agent=agent_selected, job_name=job_name)
|
|
104
|
+
layoutfile = get_agents_launch_layout(session_root=agents_dir)
|
|
105
|
+
|
|
106
|
+
regenerate_py_code = f"""
|
|
107
|
+
#!/usr/bin/env uv run --python 3.13 --with machineconfig
|
|
108
|
+
#!/usr/bin/env uv run --project $HOME/code/machineconfig
|
|
109
|
+
|
|
110
|
+
from machineconfig.scripts.python.fire_agents import *
|
|
111
|
+
|
|
112
|
+
repo_root = Path("{repo_root}")
|
|
113
|
+
search_strategy = "{search_strategy}"
|
|
114
|
+
splitting_strategy = "{splitting_strategy}"
|
|
115
|
+
agent_selected = "{agent_selected}"
|
|
116
|
+
prompt_prefix = '''{prompt_prefix}'''
|
|
117
|
+
job_name = "{job_name}"
|
|
118
|
+
keep_material_in_separate_file_input = {keep_material_in_separate_file_input}
|
|
119
|
+
separator = "{separator}"
|
|
120
|
+
prompt_material_path = Path("{prompt_material_path}")
|
|
121
|
+
agent_cap = {agent_cap}
|
|
122
|
+
task_rows = {task_rows}
|
|
123
|
+
|
|
124
|
+
prompt_material_re_splitted = chunk_prompts(prompt_material_path, splitting_strategy, agent_cap=agent_cap, task_rows=task_rows, joiner=separator)
|
|
125
|
+
agents_dir = prep_agent_launch(repo_root=repo_root, prompts_material=prompt_material_re_splitted, keep_material_in_separate_file=keep_material_in_separate_file_input, prompt_prefix=prompt_prefix, agent=agent_selected, job_name=job_name)
|
|
126
|
+
layout = get_agents_launch_layout(session_root=agents_dir)
|
|
127
|
+
|
|
128
|
+
(agents_dir / "aa_agents_relaunch.py").write_text(data=regenerate_py_code, encoding="utf-8")
|
|
129
|
+
(agents_dir / "layout.json").write_text(data=json.dumps(layout, indent=2), encoding="utf-8")
|
|
130
|
+
|
|
131
|
+
if len(layout["layoutTabs"]) > 25:
|
|
132
|
+
print("Too many agents (>25) to launch. Skipping launch.")
|
|
133
|
+
sys.exit(0)
|
|
134
|
+
manager = ZellijLocalManager(session_layouts=[layout])
|
|
135
|
+
manager.start_all_sessions()
|
|
136
|
+
manager.run_monitoring_routine()
|
|
229
137
|
|
|
230
|
-
|
|
231
|
-
|
|
138
|
+
"""
|
|
139
|
+
(agents_dir / "aa_agents_relaunch.py").write_text(data=regenerate_py_code, encoding="utf-8")
|
|
140
|
+
(agents_dir / "layout.json").write_text(data=json.dumps(layoutfile, indent=2), encoding="utf-8")
|
|
141
|
+
if len(layoutfile["layouts"][0]["layoutTabs"]) > 25:
|
|
142
|
+
print("Too many agents (>25) to launch. Skipping launch.")
|
|
143
|
+
sys.exit(0)
|
|
144
|
+
manager = ZellijLocalManager(session_layouts=layoutfile["layouts"])
|
|
232
145
|
manager.start_all_sessions()
|
|
233
146
|
manager.run_monitoring_routine()
|
|
234
147
|
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
|
|
2
|
+
from machineconfig.utils.utils2 import randstr
|
|
3
|
+
|
|
4
|
+
import random
|
|
5
|
+
import shlex
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal, TypeAlias
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
AGENTS: TypeAlias = Literal[
|
|
11
|
+
"cursor-agent", "gemini", "crush", "q", "onlyPrepPromptFiles"
|
|
12
|
+
# warp terminal
|
|
13
|
+
]
|
|
14
|
+
AGENT_NAME_FORMATTER = "agent_{idx}_cmd.sh" # e.g., agent_0_cmd.sh
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_gemini_api_keys() -> list[str]:
|
|
18
|
+
from machineconfig.utils.utils2 import read_ini
|
|
19
|
+
config = read_ini(Path.home().joinpath("dotfiles/creds/llm/gemini/api_keys.ini"))
|
|
20
|
+
res: list[str] = []
|
|
21
|
+
for a_section_name in list(config.sections()):
|
|
22
|
+
a_section = config[a_section_name]
|
|
23
|
+
if "api_key" in a_section:
|
|
24
|
+
api_key = a_section["api_key"].strip()
|
|
25
|
+
if api_key:
|
|
26
|
+
res.append(api_key)
|
|
27
|
+
print(f"Found {len(res)} Gemini API keys configured.")
|
|
28
|
+
return res
|
|
29
|
+
|
|
30
|
+
def prep_agent_launch(repo_root: Path, prompts_material: list[str], prompt_prefix: str, keep_material_in_separate_file: bool, agent: AGENTS, *, job_name: str) -> Path:
|
|
31
|
+
session_root = repo_root / ".ai" / f"tmp_prompts/{job_name}_{randstr()}"
|
|
32
|
+
session_root.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
prompt_folder = session_root / "prompts"
|
|
34
|
+
prompt_folder.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
|
|
36
|
+
all_materials_scripts: list[Path] = []
|
|
37
|
+
for idx, a_prompt_material in enumerate(prompts_material):
|
|
38
|
+
prompt_root = prompt_folder / f"agent_{idx}"
|
|
39
|
+
prompt_root.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
prompt_path = prompt_root / f"agent_{idx}_prompt.txt"
|
|
41
|
+
if keep_material_in_separate_file:
|
|
42
|
+
prompt_material_path = prompt_root / f"agent_{idx}_material.txt"
|
|
43
|
+
prompt_material_path.write_text(a_prompt_material, encoding="utf-8")
|
|
44
|
+
prompt_path.write_text(prompt_prefix + f"""\nPlease only look @ {prompt_material_path}. You don't need to do any other work beside the content of this material file.""", encoding="utf-8")
|
|
45
|
+
all_materials_scripts.append(prompt_material_path)
|
|
46
|
+
else:
|
|
47
|
+
prompt_material_path = prompt_path
|
|
48
|
+
prompt_path.write_text(prompt_prefix + """\nPlease only look @ the following:\n""" + a_prompt_material, encoding="utf-8")
|
|
49
|
+
|
|
50
|
+
agent_cmd_launch_path = prompt_root / AGENT_NAME_FORMATTER.format(idx=idx) # e.g., agent_0_cmd.sh
|
|
51
|
+
random_sleep_time = random.uniform(0, 5)
|
|
52
|
+
cmd_prefix = f"""
|
|
53
|
+
#!/usr/bin/env bash
|
|
54
|
+
|
|
55
|
+
# AGENT-{idx}-LAUNCH-SCRIPT
|
|
56
|
+
# Auto-generated by fire_agents.py
|
|
57
|
+
|
|
58
|
+
export FIRE_AGENTS_AGENT_NAME="{agent}"
|
|
59
|
+
export FIRE_AGENTS_JOB_NAME="{job_name}"
|
|
60
|
+
export FIRE_AGENTS_PROMPT_FILE="{prompt_path}"
|
|
61
|
+
export FIRE_AGENTS_MATERIAL_FILE="{prompt_material_path}"
|
|
62
|
+
export FIRE_AGENTS_AGENT_LAUNCHER="{agent_cmd_launch_path}"
|
|
63
|
+
|
|
64
|
+
echo "Sleeping for {random_sleep_time:.2f} seconds to stagger agent startups..."
|
|
65
|
+
sleep {random_sleep_time:.2f}
|
|
66
|
+
echo "Launching agent {agent} with prompt from {prompt_path}"
|
|
67
|
+
echo "Launching agent {agent} with command from {agent_cmd_launch_path}"
|
|
68
|
+
echo "--------START OF AGENT OUTPUT--------"
|
|
69
|
+
sleep 0.1
|
|
70
|
+
|
|
71
|
+
"""
|
|
72
|
+
match agent:
|
|
73
|
+
case "gemini":
|
|
74
|
+
model = "gemini-2.5-pro"
|
|
75
|
+
# model = "gemini-2.5-flash-lite"
|
|
76
|
+
# model = None # auto-select
|
|
77
|
+
if model is None:
|
|
78
|
+
model_arg = ""
|
|
79
|
+
else:
|
|
80
|
+
model_arg = f"--model {shlex.quote(model)}"
|
|
81
|
+
# Need a real shell for the pipeline; otherwise '| gemini ...' is passed as args to 'cat'
|
|
82
|
+
safe_path = shlex.quote(str(prompt_path))
|
|
83
|
+
api_keys = get_gemini_api_keys()
|
|
84
|
+
api_key = api_keys[idx % len(api_keys)] if api_keys else ""
|
|
85
|
+
# Export the environment variable so it's available to subshells
|
|
86
|
+
cmd = f"""
|
|
87
|
+
export GEMINI_API_KEY={shlex.quote(api_key)}
|
|
88
|
+
echo "Using Gemini API key $GEMINI_API_KEY"
|
|
89
|
+
bash -lc 'cat {safe_path} | gemini {model_arg} --yolo --prompt'
|
|
90
|
+
"""
|
|
91
|
+
case "cursor-agent":
|
|
92
|
+
# As originally implemented
|
|
93
|
+
cmd = f"""
|
|
94
|
+
|
|
95
|
+
cursor-agent --print --output-format text < {prompt_path}
|
|
96
|
+
|
|
97
|
+
"""
|
|
98
|
+
case "crush":
|
|
99
|
+
cmd = f"""
|
|
100
|
+
crush run {prompt_path}
|
|
101
|
+
"""
|
|
102
|
+
case "q":
|
|
103
|
+
cmd = f"""
|
|
104
|
+
q chat --no-interactive --trust-all-tools {prompt_path}
|
|
105
|
+
"""
|
|
106
|
+
case "onlyPrepPromptFiles":
|
|
107
|
+
cmd = f"""
|
|
108
|
+
echo "Prepared prompt file at {prompt_path}"
|
|
109
|
+
"""
|
|
110
|
+
case _:
|
|
111
|
+
raise ValueError(f"Unsupported agent type: {agent}")
|
|
112
|
+
cmd_postfix = """
|
|
113
|
+
sleep 0.1
|
|
114
|
+
echo "---------END OF AGENT OUTPUT---------"
|
|
115
|
+
"""
|
|
116
|
+
agent_cmd_launch_path.write_text(cmd_prefix + cmd + cmd_postfix, encoding="utf-8")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# print(f"Launching a template with #{len(tab_config)} agents")
|
|
120
|
+
if len(all_materials_scripts) > 0:
|
|
121
|
+
all_materials_list_path = session_root / "all_materials_redistributed.txt"
|
|
122
|
+
all_materials_list_path.write_text("\n".join(str(p) for p in all_materials_scripts), encoding="utf-8")
|
|
123
|
+
print(f"All prompt materials listed @ {all_materials_list_path}")
|
|
124
|
+
return session_root
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def get_agents_launch_layout(session_root: Path):
|
|
128
|
+
from machineconfig.utils.schemas.layouts.layout_types import TabConfig, LayoutConfig, LayoutsFile
|
|
129
|
+
tab_config: list[TabConfig] = []
|
|
130
|
+
prompt_root = session_root / "prompts"
|
|
131
|
+
all_dirs_under_prompts = [d for d in prompt_root.iterdir() if d.is_dir()]
|
|
132
|
+
launch_agents_squentially = ""
|
|
133
|
+
for a_prompt_dir in all_dirs_under_prompts:
|
|
134
|
+
idx = a_prompt_dir.name.split("_")[-1] # e.g., agent_0 -> 0
|
|
135
|
+
agent_cmd_path = a_prompt_dir / AGENT_NAME_FORMATTER.format(idx=idx)
|
|
136
|
+
fire_cmd = f"bash {shlex.quote(str(agent_cmd_path))}"
|
|
137
|
+
tab_config.append(TabConfig(tabName=f"Agent{idx}", startDir=str(session_root.parent.parent.parent), command=fire_cmd))
|
|
138
|
+
launch_agents_squentially += f". {shlex.quote(str(agent_cmd_path))}\n"
|
|
139
|
+
layout = LayoutConfig(layoutName="Agents", layoutTabs=tab_config)
|
|
140
|
+
(session_root / "launch_all_agents_sequentially.sh").write_text(launch_agents_squentially, encoding="utf-8")
|
|
141
|
+
layouts_file: LayoutsFile = LayoutsFile(version="1.0", layouts=[layout])
|
|
142
|
+
return layouts_file
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
|
|
2
|
+
from machineconfig.utils.source_of_truth import EXCLUDE_DIRS
|
|
3
|
+
import fnmatch
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def search_files_by_pattern(repo_root: Path, pattern: str) -> list[Path]:
|
|
8
|
+
"""Return all files under repo_root whose filename matches the given pattern.
|
|
9
|
+
|
|
10
|
+
Notes:
|
|
11
|
+
- Uses glob-style pattern matching (e.g., "*.py", "*test*", "config.*")
|
|
12
|
+
- Skips any paths that reside under directories listed in EXCLUDE_DIRS at any depth.
|
|
13
|
+
"""
|
|
14
|
+
matches: list[Path] = []
|
|
15
|
+
|
|
16
|
+
def _should_skip_dir(dir_path: Path) -> bool:
|
|
17
|
+
"""Check if directory should be skipped based on EXCLUDE_DIRS."""
|
|
18
|
+
return any(part in EXCLUDE_DIRS for part in dir_path.parts)
|
|
19
|
+
|
|
20
|
+
def _walk_and_filter(current_path: Path) -> None:
|
|
21
|
+
"""Recursively walk directories, filtering out excluded ones early."""
|
|
22
|
+
try:
|
|
23
|
+
if not current_path.is_dir():
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
# Skip if current directory is in exclude list
|
|
27
|
+
if _should_skip_dir(current_path):
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
for item in current_path.iterdir():
|
|
31
|
+
if item.is_dir():
|
|
32
|
+
_walk_and_filter(item)
|
|
33
|
+
elif item.is_file() and fnmatch.fnmatch(item.name, pattern):
|
|
34
|
+
matches.append(item)
|
|
35
|
+
except (OSError, PermissionError):
|
|
36
|
+
# Skip directories we can't read
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
_walk_and_filter(repo_root)
|
|
40
|
+
return matches
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def search_python_files(repo_root: Path, keyword: str) -> list[Path]:
|
|
44
|
+
"""Return all Python files under repo_root whose text contains keyword.
|
|
45
|
+
|
|
46
|
+
Notes:
|
|
47
|
+
- Skips any paths that reside under directories listed in EXCLUDE_DIRS at any depth.
|
|
48
|
+
- Errors reading individual files are ignored (decoded with 'ignore').
|
|
49
|
+
"""
|
|
50
|
+
keyword_lower = keyword.lower()
|
|
51
|
+
matches: list[Path] = []
|
|
52
|
+
|
|
53
|
+
def _should_skip_dir(dir_path: Path) -> bool:
|
|
54
|
+
"""Check if directory should be skipped based on EXCLUDE_DIRS."""
|
|
55
|
+
return any(part in EXCLUDE_DIRS for part in dir_path.parts)
|
|
56
|
+
|
|
57
|
+
def _walk_and_filter(current_path: Path) -> None:
|
|
58
|
+
"""Recursively walk directories, filtering out excluded ones early."""
|
|
59
|
+
try:
|
|
60
|
+
if not current_path.is_dir():
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
# Skip if current directory is in exclude list
|
|
64
|
+
if _should_skip_dir(current_path):
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
for item in current_path.iterdir():
|
|
68
|
+
if item.is_dir():
|
|
69
|
+
_walk_and_filter(item)
|
|
70
|
+
elif item.is_file() and item.suffix == ".py":
|
|
71
|
+
try:
|
|
72
|
+
if keyword_lower in item.read_text(encoding="utf-8", errors="ignore").lower():
|
|
73
|
+
matches.append(item)
|
|
74
|
+
except OSError:
|
|
75
|
+
# Skip unreadable file
|
|
76
|
+
continue
|
|
77
|
+
except (OSError, PermissionError):
|
|
78
|
+
# Skip directories we can't read
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
_walk_and_filter(repo_root)
|
|
82
|
+
return matches
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from typing import Literal, TypeAlias
|
|
4
|
+
from math import ceil
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
SPLITTING_STRATEGY: TypeAlias = Literal[
|
|
9
|
+
"agent_cap", # User decides number of agents, rows/tasks determined automatically
|
|
10
|
+
"task_rows" # User decides number of rows/tasks, number of agents determined automatically
|
|
11
|
+
]
|
|
12
|
+
DEFAULT_AGENT_CAP = 6
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def chunk_prompts(prompt_material_path: Path, strategy: SPLITTING_STRATEGY, joiner: str, *, agent_cap: int | None, task_rows: int | None) -> list[str]:
|
|
16
|
+
"""Chunk prompts based on splitting strategy.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
prompts: List of prompts to chunk
|
|
20
|
+
strategy: Either 'agent_cap' or 'task_rows'
|
|
21
|
+
agent_cap: Maximum number of agents (used with 'agent_cap' strategy)
|
|
22
|
+
task_rows: Number of rows/tasks per agent (used with 'task_rows' strategy)
|
|
23
|
+
"""
|
|
24
|
+
prompts = [p for p in prompt_material_path.read_text(encoding="utf-8", errors="ignore").split(joiner) if p.strip() != ""] # drop blank entries
|
|
25
|
+
|
|
26
|
+
if strategy == "agent_cap":
|
|
27
|
+
if agent_cap is None:
|
|
28
|
+
raise ValueError("agent_cap must be provided when using 'agent_cap' strategy")
|
|
29
|
+
|
|
30
|
+
if len(prompts) <= agent_cap:
|
|
31
|
+
return prompts
|
|
32
|
+
|
|
33
|
+
print(f"Chunking {len(prompts)} prompts into groups for up to {agent_cap} agents because it exceeds the cap.")
|
|
34
|
+
chunk_size = ceil(len(prompts) / agent_cap)
|
|
35
|
+
grouped: list[str] = []
|
|
36
|
+
for i in range(0, len(prompts), chunk_size):
|
|
37
|
+
grouped.append(joiner.join(prompts[i : i + chunk_size]))
|
|
38
|
+
return grouped
|
|
39
|
+
|
|
40
|
+
elif strategy == "task_rows":
|
|
41
|
+
if task_rows is None:
|
|
42
|
+
raise ValueError("task_rows must be provided when using 'task_rows' strategy")
|
|
43
|
+
if task_rows >= len(prompts):
|
|
44
|
+
return prompts
|
|
45
|
+
print(f"Chunking {len(prompts)} prompts into groups of {task_rows} rows/tasks each.")
|
|
46
|
+
grouped: list[str] = []
|
|
47
|
+
for i in range(0, len(prompts), task_rows):
|
|
48
|
+
grouped.append(joiner.join(prompts[i : i + task_rows]))
|
|
49
|
+
return grouped
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
raise ValueError(f"Unknown splitting strategy: {strategy}")
|
|
@@ -21,6 +21,7 @@ from machineconfig.utils.utils2 import randstr, read_toml
|
|
|
21
21
|
from machineconfig.scripts.python.fire_jobs_args_helper import get_args, FireJobArgs, extract_kwargs
|
|
22
22
|
import platform
|
|
23
23
|
from typing import Optional
|
|
24
|
+
from pathlib import Path
|
|
24
25
|
# import os
|
|
25
26
|
|
|
26
27
|
|
|
@@ -42,7 +43,7 @@ def route(args: FireJobArgs) -> None:
|
|
|
42
43
|
choice_file = PathExtended(choice_file)
|
|
43
44
|
else:
|
|
44
45
|
choice_file = path_obj
|
|
45
|
-
repo_root = get_repo_root(
|
|
46
|
+
repo_root = get_repo_root(Path(choice_file))
|
|
46
47
|
print(f"💾 Selected file: {choice_file}.\nRepo root: {repo_root}")
|
|
47
48
|
ve_root_from_file, ipy_profile = get_ve_path_and_ipython_profile(choice_file)
|
|
48
49
|
if ipy_profile is None:
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
from machineconfig.utils.utils2 import pprint, read_json, read_ini
|
|
3
|
-
from pydantic import ConfigDict
|
|
4
|
-
from pydantic.dataclasses import dataclass
|
|
5
3
|
from typing import Optional
|
|
6
4
|
import os
|
|
7
5
|
from machineconfig.utils.source_of_truth import DEFAULTS_PATH
|
|
8
6
|
from rich.console import Console
|
|
9
7
|
from rich.panel import Panel
|
|
10
8
|
from rich import box # Import box
|
|
11
|
-
|
|
9
|
+
from dataclasses import dataclass
|
|
12
10
|
|
|
13
11
|
console = Console()
|
|
14
12
|
|
|
@@ -26,8 +24,7 @@ class ArgsDefaults:
|
|
|
26
24
|
key = None
|
|
27
25
|
pwd = None
|
|
28
26
|
|
|
29
|
-
|
|
30
|
-
@dataclass(config=ConfigDict(extra="forbid", frozen=False))
|
|
27
|
+
@dataclass
|
|
31
28
|
class Args:
|
|
32
29
|
cloud: Optional[str] = None
|
|
33
30
|
|
|
@@ -8,7 +8,7 @@ in the event that username@github.com is not mentioned in the remote url.
|
|
|
8
8
|
from machineconfig.utils.source_of_truth import CONFIG_PATH, DEFAULTS_PATH
|
|
9
9
|
from machineconfig.utils.path_reduced import PathExtended as PathExtended
|
|
10
10
|
from machineconfig.utils.utils2 import randstr, read_ini
|
|
11
|
-
from machineconfig.scripts.python.
|
|
11
|
+
from machineconfig.scripts.python.repos_helper_update import update_repository
|
|
12
12
|
from machineconfig.scripts.python.repos_helper_record import main as record_repos
|
|
13
13
|
|
|
14
14
|
import argparse
|