ruslanmv commited on
Commit
3c075e4
·
verified ·
1 Parent(s): af0928e

v0.2.0: production infrastructure — spec-first generators, security validator, ArtifactBundle

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. Dockerfile +5 -0
  2. agent_generator_pkg/LICENSE +13 -0
  3. agent_generator_pkg/README.md +174 -0
  4. agent_generator_pkg/pyproject.toml +145 -0
  5. agent_generator_pkg/src/agent_generator/__init__.py +40 -0
  6. agent_generator_pkg/src/agent_generator/__pycache__/__init__.cpython-311.pyc +0 -0
  7. agent_generator_pkg/src/agent_generator/__pycache__/cli.cpython-311.pyc +0 -0
  8. agent_generator_pkg/src/agent_generator/__pycache__/config.cpython-311.pyc +0 -0
  9. agent_generator_pkg/src/agent_generator/__pycache__/wsgi.cpython-311.pyc +0 -0
  10. agent_generator_pkg/src/agent_generator/application/__init__.py +0 -0
  11. agent_generator_pkg/src/agent_generator/application/__pycache__/__init__.cpython-311.pyc +0 -0
  12. agent_generator_pkg/src/agent_generator/application/__pycache__/build_service.cpython-311.pyc +0 -0
  13. agent_generator_pkg/src/agent_generator/application/__pycache__/planning_service.cpython-311.pyc +0 -0
  14. agent_generator_pkg/src/agent_generator/application/__pycache__/validation_service.cpython-311.pyc +0 -0
  15. agent_generator_pkg/src/agent_generator/application/build_service.py +125 -0
  16. agent_generator_pkg/src/agent_generator/application/planning_service.py +152 -0
  17. agent_generator_pkg/src/agent_generator/application/release_service.py +16 -0
  18. agent_generator_pkg/src/agent_generator/application/validation_service.py +26 -0
  19. agent_generator_pkg/src/agent_generator/cli.py +245 -0
  20. agent_generator_pkg/src/agent_generator/config.py +158 -0
  21. agent_generator_pkg/src/agent_generator/data/default_prompts.yaml +31 -0
  22. agent_generator_pkg/src/agent_generator/domain/__init__.py +0 -0
  23. agent_generator_pkg/src/agent_generator/domain/__pycache__/__init__.cpython-311.pyc +0 -0
  24. agent_generator_pkg/src/agent_generator/domain/__pycache__/artifact_bundle.cpython-311.pyc +0 -0
  25. agent_generator_pkg/src/agent_generator/domain/__pycache__/capability_matrix.cpython-311.pyc +0 -0
  26. agent_generator_pkg/src/agent_generator/domain/__pycache__/project_spec.cpython-311.pyc +0 -0
  27. agent_generator_pkg/src/agent_generator/domain/__pycache__/render_plan.cpython-311.pyc +0 -0
  28. agent_generator_pkg/src/agent_generator/domain/artifact_bundle.py +31 -0
  29. agent_generator_pkg/src/agent_generator/domain/capability_matrix.py +65 -0
  30. agent_generator_pkg/src/agent_generator/domain/project_spec.py +94 -0
  31. agent_generator_pkg/src/agent_generator/domain/render_plan.py +26 -0
  32. agent_generator_pkg/src/agent_generator/frameworks/__init__.py +26 -0
  33. agent_generator_pkg/src/agent_generator/frameworks/__pycache__/__init__.cpython-311.pyc +0 -0
  34. agent_generator_pkg/src/agent_generator/frameworks/__pycache__/base.cpython-311.pyc +0 -0
  35. agent_generator_pkg/src/agent_generator/frameworks/base.py +181 -0
  36. agent_generator_pkg/src/agent_generator/frameworks/crewai/__init__.py +12 -0
  37. agent_generator_pkg/src/agent_generator/frameworks/crewai/__pycache__/__init__.cpython-311.pyc +0 -0
  38. agent_generator_pkg/src/agent_generator/frameworks/crewai/__pycache__/generator.cpython-311.pyc +0 -0
  39. agent_generator_pkg/src/agent_generator/frameworks/crewai/generator.py +83 -0
  40. agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/__init__.py +7 -0
  41. agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/__pycache__/__init__.cpython-311.pyc +0 -0
  42. agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/__pycache__/generator.cpython-311.pyc +0 -0
  43. agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/generator.py +120 -0
  44. agent_generator_pkg/src/agent_generator/frameworks/langgraph/__init__.py +7 -0
  45. agent_generator_pkg/src/agent_generator/frameworks/langgraph/__pycache__/__init__.cpython-311.pyc +0 -0
  46. agent_generator_pkg/src/agent_generator/frameworks/langgraph/__pycache__/generator.cpython-311.pyc +0 -0
  47. agent_generator_pkg/src/agent_generator/frameworks/langgraph/generator.py +107 -0
  48. agent_generator_pkg/src/agent_generator/frameworks/react/__init__.py +7 -0
  49. agent_generator_pkg/src/agent_generator/frameworks/react/__pycache__/__init__.cpython-311.pyc +0 -0
  50. agent_generator_pkg/src/agent_generator/frameworks/react/__pycache__/generator.cpython-311.pyc +0 -0
Dockerfile CHANGED
@@ -2,6 +2,11 @@ FROM python:3.11-slim
2
 
3
  WORKDIR /app
4
 
 
 
 
 
 
5
  COPY requirements.txt .
6
  RUN pip install --no-cache-dir -r requirements.txt
7
 
 
2
 
3
  WORKDIR /app
4
 
5
+ # Install agent-generator from the repo (includes all production infrastructure)
6
+ COPY agent_generator_pkg/ /tmp/agent-generator-pkg/
7
+ RUN pip install --no-cache-dir /tmp/agent-generator-pkg/ && rm -rf /tmp/agent-generator-pkg/
8
+
9
+ # Install web dependencies
10
  COPY requirements.txt .
11
  RUN pip install --no-cache-dir -r requirements.txt
12
 
