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.
Files changed (19) hide show
  1. {multiagent_init-0.1.1/src/multiagent_init.egg-info → multiagent_init-0.1.2}/PKG-INFO +2 -2
  2. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/pyproject.toml +2 -2
  3. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init/cli.py +1 -1
  4. multiagent_init-0.1.2/src/multiagent_init/generator.py +648 -0
  5. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init/templates_data.py +219 -109
  6. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init/wizard.py +19 -0
  7. {multiagent_init-0.1.1 → multiagent_init-0.1.2/src/multiagent_init.egg-info}/PKG-INFO +2 -2
  8. multiagent_init-0.1.1/src/multiagent_init/generator.py +0 -315
  9. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/LICENSE +0 -0
  10. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/README.md +0 -0
  11. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/setup.cfg +0 -0
  12. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init/__init__.py +0 -0
  13. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init.egg-info/SOURCES.txt +0 -0
  14. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init.egg-info/dependency_links.txt +0 -0
  15. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init.egg-info/entry_points.txt +0 -0
  16. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init.egg-info/requires.txt +0 -0
  17. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/src/multiagent_init.egg-info/top_level.txt +0 -0
  18. {multiagent_init-0.1.1 → multiagent_init-0.1.2}/tests/test_generator.py +0 -0
  19. {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.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 :: 3 - Alpha
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.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 :: 3 - Alpha",
15
+ "Development Status :: 5 - Production/Stable",
16
16
  "Intended Audience :: Developers",
17
17
  "License :: OSI Approved :: MIT License",
18
18
  "Programming Language :: Python :: 3",
@@ -18,7 +18,7 @@ def main() -> int:
18
18
  print()
19
19
  print(f" cd {destination.name}")
20
20
  print(" python3 -m venv .venv")
21
- print(" source .venv/bin/activate # macOS/Linux")
21
+ print(" source .venv/bin/activate")
22
22
  print(" pip install .")
23
23
  print(" python main.py")
24
24
  print()
@@ -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
+ """