Spaces:
Sleeping
Sleeping
v0.2.0: production infrastructure — spec-first generators, security validator, ArtifactBundle
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +5 -0
- agent_generator_pkg/LICENSE +13 -0
- agent_generator_pkg/README.md +174 -0
- agent_generator_pkg/pyproject.toml +145 -0
- agent_generator_pkg/src/agent_generator/__init__.py +40 -0
- agent_generator_pkg/src/agent_generator/__pycache__/__init__.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/__pycache__/cli.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/__pycache__/config.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/__pycache__/wsgi.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/application/__init__.py +0 -0
- agent_generator_pkg/src/agent_generator/application/__pycache__/__init__.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/application/__pycache__/build_service.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/application/__pycache__/planning_service.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/application/__pycache__/validation_service.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/application/build_service.py +125 -0
- agent_generator_pkg/src/agent_generator/application/planning_service.py +152 -0
- agent_generator_pkg/src/agent_generator/application/release_service.py +16 -0
- agent_generator_pkg/src/agent_generator/application/validation_service.py +26 -0
- agent_generator_pkg/src/agent_generator/cli.py +245 -0
- agent_generator_pkg/src/agent_generator/config.py +158 -0
- agent_generator_pkg/src/agent_generator/data/default_prompts.yaml +31 -0
- agent_generator_pkg/src/agent_generator/domain/__init__.py +0 -0
- agent_generator_pkg/src/agent_generator/domain/__pycache__/__init__.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/domain/__pycache__/artifact_bundle.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/domain/__pycache__/capability_matrix.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/domain/__pycache__/project_spec.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/domain/__pycache__/render_plan.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/domain/artifact_bundle.py +31 -0
- agent_generator_pkg/src/agent_generator/domain/capability_matrix.py +65 -0
- agent_generator_pkg/src/agent_generator/domain/project_spec.py +94 -0
- agent_generator_pkg/src/agent_generator/domain/render_plan.py +26 -0
- agent_generator_pkg/src/agent_generator/frameworks/__init__.py +26 -0
- agent_generator_pkg/src/agent_generator/frameworks/__pycache__/__init__.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/frameworks/__pycache__/base.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/frameworks/base.py +181 -0
- agent_generator_pkg/src/agent_generator/frameworks/crewai/__init__.py +12 -0
- agent_generator_pkg/src/agent_generator/frameworks/crewai/__pycache__/__init__.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/frameworks/crewai/__pycache__/generator.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/frameworks/crewai/generator.py +83 -0
- agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/__init__.py +7 -0
- agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/__pycache__/__init__.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/__pycache__/generator.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/frameworks/crewai_flow/generator.py +120 -0
- agent_generator_pkg/src/agent_generator/frameworks/langgraph/__init__.py +7 -0
- agent_generator_pkg/src/agent_generator/frameworks/langgraph/__pycache__/__init__.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/frameworks/langgraph/__pycache__/generator.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/frameworks/langgraph/generator.py +107 -0
- agent_generator_pkg/src/agent_generator/frameworks/react/__init__.py +7 -0
- agent_generator_pkg/src/agent_generator/frameworks/react/__pycache__/__init__.cpython-311.pyc +0 -0
- 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 |
+
[](https://pypi.org/project/agent-generator/)
|
| 10 |
+
[](https://pypi.org/project/agent-generator/)
|
| 11 |
+
[](LICENSE)
|
| 12 |
+
[](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
|
|
|