agent_generator_pkg/LICENSE ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Copyright 2025 ruslanmv.com
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
agent_generator_pkg/README.md ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <p align="left">
2
+ <img src="https://github.com/ruslanmv/agent-generator/blob/master/docs/images/logo.png" alt="Agent-Generator Logo" width="300">
3
+ </p>
4
+
5
+ # agent-generator
6
+
7
+ **Create AI agents from a single sentence.**
8
+
9
+ [![PyPI](https://img.shields.io/pypi/v/agent-generator.svg)](https://pypi.org/project/agent-generator/)
10
+ [![Python 3.10+](https://img.shields.io/pypi/pyversions/agent-generator.svg)](https://pypi.org/project/agent-generator/)
11
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
12
+ [![Try it](https://img.shields.io/badge/demo-Hugging%20Face-yellow)](https://huggingface.co/spaces/ruslanmv/agent-generator)
13
+ <a href="https://github.com/agent-matrix/matrix-hub"><img src="https://img.shields.io/badge/Powered%20by-matrix--hub-brightgreen" alt="Powered by matrix-hub"></a>
14
+
15
+ ---
16
+
17
+ ## What does it do?
18
+
19
+ You type what you want. You get a complete project.
20
+
21
+ ```bash
22
+ agent-generator "Build a research team with a researcher and a writer" -f crewai
23
+ ```
24
+
25
+ That's it. You now have working Python code with agents, tasks, and tools.
26
+
27
+ ---
28
+
29
+ ## Try it online
30
+
31
+ No install needed. Open the demo and start building:
32
+
33
+ **https://huggingface.co/spaces/ruslanmv/agent-generator**
34
+
35
+ ---
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ pip install agent-generator
41
+ ```
42
+
43
+ That gives you the CLI and all core features. Python 3.10+ required.
44
+
45
+ **Optional extras:**
46
+
47
+ ```bash
48
+ pip install "agent-generator[openai]" # OpenAI provider
49
+ pip install "agent-generator[all]" # All providers + frameworks
50
+ ```
51
+
52
+ ---
53
+
54
+ ## Quick start
55
+
56
+ ### Step 1 -- Set credentials (pick one)
57
+
58
+ ```bash
59
+ # Option A: WatsonX (default)
60
+ export WATSONX_API_KEY=your-key
61
+ export WATSONX_PROJECT_ID=your-project-id
62
+
63
+ # Option B: OpenAI
64
+ export OPENAI_API_KEY=sk-your-key
65
+ export AGENTGEN_PROVIDER=openai
66
+ ```
67
+
68
+ ### Step 2 -- Generate
69
+
70
+ ```bash
71
+ # CrewAI team
72
+ agent-generator "Research team that finds papers and writes summaries" -f crewai -o team.py
73
+
74
+ # LangGraph pipeline
75
+ agent-generator "ETL pipeline: extract, transform, load" -f langgraph -o pipeline.py
76
+
77
+ # WatsonX YAML agent
78
+ agent-generator "Customer support assistant" -f watsonx_orchestrate -o support.yaml
79
+
80
+ # Just test it (no credentials needed)
81
+ agent-generator "Hello world agent" -f crewai --dry-run
82
+ ```
83
+
84
+ ### Step 3 -- Run
85
+
86
+ ```bash
87
+ python team.py
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Supported frameworks
93
+
94
+ | Framework | What you get |
95
+ |-----------|-------------|
96
+ | **CrewAI** | `crew.py` + `agents.yaml` + `tasks.yaml` + tools + tests |
97
+ | **LangGraph** | `graph.py` with `StateGraph` and typed state |
98
+ | **WatsonX Orchestrate** | `agent.yaml` ready for `orchestrate agents import` |
99
+ | **CrewAI Flow** | Flow class with `@start` / `@listen` |
100
+ | **ReAct** | Reasoning loop with tool registry |
101
+
102
+ ---
103
+
104
+ ## Built-in tools
105
+
106
+ Your agents can use these out of the box:
107
+
108
+ | Tool | What it does |
109
+ |------|-------------|
110
+ | `web_search` | Search the web |
111
+ | `pdf_reader` | Read PDF files |
112
+ | `http_client` | Call APIs |
113
+ | `sql_query` | Query databases |
114
+ | `file_writer` | Save files |
115
+ | `vector_search` | Semantic search |
116
+
117
+ ---
118
+
119
+ ## Web UI
120
+
121
+ A visual wizard that guides you step by step:
122
+
123
+ ```bash
124
+ uvicorn agent_generator.wsgi:app --port 8000
125
+ ```
126
+
127
+ Open **http://localhost:8000** -- describe your agents, pick a framework, download a ZIP.
128
+
129
+ Works with local LLMs (Ollama), remote LLMs (OllaBridge), or cloud APIs (OpenAI).
130
+
131
+ ---
132
+
133
+ ## CLI options
134
+
135
+ ```
136
+ agent-generator "your description" [options]
137
+ ```
138
+
139
+ | Flag | What it does |
140
+ |------|-------------|
141
+ | `-f crewai` | Pick framework: `crewai`, `langgraph`, `watsonx_orchestrate`, `crewai_flow`, `react` |
142
+ | `-o file.py` | Save to file (otherwise prints to screen) |
143
+ | `--dry-run` | Generate without calling any LLM |
144
+ | `--mcp` | Add a FastAPI server wrapper |
145
+ | `-p openai` | Use OpenAI instead of WatsonX |
146
+ | `--show-cost` | Show estimated cost before generating |
147
+
148
+ ---
149
+
150
+ ## Docker
151
+
152
+ ```bash
153
+ docker build -t agent-generator .
154
+ docker run -e WATSONX_API_KEY=... -p 8000:8000 agent-generator
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Contributing
160
+
161
+ ```bash
162
+ git clone https://github.com/ruslanmv/agent-generator.git
163
+ cd agent-generator
164
+ pip install -e ".[dev,all]"
165
+ pytest # run tests
166
+ ruff check src/ # lint
167
+ mkdocs serve # docs
168
+ ```
169
+
170
+ ---
171
+
172
+ ## License
173
+
174
+ Apache 2.0 -- PRs and issues welcome.
agent_generator_pkg/pyproject.toml ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ────────────────────────────────────────────────────────────────
2
+ # pyproject.toml • build + metadata for agent-generator
3
+ # ────────────────────────────────────────────────────────────────
4
+
5
+ [build-system]
6
+ requires = ["hatchling>=1.18"]
7
+ build-backend = "hatchling.build"
8
+
9
+ [project]
10
+ name = "agent-generator"
11
+ version = "0.2.0"
12
+ description = "Turn plain-English requirements into production-ready multi-agent AI projects (CrewAI, LangGraph, WatsonX Orchestrate)."
13
+ readme = "README.md"
14
+ requires-python = ">=3.10"
15
+ license = { file = "LICENSE" }
16
+ authors = [
17
+ { name = "Ruslan M. V.", email = "contact@ruslanmv.com" }
18
+ ]
19
+ keywords = [
20
+ "multi-agent",
21
+ "crewai",
22
+ "langgraph",
23
+ "watsonx",
24
+ "orchestrate",
25
+ "LLM",
26
+ "code-generation",
27
+ ]
28
+ classifiers = [
29
+ "Development Status :: 4 - Beta",
30
+ "Intended Audience :: Developers",
31
+ "License :: OSI Approved :: Apache Software License",
32
+ "Programming Language :: Python :: 3 :: Only",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Framework :: FastAPI",
38
+ "Topic :: Software Development :: Code Generators",
39
+ "Topic :: Software Development :: Libraries :: Application Frameworks",
40
+ ]
41
+
42
+ # -----------------------------------------------------------------
43
+ # Core runtime dependencies
44
+ # -----------------------------------------------------------------
45
+ dependencies = [
46
+ "pydantic>=2.12,<3",
47
+ "pydantic-settings>=2.13,<3",
48
+ "python-dotenv>=1.0",
49
+ "typer>=0.24,<1",
50
+ "jinja2>=3.1,<4",
51
+ "requests>=2.33,<3",
52
+ "rich>=14.0,<15",
53
+ "structlog>=25.0,<26",
54
+ "pyyaml>=6.0,<7",
55
+ ]
56
+
57
+ # -----------------------------------------------------------------
58
+ # Optional extras
59
+ # -----------------------------------------------------------------
60
+ [project.optional-dependencies]
61
+ openai = [
62
+ "openai>=2.30,<3",
63
+ "tiktoken>=0.12,<1",
64
+ ]
65
+ crewai = [
66
+ "crewai>=1.12,<2",
67
+ ]
68
+ langgraph = [
69
+ "langgraph>=1.1,<2",
70
+ "langchain-core>=0.1",
71
+ ]
72
+ web = [
73
+ "fastapi>=0.135,<1",
74
+ "uvicorn>=0.42,<1",
75
+ "python-multipart>=0.0.18",
76
+ "requests>=2.33,<3",
77
+ ]
78
+ all = [
79
+ "agent-generator[openai,crewai,langgraph,web]",
80
+ ]
81
+ dev = [
82
+ "pytest>=8.2",
83
+ "pytest-xdist>=3.5",
84
+ "pytest-asyncio>=0.24",
85
+ "mypy>=1.10",
86
+ "ruff>=0.4",
87
+ "black>=24.4",
88
+ "isort>=5.0",
89
+ "pytest-cov>=5.0",
90
+ "pre-commit>=3.7",
91
+ "mkdocs>=1.6",
92
+ "mkdocs-material>=9.5",
93
+ ]
94
+
95
+ # -----------------------------------------------------------------
96
+ # CLI entry-point
97
+ # -----------------------------------------------------------------
98
+ [project.scripts]
99
+ agent-generator = "agent_generator.cli:_main"
100
+
101
+ # -----------------------------------------------------------------
102
+ # URLs
103
+ # -----------------------------------------------------------------
104
+ [project.urls]
105
+ Homepage = "https://github.com/ruslanmv/agent-generator"
106
+ Repository = "https://github.com/ruslanmv/agent-generator"
107
+ Issues = "https://github.com/ruslanmv/agent-generator/issues"
108
+
109
+ # -----------------------------------------------------------------
110
+ # Ruff configuration
111
+ # -----------------------------------------------------------------
112
+ [tool.ruff]
113
+ target-version = "py310"
114
+ line-length = 100
115
+
116
+ [tool.ruff.lint]
117
+ select = ["E", "F", "I", "W"]
118
+ ignore = ["F841", "E501"]
119
+
120
+ [tool.ruff.lint.isort]
121
+ known-first-party = ["agent_generator"]
122
+
123
+ # -----------------------------------------------------------------
124
+ # Pytest configuration
125
+ # -----------------------------------------------------------------
126
+ [tool.pytest.ini_options]
127
+ testpaths = ["tests"]
128
+ python_files = ["test_*.py"]
129
+ python_classes = ["Test*"]
130
+ python_functions = ["test_*"]
131
+
132
+ # -----------------------------------------------------------------
133
+ # MyPy configuration
134
+ # -----------------------------------------------------------------
135
+ [tool.mypy]
136
+ python_version = "3.10"
137
+ warn_return_any = true
138
+ warn_unused_configs = true
139
+ ignore_missing_imports = true
140
+
141
+ # -----------------------------------------------------------------
142
+ # Hatchling configuration
143
+ # -----------------------------------------------------------------
144
+ [tool.hatch.build.targets.wheel]
145
+ packages = ["src/agent_generator"]
agent_generator_pkg/src/agent_generator/__init__.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ────────────────────────────────────────────────────────────────
2
+ # src/agent_generator/__init__.py
3
+ # ────────────────────────────────────────────────────────────────
4
+ """
5
+ 🎉 **agent‑generator**
6
+
7
+ This package turns plain‑English requirements into ready‑to‑run multi‑agent
8
+ teams for CrewAI, LangGraph, ReAct, or watsonx Orchestrate.
9
+
10
+ Public surface
11
+ --------------
12
+ * `__version__` – semantic‑version string
13
+ * `get_settings()` – singleton accessor to global Settings
14
+ * `Settings` – Pydantic config model (sub‑module import)
15
+ * `FRAMEWORKS` – mapping name → framework generator class
16
+ * `PROVIDERS` – mapping name → LLM provider class
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ # ------------------------------------------------------------------ #
22
+ # Version
23
+ # ------------------------------------------------------------------ #
24
+ __version__: str = "0.2.0"
25
+
26
+ # ------------------------------------------------------------------ #
27
+ # Surface imports (lazy‑safe)
28
+ # ------------------------------------------------------------------ #
29
+ from agent_generator.config import Settings, get_settings # noqa: E402
30
+ from agent_generator.frameworks import FRAMEWORKS # noqa: E402
31
+ from agent_generator.providers import PROVIDERS # noqa: E402
32
+
33
+ # Anything else you’d like to expose at package level goes here
34
+ __all__ = [
35
+ "__version__",
36
+ "Settings",
37
+ "get_settings",
38
+ "PROVIDERS",
39
+ "FRAMEWORKS",
40
+ ]
agent_generator_pkg/src/agent_generator/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (1.19 kB). View file
 
agent_generator_pkg/src/agent_generator/__pycache__/cli.cpython-311.pyc ADDED
Binary file (11.5 kB). View file
 
agent_generator_pkg/src/agent_generator/__pycache__/config.cpython-311.pyc ADDED
Binary file (6.69 kB). View file
 
agent_generator_pkg/src/agent_generator/__pycache__/wsgi.cpython-311.pyc ADDED
Binary file (1.03 kB). View file
 
agent_generator_pkg/src/agent_generator/application/__init__.py ADDED
File without changes
agent_generator_pkg/src/agent_generator/application/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (173 Bytes). View file
 
agent_generator_pkg/src/agent_generator/application/__pycache__/build_service.cpython-311.pyc ADDED
Binary file (5.98 kB). View file
 
agent_generator_pkg/src/agent_generator/application/__pycache__/planning_service.cpython-311.pyc ADDED
Binary file (7.85 kB). View file
 
agent_generator_pkg/src/agent_generator/application/__pycache__/validation_service.cpython-311.pyc ADDED
Binary file (1.76 kB). View file
 
agent_generator_pkg/src/agent_generator/application/build_service.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build service: take a validated ProjectSpec and produce generated files."""
2
+ from __future__ import annotations
3
+
4
+ import ast
5
+ from typing import Any
6
+
7
+ from agent_generator.config import Settings, get_settings_lenient
8
+ from agent_generator.domain.project_spec import ProjectSpec
9
+ from agent_generator.domain.artifact_bundle import ArtifactBundle, GeneratedFile, ValidationIssue
10
+ from agent_generator.frameworks import FRAMEWORKS
11
+
12
+
13
+ def _spec_to_mermaid(spec: ProjectSpec) -> str:
14
+ """Generate a Mermaid diagram directly from a ProjectSpec (no Workflow needed)."""
15
+ lines = ["graph TD"]
16
+ for t in spec.tasks:
17
+ label = t.description[:60].replace('"', "'")
18
+ lines.append(f' {t.id}["{label}"]')
19
+ for t in spec.tasks:
20
+ for dep in t.depends_on:
21
+ lines.append(f" {dep} --> {t.id}")
22
+ return "\n".join(lines)
23
+
24
+
25
+ def build(
26
+ spec: ProjectSpec,
27
+ mcp: bool = False,
28
+ ) -> ArtifactBundle:
29
+ """Build a project from a validated ProjectSpec.
30
+
31
+ Returns an ArtifactBundle with generated files, manifest, warnings, and errors.
32
+ """
33
+ from agent_generator.application.validation_service import validate_artifact
34
+
35
+ framework_name = spec.framework.value
36
+ if framework_name not in FRAMEWORKS:
37
+ framework_name = "crewai"
38
+
39
+ settings = get_settings_lenient()
40
+
41
+ # Generate code directly from spec (no Workflow adapter)
42
+ generator = FRAMEWORKS[framework_name]()
43
+ code = generator.generate_from_spec(spec, settings, mcp=mcp or spec.runtime.mcp_wrapper)
44
+ diagram = _spec_to_mermaid(spec)
45
+
46
+ # Build file list
47
+ project_name = spec.name
48
+ generated_files: list[GeneratedFile] = []
49
+
50
+ ext = generator.file_extension
51
+ if ext == "yaml":
52
+ generated_files.append(GeneratedFile(
53
+ path=f"{project_name}.yaml",
54
+ content=code,
55
+ language="yaml",
56
+ ))
57
+ else:
58
+ generated_files.append(GeneratedFile(
59
+ path=f"src/{project_name}/main.py",
60
+ content=code,
61
+ language="python",
62
+ ))
63
+ generated_files.append(GeneratedFile(
64
+ path=f"src/{project_name}/__init__.py",
65
+ content=f'"""Generated {project_name} package."""\n',
66
+ language="python",
67
+ ))
68
+
69
+ generated_files.append(GeneratedFile(
70
+ path="README.md",
71
+ content=(
72
+ f"# {project_name}\n\n{spec.description}\n\n"
73
+ f"Framework: {framework_name}\n\n"
74
+ f"Generated by [agent-generator](https://github.com/ruslanmv/agent-generator).\n"
75
+ ),
76
+ language="markdown",
77
+ ))
78
+
79
+ # Syntax validation
80
+ errors: list[ValidationIssue] = []
81
+ warnings: list[ValidationIssue] = []
82
+ for f in generated_files:
83
+ if f.path.endswith(".py"):
84
+ try:
85
+ ast.parse(f.content)
86
+ except SyntaxError as e:
87
+ errors.append(ValidationIssue(
88
+ level="error",
89
+ message=f"{f.path}: Syntax error line {e.lineno}: {e.msg}",
90
+ ))
91
+
92
+ # Build manifest
93
+ manifest: dict[str, Any] = {
94
+ "framework": framework_name,
95
+ "diagram": diagram,
96
+ "generator_version": "0.2.0",
97
+ "project_name": project_name,
98
+ }
99
+
100
+ artifact = ArtifactBundle(
101
+ files=generated_files,
102
+ manifest=manifest,
103
+ warnings=warnings,
104
+ errors=errors,
105
+ )
106
+
107
+ # Run security validation
108
+ artifact = validate_artifact(artifact)
109
+
110
+ return artifact
111
+
112
+
113
+ def build_dict(spec: ProjectSpec, mcp: bool = False) -> dict[str, Any]:
114
+ """Build and return a backward-compatible dict (for CLI/web)."""
115
+ artifact = build(spec, mcp=mcp)
116
+ return {
117
+ "files": {f.path: f.content for f in artifact.files},
118
+ "diagram": artifact.manifest.get("diagram", ""),
119
+ "framework": artifact.manifest.get("framework", spec.framework.value),
120
+ "warnings": [w.message for w in artifact.warnings],
121
+ "errors": [e.message for e in artifact.errors],
122
+ "spec": spec.model_dump(),
123
+ "manifest": artifact.manifest,
124
+ "valid": artifact.valid,
125
+ }
agent_generator_pkg/src/agent_generator/application/planning_service.py ADDED
@@ -0,0 +1,152 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Unified planning service used by CLI and Web API."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from typing import Optional
6
+
7
+ from agent_generator.domain.project_spec import (
8
+ AgentSpec,
9
+ ArtifactMode,
10
+ FrameworkChoice,
11
+ LLMSpec,
12
+ ProjectSpec,
13
+ RuntimeSpec,
14
+ TaskSpec,
15
+ ToolSpec,
16
+ )
17
+ from agent_generator.planners.keyword_planner import KeywordPlanner
18
+ from agent_generator.planners.spec_normalizer import SpecNormalizer
19
+ from agent_generator.validators.spec_validator import SpecValidator
20
+
21
+ _keyword_planner = KeywordPlanner()
22
+ _normalizer = SpecNormalizer()
23
+ _validator = SpecValidator()
24
+
25
+
26
+ def _slugify(text: str) -> str:
27
+ slug = re.sub(r"[^a-z0-9\s-]", "", text.lower())
28
+ slug = re.sub(r"[\s_]+", "-", slug.strip())
29
+ return slug[:40] or "agent-project"
30
+
31
+
32
+ def _extract_roles(prompt: str, hints: dict) -> list[dict]:
33
+ """Extract agent roles from keyword hints + prompt analysis."""
34
+ roles = hints.get("suggested_roles", [])
35
+ if not roles:
36
+ roles = ["assistant"]
37
+ agents = []
38
+ for role in roles[:4]:
39
+ agents.append(
40
+ {
41
+ "id": role.replace(" ", "_").lower(),
42
+ "role": role.replace("_", " ").title(),
43
+ "goal": f"Handle {role}-related tasks effectively",
44
+ "backstory": f"An experienced {role} with deep domain expertise.",
45
+ "tools": hints.get("suggested_tools", [])[:2],
46
+ }
47
+ )
48
+ return agents
49
+
50
+
51
+ def _extract_tasks(prompt: str, agents: list[dict]) -> list[dict]:
52
+ """Extract tasks from prompt sentences."""
53
+ sentences = [
54
+ s.strip()
55
+ for s in re.split(r"[.!?]+", prompt)
56
+ if s.strip() and len(s.strip()) > 5
57
+ ]
58
+ if not sentences:
59
+ sentences = [prompt.strip()]
60
+ tasks = []
61
+ for i, sentence in enumerate(sentences[:8]):
62
+ agent_idx = i % len(agents)
63
+ task_id = f"task_{i + 1}"
64
+ tasks.append(
65
+ {
66
+ "id": task_id,
67
+ "description": sentence,
68
+ "agent_id": agents[agent_idx]["id"],
69
+ "expected_output": f"Completed: {sentence[:80]}",
70
+ "depends_on": [f"task_{i}"] if i > 0 else [],
71
+ }
72
+ )
73
+ return tasks
74
+
75
+
76
+ def plan(
77
+ prompt: str,
78
+ framework: Optional[str] = None,
79
+ artifact_mode: Optional[str] = None,
80
+ provider: Optional[str] = None,
81
+ mcp: bool = False,
82
+ use_llm: bool = False,
83
+ ) -> tuple[ProjectSpec, list[str]]:
84
+ """Plan a project from a natural language prompt.
85
+
86
+ Returns (validated_spec, warnings).
87
+ This is the ONLY entry point for creating a ProjectSpec.
88
+ Used by both CLI and Web API.
89
+
90
+ Parameters
91
+ ----------
92
+ use_llm : bool
93
+ If True *and* a provider is given, attempt LLM-based planning
94
+ before falling back to the keyword-based planner.
95
+ """
96
+ # Step 1: keyword classification (always runs -- provides hints)
97
+ hints = _keyword_planner.classify(
98
+ prompt, user_framework=framework, user_artifact_mode=artifact_mode
99
+ )
100
+
101
+ # ── Optional LLM planning stage ──────────────────────────────
102
+ if use_llm and provider:
103
+ try:
104
+ from agent_generator.providers import PROVIDERS
105
+
106
+ if provider in PROVIDERS:
107
+ from agent_generator.config import get_settings_lenient
108
+ from agent_generator.planners.llm_planner import LLMPlanner
109
+
110
+ provider_inst = PROVIDERS[provider](get_settings_lenient())
111
+ llm_planner = LLMPlanner(provider_inst)
112
+ llm_spec = llm_planner.plan(prompt, hints)
113
+ if llm_spec is not None:
114
+ spec, warnings = _normalizer.normalize(llm_spec)
115
+ validation = _validator.validate(spec)
116
+ return spec, warnings + validation.warnings
117
+ except Exception:
118
+ pass # Fall through to keyword-based planning
119
+
120
+ # ── Keyword-based fallback (always works, no credentials) ────
121
+ fw = hints["suggested_framework"]
122
+ mode = hints["suggested_artifact_mode"]
123
+ agents = _extract_roles(prompt, hints)
124
+ tasks = _extract_tasks(prompt, agents)
125
+ tools = [
126
+ {"id": t, "template": t, "inputs": {}}
127
+ for t in hints.get("suggested_tools", [])
128
+ ]
129
+
130
+ spec = ProjectSpec(
131
+ name=_slugify(prompt[:50]),
132
+ description=prompt[:500],
133
+ framework=FrameworkChoice(fw),
134
+ artifact_mode=ArtifactMode(mode),
135
+ llm=LLMSpec(provider=provider or "watsonx"),
136
+ agents=[AgentSpec(**a) for a in agents],
137
+ tasks=[TaskSpec(**t) for t in tasks],
138
+ tools=[ToolSpec(**t) for t in tools],
139
+ runtime=RuntimeSpec(mcp_wrapper=mcp),
140
+ )
141
+
142
+ # Step 3: normalize (fix references, apply defaults)
143
+ spec, norm_warnings = _normalizer.normalize(spec)
144
+
145
+ # Step 4: validate
146
+ validation = _validator.validate(spec)
147
+ all_warnings = norm_warnings + validation.warnings
148
+
149
+ if not validation.valid:
150
+ all_warnings.extend(validation.errors)
151
+
152
+ return spec, all_warnings
agent_generator_pkg/src/agent_generator/application/release_service.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Release service — package artifacts for distribution."""
2
+ from __future__ import annotations
3
+
4
+ from io import BytesIO
5
+ from zipfile import ZipFile, ZIP_DEFLATED
6
+
7
+ from agent_generator.domain.artifact_bundle import ArtifactBundle
8
+
9
+
10
+ def build_zip_bytes(artifact: ArtifactBundle, project_name: str = "project") -> bytes:
11
+ """Create a ZIP archive from an ArtifactBundle."""
12
+ buf = BytesIO()
13
+ with ZipFile(buf, "w", ZIP_DEFLATED) as zf:
14
+ for file in artifact.files:
15
+ zf.writestr(f"{project_name}/{file.path}", file.content)
16
+ return buf.getvalue()
agent_generator_pkg/src/agent_generator/application/validation_service.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Central validation service for specs and artifacts."""
2
+ from __future__ import annotations
3
+
4
+ from agent_generator.domain.project_spec import ProjectSpec
5
+ from agent_generator.domain.artifact_bundle import ArtifactBundle
6
+ from agent_generator.validators.spec_validator import SpecValidator, ValidationResult
7
+ from agent_generator.validators.security_validator import SecurityValidator
8
+
9
+ _spec_validator = SpecValidator()
10
+ _security_validator = SecurityValidator()
11
+
12
+
13
+ def validate_spec(spec: ProjectSpec) -> ValidationResult:
14
+ """Validate a ProjectSpec. Returns ValidationResult with errors/warnings."""
15
+ return _spec_validator.validate(spec)
16
+
17
+
18
+ def validate_artifact(artifact: ArtifactBundle) -> ArtifactBundle:
19
+ """Run security validation on generated artifacts. Mutates and returns the bundle."""
20
+ issues = _security_validator.validate(artifact)
21
+ for issue in issues:
22
+ if issue.level == "error":
23
+ artifact.errors.append(issue)
24
+ else:
25
+ artifact.warnings.append(issue)
26
+ return artifact
agent_generator_pkg/src/agent_generator/cli.py ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ────────────────────────────────────────────────────────────────
2
+ # src/agent_generator/cli.py
3
+ # ────────────────────────────────────────────────────────────────
4
+ """
5
+ Command-line interface for **agent-generator**.
6
+ Core command
7
+ ------------
8
+
9
+ agent-generator "Build me a social-media team" \
10
+ --framework crewai \
11
+ --provider watsonx \
12
+ --output social_team.py --mcp
13
+
14
+ The CLI glues together:
15
+
16
+ * Settings (env + defaults) → `config.py`
17
+ * Prompt parser → `utils.parser`
18
+ * Provider registry → `providers.__init__`
19
+ * Framework generator registry → `frameworks.base`
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ from pathlib import Path
26
+ from typing import Optional
27
+
28
+ import typer
29
+ from dotenv import load_dotenv
30
+ from pydantic_settings import SettingsError
31
+ from rich import print as rprint
32
+ from rich.console import Console
33
+ from rich.syntax import Syntax
34
+
35
+ from agent_generator.application.build_service import build_dict as build_project
36
+ from agent_generator.application.planning_service import plan as plan_project
37
+ from agent_generator.config import Settings, get_settings
38
+ from agent_generator.frameworks import FRAMEWORKS
39
+ from agent_generator.providers import PROVIDERS
40
+
41
+ # 1) Treat the current working directory as the project root
42
+ project_root = Path.cwd()
43
+
44
+ # 2) Load .env **only** from the current working directory
45
+ load_dotenv(dotenv_path=project_root / ".env", override=False)
46
+
47
+ # ────────────────────────────────────────────────────────────────
48
+ # Typer app
49
+ # ────────────────────────────────────────────────────────────────
50
+ app = typer.Typer(
51
+ add_completion=False,
52
+ help="Transform plain English into fully configured multi‑agent teams.",
53
+ )
54
+
55
+ console = Console()
56
+
57
+ VERSION = "0.2.0" # 🛈 bump on release
58
+
59
+
60
+ # ---------------------------------------------------------------- #
61
+ # Helpers
62
+ # ---------------------------------------------------------------- #
63
+ def _validate_choice(value: str, allowed: set[str], name: str) -> str:
64
+ if value not in allowed:
65
+ console.print(
66
+ f"[red]{name} '{value}' is invalid. Options: {sorted(allowed)}[/]"
67
+ )
68
+ raise typer.Exit(code=1)
69
+ return value
70
+
71
+
72
+ def _write_or_echo(text: str, output: Optional[Path]) -> None:
73
+ if output:
74
+ output.write_text(text, encoding="utf-8")
75
+ console.print(f"[bold green]✓ Written to {output}[/]")
76
+ else:
77
+ console.print(
78
+ Syntax(text, "python" if text.lstrip().startswith("from") else "yaml")
79
+ )
80
+
81
+
82
+ # ---------------------------------------------------------------- #
83
+ # generate command
84
+ # ---------------------------------------------------------------- #
85
+ @app.command()
86
+ def generate(
87
+ prompt: str = typer.Argument(..., help="Natural‑language requirement sentence(s)."),
88
+ framework: str = typer.Option(
89
+ ..., "--framework", "-f", help="Target framework.", show_choices=False
90
+ ),
91
+ provider: Optional[str] = typer.Option(
92
+ None, "--provider", "-p", help="LLM provider (defaults to config)."
93
+ ),
94
+ model: Optional[str] = typer.Option(None, help="Override model name."),
95
+ temperature: Optional[float] = typer.Option(None, help="Sampling temperature."),
96
+ max_tokens: Optional[int] = typer.Option(None, help="Max tokens for completion."),
97
+ output: Optional[Path] = typer.Option(
98
+ None,
99
+ "--output",
100
+ "-o",
101
+ help="Write result to file instead of stdout. "
102
+ "File extension inferred from framework if omitted.",
103
+ ),
104
+ mcp: bool = typer.Option(
105
+ False,
106
+ "--mcp/--no-mcp",
107
+ help="Wrap Python output in an MCP FastAPI server.",
108
+ ),
109
+ dry_run: bool = typer.Option(False, "--dry-run", help="Skip LLM call."),
110
+ show_cost: bool = typer.Option(
111
+ False, "--show-cost", help="Display token/cost info."
112
+ ),
113
+ version: bool = typer.Option(False, "--version", "-V", is_eager=True),
114
+ ):
115
+ """Generate code (or YAML) for a multi‑agent workflow."""
116
+ if version:
117
+ rprint(f"[bold]agent‑generator {VERSION}[/]")
118
+ raise typer.Exit()
119
+
120
+ # ────────────── Pre‑flight env check ──────────────
121
+ provider_name = provider or os.getenv("AGENTGEN_PROVIDER", "watsonx")
122
+ required: list[str] = []
123
+ if provider_name == "watsonx":
124
+ required = ["WATSONX_API_KEY", "WATSONX_PROJECT_ID"]
125
+ elif provider_name == "openai":
126
+ required = ["OPENAI_API_KEY"]
127
+
128
+ missing = [v for v in required if not os.getenv(v)]
129
+ if missing:
130
+ console.print(
131
+ f"\n[red]⚠️ Missing environment variables for provider '{provider_name}':[/red]\n"
132
+ + "\n".join(f" • {v}" for v in missing)
133
+ + "\n\nPlease set them (or add to your .env), for example:\n"
134
+ + (
135
+ " export WATSONX_API_KEY=…\n"
136
+ " export WATSONX_PROJECT_ID=…\n"
137
+ " export WATSONX_URL=https://us-south.ml.cloud.ibm.com\n"
138
+ if provider_name == "watsonx"
139
+ else " export OPENAI_API_KEY=sk-…\n"
140
+ )
141
+ )
142
+ raise typer.Exit(code=1)
143
+
144
+ # ───────── Load defaults and catch missing env vars ─────────
145
+ try:
146
+ defaults = get_settings()
147
+ except SettingsError as e:
148
+ console.print(f"\n[red]⚠️ Configuration error:[/red]\n{e}\n")
149
+ raise typer.Exit(code=1)
150
+
151
+ # ───────── Validate choices ─────────
152
+ framework = _validate_choice(framework, set(FRAMEWORKS), "Framework")
153
+ _provider_name = provider or defaults.provider
154
+ _provider_name = _validate_choice(_provider_name, set(PROVIDERS), "Provider")
155
+
156
+ # ───────── Plan (spec-first pipeline) ─────────
157
+ spec, plan_warnings = plan_project(
158
+ prompt, framework=framework, provider=provider_name, mcp=mcp,
159
+ )
160
+ if plan_warnings:
161
+ for w in plan_warnings:
162
+ console.print(f"[yellow]⚠ {w}[/]")
163
+
164
+ # ───────── Dry‑run shortcut ─────────
165
+ if dry_run:
166
+ console.print(
167
+ "[yellow]Dry‑run only → generating code without LLM calls or env checks.[/]"
168
+ )
169
+ result = build_project(spec, mcp=mcp)
170
+ # Pick the main generated file
171
+ code = ""
172
+ for path, content in result["files"].items():
173
+ if path.endswith(".py") or path.endswith(".yaml"):
174
+ if "main.py" in path or path.endswith(".yaml"):
175
+ code = content
176
+ break
177
+ if not code:
178
+ code = next(iter(result["files"].values()), "")
179
+ _write_or_echo(code, output)
180
+ return
181
+
182
+ # ───────── Load mutable settings ─────────
183
+ try:
184
+ settings = Settings(
185
+ provider=_provider_name,
186
+ model=model or defaults.model,
187
+ temperature=temperature or defaults.temperature,
188
+ max_tokens=max_tokens or defaults.max_tokens,
189
+ )
190
+ except SettingsError as e:
191
+ console.print(f"\n[red]⚠️ Configuration error:[/red]\n{e}\n")
192
+ raise typer.Exit(code=1)
193
+
194
+ # ───────── Build from spec ─────────
195
+ result = build_project(spec, mcp=mcp)
196
+
197
+ # Check for build errors
198
+ if result.get("errors"):
199
+ for err in result["errors"]:
200
+ console.print(f"[red]ERROR:[/] {err}")
201
+ if any("unsafe" in e.lower() or "forbidden" in e.lower() for e in result["errors"]):
202
+ console.print("[red]Build blocked due to security violations.[/]")
203
+ raise typer.Exit(code=1)
204
+
205
+ # Pick the main generated file
206
+ code = ""
207
+ for path, content in result["files"].items():
208
+ if path.endswith(".py") or path.endswith(".yaml"):
209
+ if "main.py" in path or path.endswith(".yaml"):
210
+ code = content
211
+ break
212
+ if not code:
213
+ code = next(iter(result["files"].values()), "")
214
+
215
+ # ───────── Render prompt + call LLM (optional enhancement) ─────────
216
+ provider_cls = PROVIDERS[_provider_name]
217
+ try:
218
+ provider_inst = provider_cls(settings)
219
+ except ImportError as e:
220
+ console.print(f"\n[red]⚠️ {e}[/red]\n")
221
+ raise typer.Exit(code=1)
222
+
223
+ # ───────── Cost estimate ─────────
224
+ if show_cost:
225
+ ptok = provider_inst.tokenize(prompt)
226
+ ctok = provider_inst.tokenize(code)
227
+ cost = provider_inst.estimate_cost(ptok, ctok)
228
+ console.print(
229
+ f"[cyan]≈ prompt_tokens={ptok}, completion_tokens={ctok}, "
230
+ f"est. cost=${cost:.4f}[/]"
231
+ )
232
+
233
+ # ───────── Output ─────────
234
+ _write_or_echo(code, output)
235
+
236
+
237
+ # ---------------------------------------------------------------- #
238
+ # Entry point for `python -m agent_generator.cli`
239
+ # ---------------------------------------------------------------- #
240
+ def _main() -> None: # noqa: D401
241
+ app()
242
+
243
+
244
+ if __name__ == "__main__":
245
+ _main()
agent_generator_pkg/src/agent_generator/config.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ from pathlib import Path
5
+ from typing import Literal, Optional
6
+
7
+ from dotenv import load_dotenv
8
+ from pydantic import Field, field_validator, model_validator
9
+ from pydantic_settings import BaseSettings, SettingsConfigDict, SettingsError
10
+
11
+ # ────────────────────────────────────────────────────────────────
12
+ # Immediately load .env from the current working directory
13
+ # ────────────────────────────────────────────────────────────────
14
+ load_dotenv(dotenv_path=Path.cwd() / ".env", override=False)
15
+
16
+
17
+ # ────────────────────────────────────────────────────────────────
18
+ # Settings model
19
+ # ────────────────────────────────────────────────────────────────
20
+ class Settings(BaseSettings):
21
+ """
22
+ Runtime configuration for *agent-generator*.
23
+
24
+ Environment variables with the prefix **AGENTGEN_** are read automatically,
25
+ but none are required.
26
+ If **AGENTGEN_PROVIDER** is *omitted*, the provider defaults to **watsonx**.
27
+ """
28
+
29
+ model_config = SettingsConfigDict(
30
+ env_prefix="AGENTGEN_",
31
+ env_file=str(Path.cwd() / ".env"),
32
+ case_sensitive=False,
33
+ extra="ignore",
34
+ )
35
+
36
+ # LLM provider - default is *watsonx*
37
+ provider: Literal["watsonx", "openai"] = Field(
38
+ default="watsonx",
39
+ description="Default LLM provider; can be overridden by CLI flag or env.",
40
+ )
41
+
42
+ # Global model + sampling
43
+ model: str = Field(
44
+ default="meta-llama/llama-3-3-70b-instruct",
45
+ description="Model identifier understood by the chosen provider.",
46
+ )
47
+ temperature: float = Field(default=0.7, ge=0, le=2)
48
+ max_tokens: int = Field(default=4096, ge=16)
49
+
50
+ # Provider-specific model overrides (use validation_alias for env mapping)
51
+ watsonx_model: Optional[str] = Field(
52
+ default=None, validation_alias="WATSONX_MODEL"
53
+ )
54
+ openai_model: Optional[str] = Field(
55
+ default=None, validation_alias="OPENAI_MODEL"
56
+ )
57
+
58
+ # Credentials
59
+ watsonx_api_key: Optional[str] = Field(
60
+ default=None, validation_alias="WATSONX_API_KEY"
61
+ )
62
+ watsonx_project_id: Optional[str] = Field(
63
+ default=None, validation_alias="WATSONX_PROJECT_ID"
64
+ )
65
+ watsonx_url: str = Field(
66
+ default="https://us-south.ml.cloud.ibm.com",
67
+ validation_alias="WATSONX_URL",
68
+ )
69
+ openai_api_key: Optional[str] = Field(
70
+ default=None, validation_alias="OPENAI_API_KEY"
71
+ )
72
+
73
+ # Misc
74
+ log_level: str = Field(default="INFO")
75
+ mcp_default_port: int = Field(default=8080, ge=1, le=65535)
76
+
77
+ # Skip credential validation (for dry-run / testing)
78
+ skip_credential_check: bool = Field(default=False)
79
+
80
+ # ──────────────────────────────────────────────────
81
+ # Validators
82
+ # ──────────────────────────────────────────────────
83
+ @field_validator("log_level", mode="before")
84
+ @classmethod
85
+ def _normalise_log_level(cls, v: str) -> str:
86
+ v_up = v.upper()
87
+ if v_up not in {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}:
88
+ raise ValueError(
89
+ "log_level must be one of DEBUG | INFO | WARNING | ERROR | CRITICAL"
90
+ )
91
+ return v_up
92
+
93
+ @model_validator(mode="after")
94
+ def _apply_overrides_and_check(self):
95
+ # Apply provider-specific model override
96
+ if self.provider == "watsonx" and self.watsonx_model:
97
+ object.__setattr__(self, "model", self.watsonx_model)
98
+
99
+ # For OpenAI, use explicit override or default to gpt-4o
100
+ if self.provider == "openai":
101
+ if self.openai_model:
102
+ object.__setattr__(self, "model", self.openai_model)
103
+ else:
104
+ object.__setattr__(self, "model", "gpt-4o")
105
+
106
+ # Skip credential checks if requested
107
+ if self.skip_credential_check:
108
+ return self
109
+
110
+ # Credential checks with copy-pasteable guidance
111
+ if self.provider == "openai":
112
+ if not self.openai_api_key:
113
+ raise SettingsError(
114
+ "OPENAI_API_KEY is required.\n"
115
+ "Run pip install 'agent-generator[openai]' to enable OpenAI support.\n"
116
+ "Set it in your environment or .env file, e.g.:\n"
117
+ " export OPENAI_API_KEY=sk-your-key-here"
118
+ )
119
+
120
+ if self.provider == "watsonx":
121
+ missing = []
122
+ if not self.watsonx_api_key:
123
+ missing.append("WATSONX_API_KEY")
124
+ if not self.watsonx_project_id:
125
+ missing.append("WATSONX_PROJECT_ID")
126
+ if missing:
127
+ raise SettingsError(
128
+ "Watsonx credentials missing: " + ", ".join(missing) + ".\n"
129
+ "Set the required environment variables (example):\n"
130
+ " export WATSONX_API_KEY=...\n"
131
+ " export WATSONX_PROJECT_ID=...\n"
132
+ " export WATSONX_URL=https://us-south.ml.cloud.ibm.com\n"
133
+ "Or add the same keys to your .env file."
134
+ )
135
+ return self
136
+
137
+ # Convenience flags
138
+ @property
139
+ def is_watsonx(self) -> bool:
140
+ return self.provider == "watsonx"
141
+
142
+ @property
143
+ def is_openai(self) -> bool:
144
+ return self.provider == "openai"
145
+
146
+
147
+ # ────────────────────────────────────────────────
148
+ # Singleton accessor
149
+ # ────────────────────────────────────────────────
150
+ @functools.lru_cache(maxsize=1)
151
+ def get_settings() -> Settings:
152
+ """Return a *cached* Settings instance (lazy `.env` read)."""
153
+ return Settings()
154
+
155
+
156
+ def get_settings_lenient() -> Settings:
157
+ """Return Settings without credential validation (for parsing/planning)."""
158
+ return Settings(skip_credential_check=True)
agent_generator_pkg/src/agent_generator/data/default_prompts.yaml ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ────────────────────────────────────────────────────────────────
2
+ # default_prompts.yaml — reusable prompt fragments
3
+ # ────────────────────────────────────────────────────────────────
4
+ #
5
+ # These snippets are injected by utils/prompts.py when building the
6
+ # provider‑specific prompt. Edit to fine‑tune tone or add few‑shot
7
+ # examples for better planning / code style consistency.
8
+ #
9
+ # Structure:
10
+ # system : merged at the top of every prompt
11
+ # user : optional user examples
12
+ # assistant: optional assistant examples
13
+ # ----------------------------------------------------------------
14
+
15
+ system: |
16
+ You are an expert software engineer specialising in multi‑agent AI
17
+ systems. Generate clear, idiomatic code and follow PEP‑8 style rules.
18
+ Respond **only** with code blocks unless explicitly asked for plain text.
19
+
20
+ user: |
21
+ I need a team of agents that will research the latest AI trends and
22
+ write a brief executive summary for C‑level readers.
23
+
24
+ assistant: |
25
+ ```python
26
+ # Auto‑generated CrewAI script (abbreviated)
27
+ from crewai import Agent, Task, Crew
28
+ researcher = Agent(...)
29
+ writer = Agent(...)
30
+ ...
31
+
agent_generator_pkg/src/agent_generator/domain/__init__.py ADDED
File without changes
agent_generator_pkg/src/agent_generator/domain/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (168 Bytes). View file
 
agent_generator_pkg/src/agent_generator/domain/__pycache__/artifact_bundle.cpython-311.pyc ADDED
Binary file (2.8 kB). View file
 
agent_generator_pkg/src/agent_generator/domain/__pycache__/capability_matrix.cpython-311.pyc ADDED
Binary file (2.51 kB). View file
 
agent_generator_pkg/src/agent_generator/domain/__pycache__/project_spec.cpython-311.pyc ADDED
Binary file (6.62 kB). View file
 
agent_generator_pkg/src/agent_generator/domain/__pycache__/render_plan.cpython-311.pyc ADDED
Binary file (1.87 kB). View file
 
agent_generator_pkg/src/agent_generator/domain/artifact_bundle.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Typed output contract for generated projects."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+
9
+ class GeneratedFile(BaseModel):
10
+ """A single generated file in the artifact bundle."""
11
+ path: str = Field(..., description="Relative file path.")
12
+ content: str = Field(..., description="File content.")
13
+ language: str | None = Field(default=None, description="Language hint for syntax highlighting.")
14
+
15
+
16
+ class ValidationIssue(BaseModel):
17
+ """A validation finding attached to a build."""
18
+ level: Literal["info", "warning", "error"] = "warning"
19
+ message: str = ""
20
+
21
+
22
+ class ArtifactBundle(BaseModel):
23
+ """Complete typed output of a build operation."""
24
+ files: list[GeneratedFile] = Field(default_factory=list)
25
+ manifest: dict[str, Any] = Field(default_factory=dict)
26
+ warnings: list[ValidationIssue] = Field(default_factory=list)
27
+ errors: list[ValidationIssue] = Field(default_factory=list)
28
+
29
+ @property
30
+ def valid(self) -> bool:
31
+ return not any(e.level == "error" for e in self.errors)
agent_generator_pkg/src/agent_generator/domain/capability_matrix.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Framework capability matrix -- what each framework supports."""
2
+ from __future__ import annotations
3
+
4
+ CAPABILITY_MATRIX: dict[str, dict[str, bool]] = {
5
+ "crewai": {
6
+ "code_only": True,
7
+ "yaml_only": True,
8
+ "code_and_yaml": True,
9
+ "tool_templates": True,
10
+ "mcp_wrapper": True,
11
+ },
12
+ "crewai_flow": {
13
+ "code_only": True,
14
+ "yaml_only": False,
15
+ "code_and_yaml": False,
16
+ "tool_templates": True,
17
+ "mcp_wrapper": True,
18
+ },
19
+ "langgraph": {
20
+ "code_only": True,
21
+ "yaml_only": False,
22
+ "code_and_yaml": False,
23
+ "tool_templates": True,
24
+ "mcp_wrapper": True,
25
+ },
26
+ "react": {
27
+ "code_only": True,
28
+ "yaml_only": False,
29
+ "code_and_yaml": False,
30
+ "tool_templates": True,
31
+ "mcp_wrapper": True,
32
+ },
33
+ "watsonx_orchestrate": {
34
+ "code_only": False,
35
+ "yaml_only": True,
36
+ "code_and_yaml": False,
37
+ "tool_templates": False,
38
+ "mcp_wrapper": False,
39
+ },
40
+ }
41
+
42
+
43
+ def validate_combination(framework: str, mode: str) -> bool:
44
+ """Return True if the given artifact mode is supported by the framework."""
45
+ return CAPABILITY_MATRIX.get(framework, {}).get(mode, False)
46
+
47
+
48
+ def supported_modes(framework: str) -> list[str]:
49
+ """Return the list of supported artifact modes for a framework."""
50
+ caps = CAPABILITY_MATRIX.get(framework, {})
51
+ return [
52
+ k
53
+ for k, v in caps.items()
54
+ if v and k in ("code_only", "yaml_only", "code_and_yaml")
55
+ ]
56
+
57
+
58
+ def supports_tools(framework: str) -> bool:
59
+ """Return True if the framework supports tool templates."""
60
+ return CAPABILITY_MATRIX.get(framework, {}).get("tool_templates", False)
61
+
62
+
63
+ def supports_mcp(framework: str) -> bool:
64
+ """Return True if the framework supports MCP wrapping."""
65
+ return CAPABILITY_MATRIX.get(framework, {}).get("mcp_wrapper", False)
agent_generator_pkg/src/agent_generator/domain/project_spec.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical project specification -- the single source of truth between planning and rendering."""
2
+ from __future__ import annotations
3
+
4
+ from enum import Enum
5
+ from typing import Optional
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class FrameworkChoice(str, Enum):
11
+ CREWAI = "crewai"
12
+ CREWAI_FLOW = "crewai_flow"
13
+ LANGGRAPH = "langgraph"
14
+ REACT = "react"
15
+ WATSONX_ORCHESTRATE = "watsonx_orchestrate"
16
+
17
+
18
+ class ArtifactMode(str, Enum):
19
+ CODE_ONLY = "code_only"
20
+ YAML_ONLY = "yaml_only"
21
+ CODE_AND_YAML = "code_and_yaml"
22
+
23
+
24
+ class AgentSpec(BaseModel):
25
+ """Specification for a single agent in the project."""
26
+
27
+ id: str = Field(..., description="Unique agent identifier (snake_case).")
28
+ role: str = Field(..., description="Agent role name.")
29
+ goal: str = Field(..., description="What this agent tries to achieve.")
30
+ backstory: str = Field(default="", description="Background context for the agent.")
31
+ tools: list[str] = Field(default_factory=list, description="Tool IDs this agent can use.")
32
+ llm_override: Optional[str] = Field(
33
+ default=None, description="Override LLM model for this agent."
34
+ )
35
+
36
+
37
+ class TaskSpec(BaseModel):
38
+ """Specification for a single task in the project."""
39
+
40
+ id: str = Field(..., description="Unique task identifier (snake_case).")
41
+ description: str = Field(..., description="What this task does.")
42
+ agent_id: str = Field(..., description="ID of the agent responsible.")
43
+ expected_output: str = Field(..., description="What the task should produce.")
44
+ depends_on: list[str] = Field(
45
+ default_factory=list, description="IDs of prerequisite tasks."
46
+ )
47
+
48
+
49
+ class ToolSpec(BaseModel):
50
+ """Specification for a tool pulled from the tool catalog."""
51
+
52
+ id: str = Field(..., description="Unique tool identifier.")
53
+ template: str = Field(..., description="Key in the tool catalog.")
54
+ inputs: dict[str, str] = Field(
55
+ default_factory=dict, description="Template variable overrides."
56
+ )
57
+
58
+
59
+ class LLMSpec(BaseModel):
60
+ """LLM provider and model configuration."""
61
+
62
+ provider: str = Field(default="watsonx")
63
+ model: str = Field(default="meta-llama/llama-3-3-70b-instruct")
64
+
65
+
66
+ class RuntimeSpec(BaseModel):
67
+ """Runtime options for the generated project."""
68
+
69
+ serve_api: bool = Field(default=False)
70
+ mcp_wrapper: bool = Field(default=False)
71
+ mcp_port: int = Field(default=8080, ge=1, le=65535)
72
+
73
+
74
+ class ProjectSpec(BaseModel):
75
+ """Canonical specification for a multi-agent project."""
76
+
77
+ name: str = Field(
78
+ ...,
79
+ description="Project slug (kebab-case).",
80
+ pattern=r"^[a-z0-9][a-z0-9-]*$",
81
+ )
82
+ version: str = Field(default="1.0", description="Schema version.")
83
+ template_tier: str = Field(default="production", description="Template tier: starter or production.")
84
+ metadata: dict[str, str] = Field(default_factory=dict, description="Arbitrary metadata tags.")
85
+ description: str = Field(
86
+ ..., description="One-line project description.", max_length=500
87
+ )
88
+ framework: FrameworkChoice
89
+ artifact_mode: ArtifactMode = Field(default=ArtifactMode.CODE_ONLY)
90
+ llm: LLMSpec = Field(default_factory=LLMSpec)
91
+ agents: list[AgentSpec] = Field(..., min_length=1)
92
+ tasks: list[TaskSpec] = Field(..., min_length=1)
93
+ tools: list[ToolSpec] = Field(default_factory=list)
94
+ runtime: RuntimeSpec = Field(default_factory=RuntimeSpec)
agent_generator_pkg/src/agent_generator/domain/render_plan.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Deterministic plan of which files to generate."""
2
+ from __future__ import annotations
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class ArtifactEntry(BaseModel):
8
+ """A single file to be rendered from a Jinja2 template."""
9
+
10
+ path: str = Field(..., description="Relative file path in the output project.")
11
+ template: str = Field(..., description="Jinja2 template name to render.")
12
+ language: str = Field(
13
+ default="python", description="File language for syntax highlighting."
14
+ )
15
+
16
+
17
+ class RenderPlan(BaseModel):
18
+ """Complete manifest of artifacts, dependencies, and framework version."""
19
+
20
+ artifacts: list[ArtifactEntry] = Field(default_factory=list)
21
+ dependencies: list[str] = Field(
22
+ default_factory=list, description="pip packages needed."
23
+ )
24
+ framework_version: str = Field(
25
+ default="", description="Framework pip specifier."
26
+ )
agent_generator_pkg/src/agent_generator/frameworks/__init__.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ────────────────────────────────────────────────────────────────
2
+ # src/agent_generator/frameworks/__init__.py
3
+ # ────────────────────────────────────────────────────────────────
4
+ """
5
+ Framework generators registry.
6
+
7
+ Each import below registers its subclass in the FRAMEWORKS dict
8
+ via BaseFrameworkGenerator.__init_subclass__.
9
+ """
10
+
11
+ from .base import FRAMEWORKS, BaseFrameworkGenerator
12
+ from .crewai import CrewAIGenerator
13
+ from .crewai_flow import CrewAIFlowGenerator
14
+ from .langgraph import LangGraphGenerator
15
+ from .react import ReActGenerator
16
+ from .watsonx_orchestrate import WatsonXOrchestrateGenerator
17
+
18
+ __all__ = [
19
+ "BaseFrameworkGenerator",
20
+ "FRAMEWORKS",
21
+ "CrewAIGenerator",
22
+ "CrewAIFlowGenerator",
23
+ "LangGraphGenerator",
24
+ "ReActGenerator",
25
+ "WatsonXOrchestrateGenerator",
26
+ ]
agent_generator_pkg/src/agent_generator/frameworks/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (851 Bytes). View file
 
agent_generator_pkg/src/agent_generator/frameworks/__pycache__/base.cpython-311.pyc ADDED
Binary file (8.85 kB). View file
 
agent_generator_pkg/src/agent_generator/frameworks/base.py ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ────────────────────────────────────────────────────────────────
2
+ # src/agent_generator/frameworks/base.py
3
+ # ────────────────────────────────────────────────────────────────
4
+ """
5
+ Base class + registry for every code-generation backend.
6
+
7
+ Concrete generators live in sub-packages (crewai, langgraph, ...) and must
8
+ implement ``_emit_core_code(workflow, settings)``.
9
+
10
+ Two public entry points:
11
+
12
+ * ``generate_from_spec(spec, settings)`` -- preferred v0.2 path, accepts
13
+ a ``ProjectSpec`` directly.
14
+ * ``generate_code(workflow, settings)`` -- legacy path kept for backward
15
+ compatibility; accepts a ``Workflow`` object.
16
+
17
+ Both handle the cross-cutting MCP server scaffold when ``mcp=True`` and
18
+ the output language is Python.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import textwrap
24
+ from abc import ABC, abstractmethod
25
+ from typing import ClassVar, Dict, Type
26
+
27
+ from agent_generator.config import Settings
28
+ from agent_generator.domain.project_spec import ProjectSpec
29
+ from agent_generator.models.workflow import Workflow
30
+
31
+ # Public registry populated via BaseFrameworkGenerator.__init_subclass__
32
+ FRAMEWORKS: Dict[str, Type["BaseFrameworkGenerator"]] = {}
33
+
34
+
35
+ # ────────────────────────────────────────────────
36
+ # Abstract base
37
+ # ────────────────────────────────────────────────
38
+
39
+
40
+ class BaseFrameworkGenerator(ABC):
41
+ """
42
+ Abstract interface every framework generator must implement.
43
+
44
+ Subclass attributes
45
+ -------------------
46
+ framework : str
47
+ Registry key (e.g. "crewai").
48
+ file_extension : str
49
+ "py" or "yaml". Dictates whether MCP wrapping is allowed.
50
+ """
51
+
52
+ framework: ClassVar[str] = "base"
53
+ file_extension: ClassVar[str] = "txt"
54
+
55
+ # Registry magic
56
+ def __init_subclass__(cls, **kwargs):
57
+ super().__init_subclass__(**kwargs)
58
+ if cls.framework == "base":
59
+ # Don't register the base class itself
60
+ return
61
+ if cls.framework in FRAMEWORKS:
62
+ raise RuntimeError(f"Duplicate framework key: {cls.framework}")
63
+ FRAMEWORKS[cls.framework] = cls
64
+
65
+ # ── New preferred API (v0.2) ──────────────────────────────────
66
+
67
+ def generate_from_spec(
68
+ self,
69
+ spec: ProjectSpec,
70
+ settings: Settings,
71
+ *,
72
+ mcp: bool = False,
73
+ ) -> str:
74
+ """Generate code directly from a ProjectSpec (preferred path)."""
75
+ core_code = self._emit_from_spec(spec, settings).rstrip() + "\n"
76
+
77
+ if mcp and self.file_extension == "py":
78
+ core_code = self._wrap_mcp_server(core_code, settings.mcp_default_port)
79
+
80
+ return core_code
81
+
82
+ def _emit_from_spec(self, spec: ProjectSpec, settings: Settings) -> str:
83
+ """Override in subclass for native ProjectSpec support.
84
+
85
+ Default falls back to legacy Workflow adapter.
86
+ """
87
+ from agent_generator.models.agent import Agent, LLMConfig, Tool
88
+ from agent_generator.models.task import Task
89
+ from agent_generator.models.workflow import Workflow as WF
90
+ from agent_generator.models.workflow import WorkflowEdge
91
+
92
+ agents = [
93
+ Agent(
94
+ id=a.id,
95
+ role=a.role,
96
+ tools=[Tool(name=t, description=f"Tool: {t}") for t in a.tools],
97
+ llm=LLMConfig(provider=spec.llm.provider, model=spec.llm.model),
98
+ )
99
+ for a in spec.agents
100
+ ]
101
+ tasks = [
102
+ Task(
103
+ id=t.id,
104
+ goal=t.description,
105
+ inputs=list(t.depends_on),
106
+ outputs=[t.expected_output] if t.expected_output else [],
107
+ agent_id=t.agent_id,
108
+ )
109
+ for t in spec.tasks
110
+ ]
111
+ edges = [
112
+ WorkflowEdge(source=dep, target=t.id)
113
+ for t in spec.tasks
114
+ for dep in t.depends_on
115
+ ]
116
+ workflow = WF(agents=agents, tasks=tasks, edges=edges)
117
+ return self._emit_core_code(workflow, settings)
118
+
119
+ # ── Legacy API ────────────────────────────────────────────────
120
+
121
+ def generate_code(
122
+ self,
123
+ workflow: Workflow,
124
+ settings: Settings,
125
+ *,
126
+ mcp: bool = False,
127
+ ) -> str:
128
+ """Legacy API -- generate from Workflow object."""
129
+ core_code = self._emit_core_code(workflow, settings).rstrip() + "\n"
130
+
131
+ if mcp and self.file_extension == "py":
132
+ core_code = self._wrap_mcp_server(core_code, settings.mcp_default_port)
133
+
134
+ return core_code
135
+
136
+ @abstractmethod
137
+ def _emit_core_code(self, workflow: Workflow, settings: Settings) -> str:
138
+ """Legacy: framework-specific generation from Workflow."""
139
+
140
+ @staticmethod
141
+ def _wrap_mcp_server(code: str, port: int) -> str:
142
+ """
143
+ Append a tiny FastAPI MCP server so the file can be dockerised and
144
+ registered in an MCP Gateway.
145
+
146
+ Expects the core code to expose `main()` returning serialisable output.
147
+ """
148
+ wrapper = textwrap.dedent(
149
+ f"""
150
+ # ──────────────────────────────────────────────────────
151
+ # MCP HTTP wrapper (auto-generated)
152
+ # ──────────────────────────────────────────────────────
153
+ if __name__ == "__main__":
154
+ import json
155
+ from fastapi import FastAPI
156
+ from fastapi.middleware.cors import CORSMiddleware
157
+ import uvicorn
158
+
159
+ app = FastAPI(title="agent-generator MCP skill")
160
+
161
+ app.add_middleware(
162
+ CORSMiddleware,
163
+ allow_origins=["*"],
164
+ allow_credentials=True,
165
+ allow_methods=["*"],
166
+ allow_headers=["*"],
167
+ )
168
+
169
+ @app.post("/invoke")
170
+ async def invoke():
171
+ result = main() # type: ignore[name-defined]
172
+ return json.loads(json.dumps(result, default=str))
173
+
174
+ uvicorn.run(app, host="0.0.0.0", port={port})
175
+ """
176
+ ).lstrip()
177
+
178
+ return f"{code}\n{wrapper}"
179
+
180
+
181
+ __all__ = ["BaseFrameworkGenerator", "FRAMEWORKS"]
agent_generator_pkg/src/agent_generator/frameworks/crewai/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CrewAI framework plug‑in.
3
+
4
+ Example
5
+ -------
6
+ from agent_generator.frameworks import FRAMEWORKS
7
+ py_src = FRAMEWORKS["crewai"]().generate_code(workflow, settings, mcp=True)
8
+ """
9
+
10
+ from .generator import CrewAIGenerator # noqa: F401
11
+
12
+ __all__ = ["CrewAIGenerator"]
agent_generator_pkg/src/agent_generator/frameworks/crewai/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (478 Bytes). View file
 
agent_generator_pkg/src/agent_generator/frameworks/crewai/__pycache__/generator.cpython-311.pyc ADDED
Binary file (4.08 kB). View file
 
agent_generator_pkg/src/agent_generator/frameworks/crewai/generator.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import textwrap
4
+
5
+ from jinja2 import Template
6
+
7
+ from agent_generator.config import Settings
8
+ from agent_generator.frameworks.base import BaseFrameworkGenerator
9
+ from agent_generator.models.workflow import Workflow
10
+
11
+ _CREWAI_TEMPLATE = Template(
12
+ textwrap.dedent(
13
+ '''
14
+ """Auto-generated CrewAI project (crewai 1.x)."""
15
+
16
+ from crewai import Agent as CrewAgent, Task as CrewTask, Crew, Process
17
+ from typing import Any
18
+
19
+ # ─────────────────────────────────────────────────────────
20
+ # Agents
21
+ # ─────────────────────────────────────────────────────────
22
+ {% for agent in agents %}
23
+ {{ agent.id }} = CrewAgent(
24
+ role="{{ agent.role }}",
25
+ goal="{{ agent.role }} - achieve assigned objectives efficiently",
26
+ backstory="An experienced {{ agent.role | lower }} with deep domain expertise.",
27
+ verbose=True,
28
+ allow_delegation=False,
29
+ {%- if agent.tools %}
30
+ tools=[{{ agent.tools | map(attribute="name") | join(", ") }}],
31
+ {%- endif %}
32
+ )
33
+ {% endfor %}
34
+
35
+ # ─────────────────────────────────────────────────────────
36
+ # Tasks
37
+ # ─────────────────────────────────────────────────────────
38
+ {% for task in tasks %}
39
+ {{ task.id }} = CrewTask(
40
+ description="""{{ task.goal }}""",
41
+ agent={{ task.agent_id }},
42
+ expected_output="{{ task.outputs[0] if task.outputs else task.goal }}",
43
+ )
44
+ {% endfor %}
45
+
46
+ # ─────────────────────────────────────────────────────────
47
+ # Crew assembly
48
+ # ─────────────────────────────────────────────────────────
49
+ crew = Crew(
50
+ agents=[{% for agent in agents %}{{ agent.id }}, {% endfor %}],
51
+ tasks=[{% for task in tasks %}{{ task.id }}, {% endfor %}],
52
+ process=Process.sequential,
53
+ verbose=True,
54
+ )
55
+
56
+
57
+ def main() -> Any:
58
+ """Entry-point for MCP wrapper or direct execution."""
59
+ return crew.kickoff()
60
+
61
+
62
+ if __name__ == "__main__":
63
+ result = main()
64
+ print(result)
65
+ '''
66
+ ).strip("\n"),
67
+ trim_blocks=True,
68
+ lstrip_blocks=True,
69
+ )
70
+
71
+
72
+ class CrewAIGenerator(BaseFrameworkGenerator):
73
+ framework = "crewai"
74
+ file_extension = "py"
75
+
76
+ def _emit_core_code(self, workflow: Workflow, settings: Settings) -> str:
77
+ return (
78
+ _CREWAI_TEMPLATE.render(
79
+ agents=workflow.agents,
80
+ tasks=workflow.tasks,
81
+ )
82
+ + "\n"
83
+ )
agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """
2
+ CrewAI Flow framework plug‑in.
3
+ """
4
+
5
+ from .generator import CrewAIFlowGenerator # noqa: F401
6
+
7
+ __all__ = ["CrewAIFlowGenerator"]
agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (342 Bytes). View file
 
agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/__pycache__/generator.cpython-311.pyc ADDED
Binary file (5.69 kB). View file
 
agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/generator.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import textwrap
4
+
5
+ from jinja2 import Template
6
+
7
+ from agent_generator.config import Settings
8
+ from agent_generator.frameworks.base import BaseFrameworkGenerator
9
+ from agent_generator.models.workflow import Workflow
10
+
11
+ _FLOW_TEMPLATE = Template(
12
+ textwrap.dedent(
13
+ '''
14
+ """Auto-generated CrewAI Flow pipeline (crewai 1.x)."""
15
+
16
+ from crewai import Agent as CrewAgent, Task as CrewTask, Crew, Process
17
+ from crewai.flow.flow import Flow, listen, start
18
+ from pydantic import BaseModel, Field
19
+ from typing import Any
20
+
21
+ # ─────────────────────────────────────────────────────────
22
+ # Shared state
23
+ # ─────────────────────────────────────────────────────────
24
+
25
+ class FlowState(BaseModel):
26
+ """State passed between flow steps."""
27
+ results: dict[str, Any] = Field(default_factory=dict)
28
+ current_step: str = ""
29
+
30
+ # ─────────────────────────────────────────────────────────
31
+ # Agents
32
+ # ─────────────────────────────────────────────────────────
33
+ {% for agent in agents %}
34
+ {{ agent.id }} = CrewAgent(
35
+ role="{{ agent.role }}",
36
+ goal="{{ agent.role }} - complete assigned objectives",
37
+ backstory="An experienced {{ agent.role | lower }} with domain expertise.",
38
+ verbose=True,
39
+ )
40
+ {% endfor %}
41
+
42
+ # ─────────────────────────────────────────────────────────
43
+ # Flow definition
44
+ # ─────────────────────────────────────────────────────────
45
+
46
+ class WorkflowFlow(Flow[FlowState]):
47
+ """Multi-step flow orchestrating agent tasks."""
48
+
49
+ @start()
50
+ def begin(self):
51
+ """Initialize the flow and run the first task."""
52
+ self.state.current_step = "begin"
53
+ {%- if tasks %}
54
+ crew = Crew(
55
+ agents=[{{ tasks[0].agent_id }}],
56
+ tasks=[
57
+ CrewTask(
58
+ description="""{{ tasks[0].goal }}""",
59
+ agent={{ tasks[0].agent_id }},
60
+ expected_output="{{ tasks[0].outputs[0] if tasks[0].outputs else 'Result of the task' }}",
61
+ )
62
+ ],
63
+ process=Process.sequential,
64
+ )
65
+ result = crew.kickoff()
66
+ self.state.results["{{ tasks[0].id }}"] = str(result)
67
+ {%- endif %}
68
+ return self.state
69
+ {%- for task in tasks[1:] %}
70
+
71
+ @listen(begin{% if not loop.first %}_step_{{ loop.index }}{% endif %})
72
+ def step_{{ loop.index + 1 }}(self):
73
+ """Execute: {{ task.goal }}"""
74
+ self.state.current_step = "{{ task.id }}"
75
+ crew = Crew(
76
+ agents=[{{ task.agent_id }}],
77
+ tasks=[
78
+ CrewTask(
79
+ description="""{{ task.goal }}""",
80
+ agent={{ task.agent_id }},
81
+ expected_output="{{ task.outputs[0] if task.outputs else 'Result of the task' }}",
82
+ )
83
+ ],
84
+ process=Process.sequential,
85
+ )
86
+ result = crew.kickoff()
87
+ self.state.results["{{ task.id }}"] = str(result)
88
+ return self.state
89
+ {%- endfor %}
90
+
91
+
92
+ def main() -> Any:
93
+ """Entry-point for MCP wrapper or direct execution."""
94
+ flow = WorkflowFlow()
95
+ result = flow.kickoff()
96
+ return result
97
+
98
+
99
+ if __name__ == "__main__":
100
+ result = main()
101
+ print(result)
102
+ '''
103
+ ).strip("\n"),
104
+ trim_blocks=True,
105
+ lstrip_blocks=True,
106
+ )
107
+
108
+
109
+ class CrewAIFlowGenerator(BaseFrameworkGenerator):
110
+ framework = "crewai_flow"
111
+ file_extension = "py"
112
+
113
+ def _emit_core_code(self, workflow: Workflow, settings: Settings) -> str:
114
+ return (
115
+ _FLOW_TEMPLATE.render(
116
+ agents=workflow.agents,
117
+ tasks=workflow.tasks,
118
+ )
119
+ + "\n"
120
+ )
agent_generator_pkg/src/agent_generator/frameworks/langgraph/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """
2
+ LangGraph framework plug‑in.
3
+ """
4
+
5
+ from .generator import LangGraphGenerator # noqa: F401
6
+
7
+ __all__ = ["LangGraphGenerator"]
agent_generator_pkg/src/agent_generator/frameworks/langgraph/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (336 Bytes). View file
 
agent_generator_pkg/src/agent_generator/frameworks/langgraph/__pycache__/generator.cpython-311.pyc ADDED
Binary file (4.79 kB). View file
 
agent_generator_pkg/src/agent_generator/frameworks/langgraph/generator.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import textwrap
4
+
5
+ from jinja2 import Template
6
+
7
+ from agent_generator.config import Settings
8
+ from agent_generator.frameworks.base import BaseFrameworkGenerator
9
+ from agent_generator.models.workflow import Workflow
10
+
11
+ _LANG_TEMPLATE = Template(
12
+ textwrap.dedent(
13
+ '''
14
+ """Auto-generated LangGraph workflow (langgraph 1.x)."""
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, TypedDict
19
+
20
+ from langgraph.graph import START, StateGraph
21
+
22
+ # ─────────────────────────────────────────────────────────
23
+ # State schema
24
+ # ─────────────────────────────────────────────────────────
25
+
26
+ class WorkflowState(TypedDict):
27
+ """Shared state passed between nodes."""
28
+ input: str
29
+ {%- for task in tasks %}
30
+ {{ task.id }}_output: str
31
+ {%- endfor %}
32
+
33
+ # ─────────────────────────────────────────────────────────
34
+ # Node functions
35
+ # ─────────────────────────────────────────────────────────
36
+ {% for task in tasks %}
37
+ def {{ task.id }}(state: WorkflowState) -> dict[str, Any]:
38
+ """{{ task.goal }}"""
39
+ # Process input and produce output for this step
40
+ context = state.get("input", "")
41
+ {%- if task.inputs %}
42
+ # Also consider upstream outputs
43
+ {%- for inp_name in task.inputs %}
44
+ prev = state.get("{{ inp_name }}", "")
45
+ if prev:
46
+ context = f"{context}\\n{prev}"
47
+ {%- endfor %}
48
+ {%- endif %}
49
+ result = f"[{{ task.id }}] Processed: {context[:200]}"
50
+ return {"{{ task.id }}_output": result}
51
+
52
+ {% endfor %}
53
+
54
+ # ─────────────────────────────────────────────────────────
55
+ # Graph construction
56
+ # ─────────────────────────────────────────────────────────
57
+
58
+ def build_graph() -> StateGraph:
59
+ """Build and return the compiled LangGraph workflow."""
60
+ graph = StateGraph(WorkflowState)
61
+ {%- for task in tasks %}
62
+ graph.add_node("{{ task.id }}", {{ task.id }})
63
+ {%- endfor %}
64
+
65
+ # Edges
66
+ {%- if edges %}
67
+ graph.add_edge(START, "{{ edges[0].source }}")
68
+ {%- for edge in edges %}
69
+ graph.add_edge("{{ edge.source }}", "{{ edge.target }}")
70
+ {%- endfor %}
71
+ {%- else %}
72
+ graph.add_edge(START, "{{ tasks[0].id }}")
73
+ {%- endif %}
74
+
75
+ return graph
76
+
77
+
78
+ def main() -> Any:
79
+ """Entry-point for MCP wrapper or direct execution."""
80
+ graph = build_graph()
81
+ app = graph.compile()
82
+ result = app.invoke({"input": ""})
83
+ return result
84
+
85
+
86
+ if __name__ == "__main__":
87
+ result = main()
88
+ print(result)
89
+ '''
90
+ ).strip("\n"),
91
+ trim_blocks=True,
92
+ lstrip_blocks=True,
93
+ )
94
+
95
+
96
+ class LangGraphGenerator(BaseFrameworkGenerator):
97
+ framework = "langgraph"
98
+ file_extension = "py"
99
+
100
+ def _emit_core_code(self, workflow: Workflow, settings: Settings) -> str:
101
+ return (
102
+ _LANG_TEMPLATE.render(
103
+ tasks=workflow.tasks,
104
+ edges=workflow.edges,
105
+ )
106
+ + "\n"
107
+ )
agent_generator_pkg/src/agent_generator/frameworks/react/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """
2
+ ReAct framework plug‑in.
3
+ """
4
+
5
+ from .generator import ReActGenerator # noqa: F401
6
+
7
+ __all__ = ["ReActGenerator"]
agent_generator_pkg/src/agent_generator/frameworks/react/__pycache__/__init__.cpython-311.pyc ADDED
Binary file (324 Bytes). View file
 
agent_generator_pkg/src/agent_generator/frameworks/react/__pycache__/generator.cpython-311.pyc ADDED
Binary file (7.46 kB). View file