multiagent-init 0.1.1__tar.gz → 0.1.2__tar.gz
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.
- {multiagent_init-0.1.1/src/multiagent_init.egg-info → multiagent_init-0.1.2}/PKG-INFO +2 -2
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/pyproject.toml +2 -2
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init/cli.py +1 -1
- multiagent_init-0.1.2/src/multiagent_init/generator.py +648 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init/templates_data.py +219 -109
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init/wizard.py +19 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2/src/multiagent_init.egg-info}/PKG-INFO +2 -2
- multiagent_init-0.1.1/src/multiagent_init/generator.py +0 -315
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/LICENSE +0 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/README.md +0 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/setup.cfg +0 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init/__init__.py +0 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init.egg-info/SOURCES.txt +0 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init.egg-info/dependency_links.txt +0 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init.egg-info/entry_points.txt +0 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init.egg-info/requires.txt +0 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init.egg-info/top_level.txt +0 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/tests/test_generator.py +0 -0
- {multiagent_init-0.1.1 → multiagent_init-0.1.2}/tests/test_wizard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: multiagent-init
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Interactive CLI for generating multi-agent AI project boilerplates.
|
|
5
5
|
Author-email: Pavan Sekhar Mandavilli <pavanmandavilli2004@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://github.com/Pavanmandavilli/multiagent-init
|
|
|
8
8
|
Project-URL: Repository, https://github.com/Pavanmandavilli/multiagent-init
|
|
9
9
|
Project-URL: Issues, https://github.com/Pavanmandavilli/multiagent-init/issues
|
|
10
10
|
Keywords: multi-agent,agents,ai,scaffolding,cli,generator
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "multiagent-init"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Interactive CLI for generating multi-agent AI project boilerplates."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -12,7 +12,7 @@ license = {text = "MIT"}
|
|
|
12
12
|
authors = [{name = "Pavan Sekhar Mandavilli", email = "pavanmandavilli2004@gmail.com"}]
|
|
13
13
|
keywords = ["multi-agent", "agents", "ai", "scaffolding", "cli", "generator"]
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
16
|
"Intended Audience :: Developers",
|
|
17
17
|
"License :: OSI Approved :: MIT License",
|
|
18
18
|
"Programming Language :: Python :: 3",
|
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .templates_data import (
|
|
7
|
+
AUTOGEN_TOOL_WRAPPER,
|
|
8
|
+
CONFIG_TEMPLATE,
|
|
9
|
+
CREWAI_TOOL_WRAPPER,
|
|
10
|
+
DOCKER_FILE,
|
|
11
|
+
FRAMEWORK_FILES,
|
|
12
|
+
LANGGRAPH_ORCHESTRATOR_TEMPLATES,
|
|
13
|
+
NO_TOOL_SNIPPET,
|
|
14
|
+
ORCHESTRATOR_TEMPLATES,
|
|
15
|
+
PROVIDER_CLIENT_IMPORTS,
|
|
16
|
+
PROVIDER_CLIENT_INIT,
|
|
17
|
+
PROVIDER_CALL_SNIPPETS,
|
|
18
|
+
PROVIDER_DEFAULT_MODELS,
|
|
19
|
+
PROVIDER_DEPENDENCIES,
|
|
20
|
+
PROVIDER_ENV_VARS,
|
|
21
|
+
TEST_TEMPLATE,
|
|
22
|
+
TOOL_FILE,
|
|
23
|
+
TOOL_SEARCH_SNIPPET,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
DEFAULT_AGENT_CLASSES = {
|
|
27
|
+
"Researcher": "ResearcherAgent",
|
|
28
|
+
"Writer": "WriterAgent",
|
|
29
|
+
"Reviewer": "ReviewerAgent",
|
|
30
|
+
"Planner": "PlannerAgent",
|
|
31
|
+
"Executor": "ExecutorAgent",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _safe_project_name(value: str) -> str:
|
|
36
|
+
value = value.strip()
|
|
37
|
+
|
|
38
|
+
if not value:
|
|
39
|
+
raise ValueError("project name cannot be empty")
|
|
40
|
+
|
|
41
|
+
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_-]*", value):
|
|
42
|
+
raise ValueError("invalid project name")
|
|
43
|
+
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _safe_package_name(project_name: str) -> str:
|
|
48
|
+
value = re.sub(r"[^A-Za-z0-9_]", "_", project_name).lower()
|
|
49
|
+
|
|
50
|
+
if value[0].isdigit():
|
|
51
|
+
value = f"_{value}"
|
|
52
|
+
|
|
53
|
+
return value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def agent_to_module(name: str) -> str:
|
|
57
|
+
value = re.sub(r"[^A-Za-z0-9]+", "_", name.strip())
|
|
58
|
+
value = re.sub(r"_+", "_", value).strip("_").lower()
|
|
59
|
+
|
|
60
|
+
if not value:
|
|
61
|
+
value = "agent"
|
|
62
|
+
|
|
63
|
+
if value[0].isdigit():
|
|
64
|
+
value = f"agent_{value}"
|
|
65
|
+
|
|
66
|
+
return value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def agent_to_class(name: str) -> str:
|
|
70
|
+
parts = re.findall(r"[A-Za-z0-9]+", name.strip())
|
|
71
|
+
|
|
72
|
+
if not parts:
|
|
73
|
+
return "Agent"
|
|
74
|
+
|
|
75
|
+
class_name = "".join(part.capitalize() for part in parts)
|
|
76
|
+
|
|
77
|
+
if class_name[0].isdigit():
|
|
78
|
+
class_name = f"Agent{class_name}"
|
|
79
|
+
|
|
80
|
+
return f"{class_name}Agent"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _write(path: Path, content: str) -> None:
|
|
84
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
path.write_text(content, encoding="utf-8")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _comment_block(block: str) -> str:
|
|
89
|
+
"""Prefix every non-blank line of an already-indented code block with '# '."""
|
|
90
|
+
lines = block.rstrip("\n").split("\n")
|
|
91
|
+
commented = []
|
|
92
|
+
|
|
93
|
+
for line in lines:
|
|
94
|
+
if not line.strip():
|
|
95
|
+
commented.append("")
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
stripped = line.lstrip(" ")
|
|
99
|
+
indent = line[: len(line) - len(stripped)]
|
|
100
|
+
commented.append(f"{indent}# {stripped}")
|
|
101
|
+
|
|
102
|
+
return "\n".join(commented) + "\n"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _wired_run_body(provider: str, agent_name: str, include_tools: bool) -> str:
|
|
106
|
+
"""The real LLM (+ optional tool) call for an agent's run(), 8-space indented.
|
|
107
|
+
|
|
108
|
+
Assumes the provider client class and (if include_tools) WebSearchTool are
|
|
109
|
+
already imported at the top of the file - no imports happen in here.
|
|
110
|
+
"""
|
|
111
|
+
tool_snippet = TOOL_SEARCH_SNIPPET if include_tools else NO_TOOL_SNIPPET
|
|
112
|
+
client_snippet = PROVIDER_CLIENT_INIT[provider]
|
|
113
|
+
call_snippet = PROVIDER_CALL_SNIPPETS[provider].replace("{{AGENT_NAME}}", agent_name)
|
|
114
|
+
|
|
115
|
+
return tool_snippet + client_snippet + call_snippet
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _llm_import_lines(
|
|
119
|
+
package_name: str,
|
|
120
|
+
provider: str,
|
|
121
|
+
include_tools: bool,
|
|
122
|
+
is_default: bool,
|
|
123
|
+
) -> list[str]:
|
|
124
|
+
"""Top-of-file imports for the LLM client (+ tool), live or commented out."""
|
|
125
|
+
lines = [f"from {package_name} import config"]
|
|
126
|
+
|
|
127
|
+
provider_import = PROVIDER_CLIENT_IMPORTS[provider]
|
|
128
|
+
tool_import = f"from {package_name}.tools.web_search import WebSearchTool"
|
|
129
|
+
|
|
130
|
+
if is_default:
|
|
131
|
+
lines.append(provider_import)
|
|
132
|
+
if include_tools:
|
|
133
|
+
lines.append(tool_import)
|
|
134
|
+
else:
|
|
135
|
+
lines.append(f"# {provider_import}")
|
|
136
|
+
if include_tools:
|
|
137
|
+
lines.append(f"# {tool_import}")
|
|
138
|
+
|
|
139
|
+
return lines
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _render_run_method_body(
|
|
143
|
+
provider: str,
|
|
144
|
+
agent_name: str,
|
|
145
|
+
include_tools: bool,
|
|
146
|
+
is_default: bool,
|
|
147
|
+
) -> str:
|
|
148
|
+
wired_body = _wired_run_body(provider, agent_name, include_tools)
|
|
149
|
+
|
|
150
|
+
if is_default:
|
|
151
|
+
return wired_body
|
|
152
|
+
|
|
153
|
+
note = (
|
|
154
|
+
" # TODO: connect this agent to your LLM/framework.\n"
|
|
155
|
+
" # Remove the '# ' below (and from the matching imports at the top\n"
|
|
156
|
+
" # of this file) to wire this agent to your configured provider"
|
|
157
|
+
+ (" and tools" if include_tools else "")
|
|
158
|
+
+ ".\n"
|
|
159
|
+
)
|
|
160
|
+
fallback = f' return f"[{agent_name}] Completed: {{task}}"\n'
|
|
161
|
+
|
|
162
|
+
return note + _comment_block(wired_body) + fallback
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _render_basic_agent(
|
|
166
|
+
provider: str,
|
|
167
|
+
package_name: str,
|
|
168
|
+
agent_name: str,
|
|
169
|
+
class_name: str,
|
|
170
|
+
include_tools: bool,
|
|
171
|
+
is_default: bool,
|
|
172
|
+
) -> str:
|
|
173
|
+
run_body = _render_run_method_body(provider, agent_name, include_tools, is_default)
|
|
174
|
+
imports = "\n".join(_llm_import_lines(package_name, provider, include_tools, is_default))
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
f"{imports}\n"
|
|
178
|
+
"\n"
|
|
179
|
+
"\n"
|
|
180
|
+
f"class {class_name}:\n"
|
|
181
|
+
f' """{agent_name} agent."""\n'
|
|
182
|
+
"\n"
|
|
183
|
+
f' name = "{agent_name}"\n'
|
|
184
|
+
"\n"
|
|
185
|
+
" def run(self, task: str) -> str:\n"
|
|
186
|
+
f"{run_body}"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _render_langgraph_agent(
|
|
191
|
+
provider: str,
|
|
192
|
+
package_name: str,
|
|
193
|
+
agent_name: str,
|
|
194
|
+
class_name: str,
|
|
195
|
+
include_tools: bool,
|
|
196
|
+
is_default: bool,
|
|
197
|
+
) -> str:
|
|
198
|
+
run_body = _render_run_method_body(provider, agent_name, include_tools, is_default)
|
|
199
|
+
imports = "\n".join(_llm_import_lines(package_name, provider, include_tools, is_default))
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
f"{imports}\n"
|
|
203
|
+
"\n"
|
|
204
|
+
"\n"
|
|
205
|
+
f"class {class_name}:\n"
|
|
206
|
+
f' """{agent_name} node."""\n'
|
|
207
|
+
"\n"
|
|
208
|
+
f' name = "{agent_name}"\n'
|
|
209
|
+
"\n"
|
|
210
|
+
" def run(self, task: str) -> str:\n"
|
|
211
|
+
f"{run_body}"
|
|
212
|
+
"\n"
|
|
213
|
+
" def __call__(self, state: dict) -> dict:\n"
|
|
214
|
+
' """LangGraph node entry point - transforms and returns the graph state."""\n'
|
|
215
|
+
' state["result"] = self.run(state["task"])\n'
|
|
216
|
+
' state["task"] = state["result"]\n'
|
|
217
|
+
' state["completed"] = state.get("completed", 0) + 1\n'
|
|
218
|
+
" return state\n"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _render_crewai_agent(
|
|
223
|
+
provider: str,
|
|
224
|
+
package_name: str,
|
|
225
|
+
agent_name: str,
|
|
226
|
+
class_name: str,
|
|
227
|
+
include_tools: bool,
|
|
228
|
+
is_default: bool,
|
|
229
|
+
) -> str:
|
|
230
|
+
llm_value = 'f"gemini/{config.MODEL_NAME}"' if provider == "google" else "config.MODEL_NAME"
|
|
231
|
+
|
|
232
|
+
import_lines = [
|
|
233
|
+
"from crewai import Agent, Crew, Task",
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
if include_tools:
|
|
237
|
+
import_lines.insert(1, "from crewai.tools import tool")
|
|
238
|
+
|
|
239
|
+
import_lines += ["", f"from {package_name} import config"]
|
|
240
|
+
|
|
241
|
+
if include_tools:
|
|
242
|
+
import_lines.append(f"from {package_name}.tools.web_search import WebSearchTool")
|
|
243
|
+
|
|
244
|
+
if is_default:
|
|
245
|
+
init_extra = f" llm={llm_value},\n"
|
|
246
|
+
if include_tools:
|
|
247
|
+
init_extra += " tools=[web_search_tool],\n"
|
|
248
|
+
else:
|
|
249
|
+
comment_lines = [
|
|
250
|
+
" # Remove the '# ' below to wire this agent to an LLM"
|
|
251
|
+
+ (" and tools" if include_tools else "")
|
|
252
|
+
+ ".",
|
|
253
|
+
f" # llm={llm_value},",
|
|
254
|
+
]
|
|
255
|
+
if include_tools:
|
|
256
|
+
comment_lines.append(" # tools=[web_search_tool],")
|
|
257
|
+
init_extra = "\n".join(comment_lines) + "\n"
|
|
258
|
+
|
|
259
|
+
class_header = (
|
|
260
|
+
f"class {class_name}:\n"
|
|
261
|
+
f' """{agent_name} agent."""\n'
|
|
262
|
+
"\n"
|
|
263
|
+
f' name = "{agent_name}"\n'
|
|
264
|
+
"\n"
|
|
265
|
+
" def __init__(self) -> None:\n"
|
|
266
|
+
" self.agent = Agent(\n"
|
|
267
|
+
f' role="{agent_name}",\n'
|
|
268
|
+
f' goal="Complete tasks assigned to the {agent_name} role.",\n'
|
|
269
|
+
f' backstory="An AI agent specialized in {agent_name} tasks.",\n'
|
|
270
|
+
f"{init_extra}"
|
|
271
|
+
" allow_delegation=False,\n"
|
|
272
|
+
" )\n"
|
|
273
|
+
"\n"
|
|
274
|
+
" def run(self, task: str) -> str:\n"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
wired_run = (
|
|
278
|
+
" crew_task = Task(\n"
|
|
279
|
+
" description=task,\n"
|
|
280
|
+
" agent=self.agent,\n"
|
|
281
|
+
' expected_output="A completed response for the assigned step.",\n'
|
|
282
|
+
" )\n"
|
|
283
|
+
" return Crew(agents=[self.agent], tasks=[crew_task]).kickoff().raw\n"
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if is_default:
|
|
287
|
+
run_body = wired_run
|
|
288
|
+
else:
|
|
289
|
+
note = (
|
|
290
|
+
" # TODO: wrap `task` in a crewai.Task and execute it via a Crew.\n"
|
|
291
|
+
" # Remove the '# ' below to wire this agent's run() to a real Crew.\n"
|
|
292
|
+
)
|
|
293
|
+
fallback = f' return f"[{agent_name}] Completed: {{task}}"\n'
|
|
294
|
+
run_body = note + _comment_block(wired_run) + fallback
|
|
295
|
+
|
|
296
|
+
parts = ["\n".join(import_lines)]
|
|
297
|
+
|
|
298
|
+
if include_tools:
|
|
299
|
+
parts.append(CREWAI_TOOL_WRAPPER)
|
|
300
|
+
|
|
301
|
+
parts.append(class_header + run_body)
|
|
302
|
+
|
|
303
|
+
return "\n\n".join(parts)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _render_autogen_agent(
|
|
307
|
+
provider: str,
|
|
308
|
+
package_name: str,
|
|
309
|
+
agent_name: str,
|
|
310
|
+
class_name: str,
|
|
311
|
+
include_tools: bool,
|
|
312
|
+
is_default: bool,
|
|
313
|
+
) -> str:
|
|
314
|
+
import_lines = ["from autogen_agentchat.agents import AssistantAgent", "", f"from {package_name} import config"]
|
|
315
|
+
|
|
316
|
+
if include_tools:
|
|
317
|
+
import_lines.append(f"from {package_name}.tools.web_search import WebSearchTool")
|
|
318
|
+
|
|
319
|
+
provider_import = PROVIDER_CLIENT_IMPORTS[provider]
|
|
320
|
+
import_lines.append(provider_import if is_default else f"# {provider_import}")
|
|
321
|
+
|
|
322
|
+
if is_default:
|
|
323
|
+
tools_extra = " tools=[web_search],\n" if include_tools else ""
|
|
324
|
+
else:
|
|
325
|
+
tools_extra = " # tools=[web_search],\n" if include_tools else ""
|
|
326
|
+
|
|
327
|
+
build_method = (
|
|
328
|
+
" def build(self, model_client) -> AssistantAgent:\n"
|
|
329
|
+
' """Build a real AutoGen AssistantAgent once you have a model client."""\n'
|
|
330
|
+
" return AssistantAgent(\n"
|
|
331
|
+
f' name="{agent_name}",\n'
|
|
332
|
+
" model_client=model_client,\n"
|
|
333
|
+
f' system_message="You are the {agent_name} agent.",\n'
|
|
334
|
+
f"{tools_extra}"
|
|
335
|
+
" )\n"
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
run_body = _render_run_method_body(provider, agent_name, include_tools, is_default)
|
|
339
|
+
|
|
340
|
+
class_body = (
|
|
341
|
+
f"class {class_name}:\n"
|
|
342
|
+
f' """{agent_name} agent."""\n'
|
|
343
|
+
"\n"
|
|
344
|
+
f' name = "{agent_name}"\n'
|
|
345
|
+
"\n"
|
|
346
|
+
f"{build_method}"
|
|
347
|
+
"\n"
|
|
348
|
+
" def run(self, task: str) -> str:\n"
|
|
349
|
+
f"{run_body}"
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
parts = ["\n".join(import_lines)]
|
|
353
|
+
|
|
354
|
+
if include_tools:
|
|
355
|
+
parts.append(AUTOGEN_TOOL_WRAPPER)
|
|
356
|
+
|
|
357
|
+
parts.append(class_body)
|
|
358
|
+
|
|
359
|
+
return "\n\n".join(parts)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
_AGENT_RENDERERS = {
|
|
363
|
+
"basic": _render_basic_agent,
|
|
364
|
+
"langgraph": _render_langgraph_agent,
|
|
365
|
+
"crewai": _render_crewai_agent,
|
|
366
|
+
"autogen": _render_autogen_agent,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def generate_project(config: dict) -> Path:
|
|
371
|
+
project_name = _safe_project_name(config["project_name"])
|
|
372
|
+
package_name = _safe_package_name(project_name)
|
|
373
|
+
destination = Path.cwd() / project_name
|
|
374
|
+
|
|
375
|
+
if destination.exists():
|
|
376
|
+
if not destination.is_dir():
|
|
377
|
+
raise FileExistsError(f"'{project_name}' already exists and is not a directory")
|
|
378
|
+
|
|
379
|
+
if any(destination.iterdir()):
|
|
380
|
+
raise FileExistsError(
|
|
381
|
+
f"directory '{project_name}' already exists and is not empty"
|
|
382
|
+
)
|
|
383
|
+
else:
|
|
384
|
+
destination.mkdir(parents=True)
|
|
385
|
+
|
|
386
|
+
package_dir = destination / "src" / package_name
|
|
387
|
+
|
|
388
|
+
provider = config["provider"]
|
|
389
|
+
is_default = config["agent_mode"] == "default"
|
|
390
|
+
|
|
391
|
+
framework_pyproject = FRAMEWORK_FILES[config["framework"]]
|
|
392
|
+
framework_pyproject = framework_pyproject.replace("{{PROJECT_NAME}}", project_name)
|
|
393
|
+
framework_pyproject = framework_pyproject.replace("{{PACKAGE_NAME}}", package_name)
|
|
394
|
+
framework_pyproject = framework_pyproject.replace(
|
|
395
|
+
"{{PROVIDER_DEPENDENCY}}", PROVIDER_DEPENDENCIES[provider]
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
_write(destination / "pyproject.toml", framework_pyproject)
|
|
399
|
+
|
|
400
|
+
_write(
|
|
401
|
+
destination / "README.md",
|
|
402
|
+
_render_readme(config, package_name),
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
provider_env_var = PROVIDER_ENV_VARS[provider]
|
|
406
|
+
default_model = PROVIDER_DEFAULT_MODELS[provider]
|
|
407
|
+
|
|
408
|
+
_write(
|
|
409
|
+
destination / ".env",
|
|
410
|
+
f"MODEL_NAME={default_model}\n{provider_env_var}=\n",
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
_write(
|
|
414
|
+
destination / ".gitignore",
|
|
415
|
+
"__pycache__/\n*.py[cod]\n.venv/\n.env\n.pytest_cache/\n",
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
_write(package_dir / "__init__.py", '"""Generated package."""\n')
|
|
419
|
+
|
|
420
|
+
config_content = CONFIG_TEMPLATE.replace("{{PROVIDER_ENV_VAR}}", provider_env_var)
|
|
421
|
+
config_content = config_content.replace("{{PROVIDER}}", provider)
|
|
422
|
+
config_content = config_content.replace("{{DEFAULT_MODEL}}", default_model)
|
|
423
|
+
_write(package_dir / "config.py", config_content)
|
|
424
|
+
|
|
425
|
+
render_agent = _AGENT_RENDERERS[config["framework"]]
|
|
426
|
+
|
|
427
|
+
agent_files = []
|
|
428
|
+
|
|
429
|
+
for agent_name in config["agent_names"]:
|
|
430
|
+
module_name = agent_to_module(agent_name)
|
|
431
|
+
class_name = agent_to_class(agent_name)
|
|
432
|
+
|
|
433
|
+
agent_files.append(
|
|
434
|
+
{
|
|
435
|
+
"name": agent_name,
|
|
436
|
+
"module": module_name,
|
|
437
|
+
"class": class_name,
|
|
438
|
+
}
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
content = render_agent(
|
|
442
|
+
provider,
|
|
443
|
+
package_name,
|
|
444
|
+
agent_name,
|
|
445
|
+
class_name,
|
|
446
|
+
config["include_tools"],
|
|
447
|
+
is_default,
|
|
448
|
+
)
|
|
449
|
+
|
|
450
|
+
_write(
|
|
451
|
+
package_dir / "agents" / f"{module_name}.py",
|
|
452
|
+
content,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
_write(
|
|
456
|
+
package_dir / "agents" / "__init__.py",
|
|
457
|
+
'"""Generated agents."""\n',
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
imports = "\n".join(
|
|
461
|
+
f"from {package_name}.agents.{item['module']} import {item['class']}"
|
|
462
|
+
for item in agent_files
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
instances = ",\n ".join(
|
|
466
|
+
f"{item['class']}()"
|
|
467
|
+
for item in agent_files
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
main_content = f"""from __future__ import annotations
|
|
471
|
+
|
|
472
|
+
import logging
|
|
473
|
+
|
|
474
|
+
{imports}
|
|
475
|
+
|
|
476
|
+
from {package_name} import config
|
|
477
|
+
from {package_name}.workflow.orchestrator import Orchestrator
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def main() -> None:
|
|
481
|
+
logging.basicConfig(
|
|
482
|
+
level=logging.INFO,
|
|
483
|
+
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
config.validate()
|
|
487
|
+
|
|
488
|
+
agents = [
|
|
489
|
+
{instances}
|
|
490
|
+
]
|
|
491
|
+
|
|
492
|
+
orchestrator = Orchestrator(agents)
|
|
493
|
+
|
|
494
|
+
result = orchestrator.run(
|
|
495
|
+
"Build a short report about multi-agent systems."
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
print("\\n=== Final result ===")
|
|
499
|
+
print(result)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
if __name__ == "__main__":
|
|
503
|
+
main()
|
|
504
|
+
"""
|
|
505
|
+
|
|
506
|
+
_write(destination / "main.py", main_content)
|
|
507
|
+
|
|
508
|
+
_write(
|
|
509
|
+
package_dir / "workflow" / "__init__.py",
|
|
510
|
+
'"""Workflow and orchestration."""\n',
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
if config["framework"] == "langgraph":
|
|
514
|
+
orchestrator_content = LANGGRAPH_ORCHESTRATOR_TEMPLATES[
|
|
515
|
+
config.get("langgraph_orchestration", "sequential")
|
|
516
|
+
]
|
|
517
|
+
else:
|
|
518
|
+
orchestrator_content = ORCHESTRATOR_TEMPLATES[config["framework"]]
|
|
519
|
+
|
|
520
|
+
_write(
|
|
521
|
+
package_dir / "workflow" / "orchestrator.py",
|
|
522
|
+
orchestrator_content,
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
if config["include_tools"]:
|
|
526
|
+
_write(
|
|
527
|
+
package_dir / "tools" / "__init__.py",
|
|
528
|
+
'"""Tools available to agents."""\n',
|
|
529
|
+
)
|
|
530
|
+
_write(package_dir / "tools" / "web_search.py", TOOL_FILE)
|
|
531
|
+
|
|
532
|
+
if config["include_tests"]:
|
|
533
|
+
test_content = TEST_TEMPLATE.replace(
|
|
534
|
+
"{{TEST_IMPORTS}}",
|
|
535
|
+
"\n".join(
|
|
536
|
+
f"from {package_name}.agents.{item['module']} import {item['class']}"
|
|
537
|
+
for item in agent_files
|
|
538
|
+
),
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
assertions = "\n".join(
|
|
542
|
+
f' self.assertEqual({item["class"]}().name, "{item["name"]}")'
|
|
543
|
+
for item in agent_files
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
test_content = test_content.replace("{{ASSERTIONS}}", assertions)
|
|
547
|
+
|
|
548
|
+
_write(
|
|
549
|
+
destination / "tests" / "test_agents.py",
|
|
550
|
+
test_content,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
if config["include_docker"]:
|
|
554
|
+
docker_content = DOCKER_FILE.replace("{{PACKAGE_NAME}}", package_name)
|
|
555
|
+
_write(destination / "Dockerfile", docker_content)
|
|
556
|
+
|
|
557
|
+
print()
|
|
558
|
+
print("Creating project...")
|
|
559
|
+
print()
|
|
560
|
+
print("✓ Agents")
|
|
561
|
+
print("✓ Workflow")
|
|
562
|
+
|
|
563
|
+
if config["include_tools"]:
|
|
564
|
+
print("✓ Tools")
|
|
565
|
+
|
|
566
|
+
print("✓ Configuration")
|
|
567
|
+
|
|
568
|
+
if config["include_tests"]:
|
|
569
|
+
print("✓ Tests")
|
|
570
|
+
|
|
571
|
+
if config["include_docker"]:
|
|
572
|
+
print("✓ Docker")
|
|
573
|
+
|
|
574
|
+
print("✓ README")
|
|
575
|
+
|
|
576
|
+
return destination
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _render_readme(config: dict, package_name: str) -> str:
|
|
580
|
+
agents = "\n".join(
|
|
581
|
+
f"- `{name}`"
|
|
582
|
+
for name in config["agent_names"]
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
wiring_note = (
|
|
586
|
+
"Every agent is pre-wired to call your configured model provider (and the "
|
|
587
|
+
"web-search tool, if included) - just set your API key in `.env` and run."
|
|
588
|
+
if config["agent_mode"] == "default"
|
|
589
|
+
else (
|
|
590
|
+
"Each agent ships with a ready-to-use LLM (and tool) call written out as "
|
|
591
|
+
"a comment inside `run()`. Remove the leading `# ` on those lines (and "
|
|
592
|
+
"the placeholder `return` below them) to wire it up."
|
|
593
|
+
)
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
orchestration_line = ""
|
|
597
|
+
if config["framework"] == "langgraph":
|
|
598
|
+
mode = config.get("langgraph_orchestration", "sequential")
|
|
599
|
+
orchestration_line = (
|
|
600
|
+
f"- Orchestration: `{mode}`"
|
|
601
|
+
+ (
|
|
602
|
+
" (fixed pipeline, agents run in order)"
|
|
603
|
+
if mode == "sequential"
|
|
604
|
+
else " (a supervisor node routes to the next agent at runtime)"
|
|
605
|
+
)
|
|
606
|
+
+ "\n"
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
return f"""# {config["project_name"]}
|
|
610
|
+
|
|
611
|
+
Generated by **multiagent-init**.
|
|
612
|
+
|
|
613
|
+
## Configuration
|
|
614
|
+
|
|
615
|
+
- Framework: `{config["framework"]}`
|
|
616
|
+
- Model provider: `{config["provider"]}`
|
|
617
|
+
- Agent configuration: `{config["agent_mode"]}`
|
|
618
|
+
- Number of agents: `{config["agent_count"]}`
|
|
619
|
+
{orchestration_line}
|
|
620
|
+
|
|
621
|
+
## Agents
|
|
622
|
+
|
|
623
|
+
{agents}
|
|
624
|
+
|
|
625
|
+
## Structure
|
|
626
|
+
|
|
627
|
+
```text
|
|
628
|
+
src/{package_name}/
|
|
629
|
+
agents/ Individual agents
|
|
630
|
+
workflow/ Orchestration
|
|
631
|
+
tools/ Optional tools
|
|
632
|
+
config.py Configuration
|
|
633
|
+
tests/ Optional tests
|
|
634
|
+
main.py Entry point (python main.py)
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## Run
|
|
638
|
+
|
|
639
|
+
```bash
|
|
640
|
+
pip install .
|
|
641
|
+
python main.py
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Set your provider's API key in `.env` before running - `config.validate()` fails
|
|
645
|
+
fast with a clear error if it's missing.
|
|
646
|
+
|
|
647
|
+
{wiring_note}
|
|
648
|
+
"""
|