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

v0.2.1 production RC — AST security validator, OutputTier, reference validation

Browse files
agent_generator_pkg/pyproject.toml CHANGED
@@ -8,7 +8,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"
@@ -78,6 +78,10 @@ web = [
78
  all = [
79
  "agent-generator[openai,crewai,langgraph,web]",
80
  ]
 
 
 
 
81
  dev = [
82
  "pytest>=8.2",
83
  "pytest-xdist>=3.5",
 
8
 
9
  [project]
10
  name = "agent-generator"
11
+ version = "0.2.1"
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"
 
78
  all = [
79
  "agent-generator[openai,crewai,langgraph,web]",
80
  ]
81
+ release = [
82
+ "build>=1.2",
83
+ "twine>=5.0",
84
+ ]
85
  dev = [
86
  "pytest>=8.2",
87
  "pytest-xdist>=3.5",
agent_generator_pkg/src/agent_generator/__init__.py CHANGED
@@ -21,7 +21,7 @@ from __future__ import annotations
21
  # ------------------------------------------------------------------ #
22
  # Version
23
  # ------------------------------------------------------------------ #
24
- __version__: str = "0.2.0"
25
 
26
  # ------------------------------------------------------------------ #
27
  # Surface imports (lazy‑safe)
 
21
  # ------------------------------------------------------------------ #
22
  # Version
23
  # ------------------------------------------------------------------ #
24
+ __version__: str = "0.2.1"
25
 
26
  # ------------------------------------------------------------------ #
27
  # Surface imports (lazy‑safe)
agent_generator_pkg/src/agent_generator/__pycache__/__init__.cpython-311.pyc CHANGED
Binary files a/agent_generator_pkg/src/agent_generator/__pycache__/__init__.cpython-311.pyc and b/agent_generator_pkg/src/agent_generator/__pycache__/__init__.cpython-311.pyc differ
 
agent_generator_pkg/src/agent_generator/__pycache__/cli.cpython-311.pyc CHANGED
Binary files a/agent_generator_pkg/src/agent_generator/__pycache__/cli.cpython-311.pyc and b/agent_generator_pkg/src/agent_generator/__pycache__/cli.cpython-311.pyc differ
 
agent_generator_pkg/src/agent_generator/application/__pycache__/build_service.cpython-311.pyc CHANGED
Binary files a/agent_generator_pkg/src/agent_generator/application/__pycache__/build_service.cpython-311.pyc and b/agent_generator_pkg/src/agent_generator/application/__pycache__/build_service.cpython-311.pyc differ
 
agent_generator_pkg/src/agent_generator/application/__pycache__/planning_service.cpython-311.pyc CHANGED
Binary files a/agent_generator_pkg/src/agent_generator/application/__pycache__/planning_service.cpython-311.pyc and b/agent_generator_pkg/src/agent_generator/application/__pycache__/planning_service.cpython-311.pyc differ
 
agent_generator_pkg/src/agent_generator/application/build_service.py CHANGED
@@ -70,7 +70,8 @@ def build(
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",
@@ -93,8 +94,9 @@ def build(
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(
 
70
  path="README.md",
71
  content=(
72
  f"# {project_name}\n\n{spec.description}\n\n"
73
+ f"Framework: {framework_name}\n"
74
+ f"Tier: {spec.template_tier.value}\n\n"
75
  f"Generated by [agent-generator](https://github.com/ruslanmv/agent-generator).\n"
76
  ),
77
  language="markdown",
 
94
  manifest: dict[str, Any] = {
95
  "framework": framework_name,
96
  "diagram": diagram,
97
+ "generator_version": "0.2.1",
98
  "project_name": project_name,
99
+ "template_tier": spec.template_tier.value,
100
  }
101
 
102
  artifact = ArtifactBundle(
agent_generator_pkg/src/agent_generator/application/planning_service.py CHANGED
@@ -2,6 +2,7 @@
2
  from __future__ import annotations
3
 
4
  import re
 
5
  from typing import Optional
6
 
7
  from agent_generator.domain.project_spec import (
@@ -9,6 +10,7 @@ from agent_generator.domain.project_spec import (
9
  ArtifactMode,
10
  FrameworkChoice,
11
  LLMSpec,
 
12
  ProjectSpec,
13
  RuntimeSpec,
14
  TaskSpec,
@@ -16,11 +18,10 @@ from agent_generator.domain.project_spec import (
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:
@@ -100,6 +101,10 @@ def plan(
100
 
101
  # ── Optional LLM planning stage ──────────────────────────────
102
  if use_llm and provider:
 
 
 
 
103
  try:
104
  from agent_generator.providers import PROVIDERS
105
 
@@ -111,9 +116,9 @@ def plan(
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
 
@@ -132,6 +137,7 @@ def plan(
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],
@@ -143,7 +149,7 @@ def plan(
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:
 
2
  from __future__ import annotations
3
 
4
  import re
5
+ import warnings as _warnings
6
  from typing import Optional
7
 
8
  from agent_generator.domain.project_spec import (
 
10
  ArtifactMode,
11
  FrameworkChoice,
12
  LLMSpec,
13
+ OutputTier,
14
  ProjectSpec,
15
  RuntimeSpec,
16
  TaskSpec,
 
18
  )
19
  from agent_generator.planners.keyword_planner import KeywordPlanner
20
  from agent_generator.planners.spec_normalizer import SpecNormalizer
21
+ from agent_generator.application.validation_service import validate_spec
22
 
23
  _keyword_planner = KeywordPlanner()
24
  _normalizer = SpecNormalizer()
 
25
 
26
 
27
  def _slugify(text: str) -> str:
 
101
 
102
  # ── Optional LLM planning stage ──────────────────────────────
103
  if use_llm and provider:
104
+ _warnings.warn(
105
+ "LLM-based planning is experimental and not enabled by default.",
106
+ stacklevel=2,
107
+ )
108
  try:
109
  from agent_generator.providers import PROVIDERS
110
 
 
116
  llm_planner = LLMPlanner(provider_inst)
117
  llm_spec = llm_planner.plan(prompt, hints)
118
  if llm_spec is not None:
119
+ spec, norm_warnings = _normalizer.normalize(llm_spec)
120
+ validation = validate_spec(spec)
121
+ return spec, norm_warnings + validation.warnings
122
  except Exception:
123
  pass # Fall through to keyword-based planning
124
 
 
137
  description=prompt[:500],
138
  framework=FrameworkChoice(fw),
139
  artifact_mode=ArtifactMode(mode),
140
+ template_tier=OutputTier.PRODUCTION,
141
  llm=LLMSpec(provider=provider or "watsonx"),
142
  agents=[AgentSpec(**a) for a in agents],
143
  tasks=[TaskSpec(**t) for t in tasks],
 
149
  spec, norm_warnings = _normalizer.normalize(spec)
150
 
151
  # Step 4: validate
152
+ validation = validate_spec(spec)
153
  all_warnings = norm_warnings + validation.warnings
154
 
155
  if not validation.valid:
agent_generator_pkg/src/agent_generator/cli.py CHANGED
@@ -54,7 +54,7 @@ app = typer.Typer(
54
 
55
  console = Console()
56
 
57
- VERSION = "0.2.0" # 🛈 bump on release
58
 
59
 
60
  # ---------------------------------------------------------------- #
 
54
 
55
  console = Console()
56
 
57
+ VERSION = "0.2.1" # 🛈 bump on release
58
 
59
 
60
  # ---------------------------------------------------------------- #
agent_generator_pkg/src/agent_generator/domain/__pycache__/project_spec.cpython-311.pyc CHANGED
Binary files a/agent_generator_pkg/src/agent_generator/domain/__pycache__/project_spec.cpython-311.pyc and b/agent_generator_pkg/src/agent_generator/domain/__pycache__/project_spec.cpython-311.pyc differ
 
agent_generator_pkg/src/agent_generator/domain/project_spec.py CHANGED
@@ -1,10 +1,10 @@
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):
@@ -21,54 +21,45 @@ class ArtifactMode(str, Enum):
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):
@@ -80,11 +71,9 @@ class ProjectSpec(BaseModel):
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)
@@ -92,3 +81,23 @@ class ProjectSpec(BaseModel):
92
  tasks: list[TaskSpec] = Field(..., min_length=1)
93
  tools: list[ToolSpec] = Field(default_factory=list)
94
  runtime: RuntimeSpec = Field(default_factory=RuntimeSpec)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Canonical project specification the single source of truth."""
2
  from __future__ import annotations
3
 
4
  from enum import Enum
5
  from typing import Optional
6
 
7
+ from pydantic import BaseModel, Field, model_validator
8
 
9
 
10
  class FrameworkChoice(str, Enum):
 
21
  CODE_AND_YAML = "code_and_yaml"
22
 
23
 
24
+ class OutputTier(str, Enum):
25
+ STARTER = "starter"
26
+ PRODUCTION = "production"
27
+
28
 
29
+ class AgentSpec(BaseModel):
30
  id: str = Field(..., description="Unique agent identifier (snake_case).")
31
  role: str = Field(..., description="Agent role name.")
32
  goal: str = Field(..., description="What this agent tries to achieve.")
33
  backstory: str = Field(default="", description="Background context for the agent.")
34
  tools: list[str] = Field(default_factory=list, description="Tool IDs this agent can use.")
35
+ llm_override: Optional[str] = Field(default=None, description="Optional model override.")
 
 
36
 
37
 
38
  class TaskSpec(BaseModel):
 
 
39
  id: str = Field(..., description="Unique task identifier (snake_case).")
40
  description: str = Field(..., description="What this task does.")
41
  agent_id: str = Field(..., description="ID of the agent responsible.")
42
  expected_output: str = Field(..., description="What the task should produce.")
43
+ depends_on: list[str] = Field(default_factory=list, description="IDs of prerequisite tasks.")
 
 
44
 
45
 
46
  class ToolSpec(BaseModel):
 
 
47
  id: str = Field(..., description="Unique tool identifier.")
48
+ template: str = Field(..., description="Tool catalog template key.")
49
+ inputs: dict[str, str] = Field(default_factory=dict, description="Template variable overrides.")
 
 
50
 
51
 
52
  class LLMSpec(BaseModel):
 
 
53
  provider: str = Field(default="watsonx")
54
  model: str = Field(default="meta-llama/llama-3-3-70b-instruct")
55
+ temperature: float = Field(default=0.0, ge=0.0, le=2.0)
56
 
57
 
58
  class RuntimeSpec(BaseModel):
 
 
59
  serve_api: bool = Field(default=False)
60
  mcp_wrapper: bool = Field(default=False)
61
  mcp_port: int = Field(default=8080, ge=1, le=65535)
62
+ healthcheck_path: str = Field(default="/health")
63
 
64
 
65
  class ProjectSpec(BaseModel):
 
71
  pattern=r"^[a-z0-9][a-z0-9-]*$",
72
  )
73
  version: str = Field(default="1.0", description="Schema version.")
74
+ template_tier: OutputTier = Field(default=OutputTier.PRODUCTION)
75
+ metadata: dict[str, str] = Field(default_factory=dict)
76
+ description: str = Field(..., max_length=500)
 
 
77
  framework: FrameworkChoice
78
  artifact_mode: ArtifactMode = Field(default=ArtifactMode.CODE_ONLY)
79
  llm: LLMSpec = Field(default_factory=LLMSpec)
 
81
  tasks: list[TaskSpec] = Field(..., min_length=1)
82
  tools: list[ToolSpec] = Field(default_factory=list)
83
  runtime: RuntimeSpec = Field(default_factory=RuntimeSpec)
84
+
85
+ @model_validator(mode="after")
86
+ def validate_references(self) -> "ProjectSpec":
87
+ agent_ids = {a.id for a in self.agents}
88
+ task_ids = {t.id for t in self.tasks}
89
+ tool_ids = {t.id for t in self.tools}
90
+
91
+ for task in self.tasks:
92
+ if task.agent_id not in agent_ids:
93
+ raise ValueError(f"Task '{task.id}' references unknown agent '{task.agent_id}'")
94
+ for dep in task.depends_on:
95
+ if dep not in task_ids:
96
+ raise ValueError(f"Task '{task.id}' depends on unknown task '{dep}'")
97
+
98
+ for agent in self.agents:
99
+ for tool_id in agent.tools:
100
+ if tool_id not in tool_ids:
101
+ raise ValueError(f"Agent '{agent.id}' references unknown tool '{tool_id}'")
102
+
103
+ return self
agent_generator_pkg/src/agent_generator/validators/__pycache__/security_validator.cpython-311.pyc CHANGED
Binary files a/agent_generator_pkg/src/agent_generator/validators/__pycache__/security_validator.cpython-311.pyc and b/agent_generator_pkg/src/agent_generator/validators/__pycache__/security_validator.cpython-311.pyc differ
 
agent_generator_pkg/src/agent_generator/validators/security_validator.py CHANGED
@@ -1,29 +1,66 @@
1
- """Security validator — blocks unsafe patterns in generated code."""
2
  from __future__ import annotations
3
 
 
 
4
  from agent_generator.domain.artifact_bundle import ArtifactBundle, ValidationIssue
5
 
6
- FORBIDDEN_PATTERNS: list[tuple[str, str]] = [
7
- ("eval(", "eval() is unsafe — use ast.literal_eval or a safe evaluator"),
8
- ("exec(", "exec() is unsafe — avoid dynamic code execution"),
9
- ("subprocess.Popen(", "subprocess.Popen without restrictions is unsafe"),
10
- ("os.system(", "os.system() is unsafe — use subprocess with shell=False"),
11
- ("__import__(", "__import__() is unsafe — use regular imports"),
12
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
 
15
  class SecurityValidator:
16
- """Scan generated artifacts for forbidden code patterns."""
17
 
18
  def validate(self, artifact: ArtifactBundle) -> list[ValidationIssue]:
19
  issues: list[ValidationIssue] = []
20
  for file in artifact.files:
21
  if not file.path.endswith(".py"):
22
  continue
23
- for pattern, reason in FORBIDDEN_PATTERNS:
24
- if pattern in file.content:
25
- issues.append(ValidationIssue(
26
- level="error",
27
- message=f"{file.path}: {reason}",
28
- ))
 
 
 
 
 
 
 
 
 
 
 
 
 
29
  return issues
 
1
+ """AST-based security validator — blocks unsafe patterns in generated code."""
2
  from __future__ import annotations
3
 
4
+ import ast
5
+
6
  from agent_generator.domain.artifact_bundle import ArtifactBundle, ValidationIssue
7
 
8
+ FORBIDDEN_CALLS = {"eval", "exec", "__import__"}
9
+
10
+ FORBIDDEN_ATTR_CALLS = {
11
+ ("os", "system"),
12
+ ("subprocess", "Popen"),
13
+ ("subprocess", "call"),
14
+ ("subprocess", "run"),
15
+ }
16
+
17
+
18
+ class _SecurityVisitor(ast.NodeVisitor):
19
+ def __init__(self, path: str) -> None:
20
+ self.path = path
21
+ self.issues: list[ValidationIssue] = []
22
+
23
+ def visit_Call(self, node: ast.Call) -> None:
24
+ if isinstance(node.func, ast.Name) and node.func.id in FORBIDDEN_CALLS:
25
+ self.issues.append(ValidationIssue(
26
+ level="error",
27
+ message=f"{self.path}: forbidden call '{node.func.id}()' detected",
28
+ ))
29
+ if isinstance(node.func, ast.Attribute) and isinstance(node.func.value, ast.Name):
30
+ key = (node.func.value.id, node.func.attr)
31
+ if key in FORBIDDEN_ATTR_CALLS:
32
+ self.issues.append(ValidationIssue(
33
+ level="error",
34
+ message=f"{self.path}: forbidden call '{key[0]}.{key[1]}()' detected",
35
+ ))
36
+ self.generic_visit(node)
37
 
38
 
39
  class SecurityValidator:
40
+ """Scan generated artifacts for forbidden code patterns using AST analysis."""
41
 
42
  def validate(self, artifact: ArtifactBundle) -> list[ValidationIssue]:
43
  issues: list[ValidationIssue] = []
44
  for file in artifact.files:
45
  if not file.path.endswith(".py"):
46
  continue
47
+ try:
48
+ tree = ast.parse(file.content)
49
+ except SyntaxError as exc:
50
+ issues.append(ValidationIssue(
51
+ level="error",
52
+ message=f"{file.path}: syntax error during security scan: {exc.msg}",
53
+ ))
54
+ continue
55
+
56
+ visitor = _SecurityVisitor(file.path)
57
+ visitor.visit(tree)
58
+ issues.extend(visitor.issues)
59
+
60
+ # Warn on requests calls without timeout
61
+ if "requests." in file.content and "timeout=" not in file.content:
62
+ issues.append(ValidationIssue(
63
+ level="warning",
64
+ message=f"{file.path}: HTTP request may be missing timeout parameter",
65
+ ))
66
  return issues
agent_generator_pkg/src/agent_generator/web/routes/__pycache__/api.cpython-311.pyc CHANGED
Binary files a/agent_generator_pkg/src/agent_generator/web/routes/__pycache__/api.cpython-311.pyc and b/agent_generator_pkg/src/agent_generator/web/routes/__pycache__/api.cpython-311.pyc differ
 
agent_generator_pkg/src/agent_generator/web/routes/__pycache__/pages.cpython-311.pyc CHANGED
Binary files a/agent_generator_pkg/src/agent_generator/web/routes/__pycache__/pages.cpython-311.pyc and b/agent_generator_pkg/src/agent_generator/web/routes/__pycache__/pages.cpython-311.pyc differ
 
agent_generator_pkg/src/agent_generator/web/routes/api.py CHANGED
@@ -24,10 +24,10 @@ from fastapi import APIRouter, HTTPException, Request
24
  from fastapi.responses import JSONResponse
25
  from pydantic import BaseModel, Field, ValidationError
26
 
 
 
27
  from agent_generator.config import Settings, get_settings
28
  from agent_generator.frameworks import FRAMEWORKS
29
- from agent_generator.utils.parser import parse_natural_language_to_workflow
30
- from agent_generator.utils.visualizer import to_mermaid
31
  from agent_generator.web.inference import (
32
  get_inference_client,
33
  get_inference_settings,
@@ -121,43 +121,18 @@ async def plan(req: PlanRequest):
121
  if not prompt:
122
  raise HTTPException(status_code=400, detail="Prompt is required.")
123
 
124
- framework = req.framework if req.framework in FRAMEWORKS else "crewai"
125
-
126
- workflow = parse_natural_language_to_workflow(prompt)
127
-
128
- agents: list[AgentInfo] = []
129
- if hasattr(workflow, "agents"):
130
- for a in workflow.agents:
131
- agents.append(AgentInfo(
132
- role=a.role if hasattr(a, "role") else str(a),
133
- goal=a.goal if hasattr(a, "goal") else "",
134
- tools=a.tools if hasattr(a, "tools") else [],
135
- ))
136
-
137
- tasks: list[TaskInfo] = []
138
- if hasattr(workflow, "tasks"):
139
- for t in workflow.tasks:
140
- tasks.append(TaskInfo(
141
- description=t.description if hasattr(t, "description") else str(t),
142
- agent_role=t.agent_id if hasattr(t, "agent_id") else "",
143
- expected_output=t.expected_output if hasattr(t, "expected_output") else "",
144
- depends_on=t.depends_on if hasattr(t, "depends_on") else [],
145
- ))
146
-
147
- warnings: list[str] = []
148
- if not agents:
149
- warnings.append("No agents detected. The LLM will infer agent roles.")
150
- if not tasks:
151
- warnings.append("No tasks detected. The LLM will generate default tasks.")
152
-
153
- name = workflow.name if hasattr(workflow, "name") and workflow.name else "agent-project"
154
- description = workflow.description if hasattr(workflow, "description") and workflow.description else prompt[:200]
155
 
156
  return ProjectPlan(
157
- name=name,
158
- description=description,
159
- framework=framework,
160
- artifact_mode=req.artifact_mode,
161
  agents=agents,
162
  tasks=tasks,
163
  warnings=warnings,
@@ -167,52 +142,23 @@ async def plan(req: PlanRequest):
167
  @router.post("/build", response_model=BuildResponse)
168
  async def build(req: BuildRequest):
169
  """Take a structured plan and produce code artifacts."""
170
- plan = req.plan
171
- framework = plan.framework if plan.framework in FRAMEWORKS else "crewai"
172
 
173
- try:
174
- settings = Settings(
175
- provider=req.provider or get_settings().provider,
176
- model=req.model or get_settings().model,
177
- temperature=req.temperature if req.temperature is not None else get_settings().temperature,
178
- max_tokens=get_settings().max_tokens,
179
- )
180
- except (ValidationError, Exception):
181
- settings = get_settings()
182
-
183
- # Reconstruct prompt from plan for the parser
184
- prompt_parts = [f"Project: {plan.name}", f"Description: {plan.description}"]
185
- for a in plan.agents:
186
- prompt_parts.append(f"Agent '{a.role}': {a.goal}")
187
- for t in plan.tasks:
188
- prompt_parts.append(f"Task: {t.description} (assigned to {t.agent_role})")
189
- combined_prompt = "\n".join(prompt_parts)
190
-
191
- workflow = parse_natural_language_to_workflow(combined_prompt)
192
- generator_cls = FRAMEWORKS[framework]
193
- generator = generator_cls()
194
- code = generator.generate_code(workflow, settings, mcp=req.mcp)
195
- diagram = to_mermaid(workflow)
196
-
197
- files = [
198
- FileArtifact(path=f"src/{plan.name}/main.py", content=code, language="python"),
199
- FileArtifact(path=f"src/{plan.name}/__init__.py", content=f'"""Generated {plan.name} package."""\n', language="python"),
200
- ]
201
-
202
- errors: list[str] = []
203
- warnings = list(plan.warnings)
204
-
205
- if len(code) < 50:
206
- warnings.append("Generated code is very short.")
207
 
208
  return BuildResponse(
209
- project_name=plan.name,
210
- framework=framework,
211
  files=files,
212
- diagram=diagram,
213
- errors=errors,
214
- warnings=warnings,
215
- validation_passed=len(errors) == 0,
216
  )
217
 
218
 
@@ -220,40 +166,28 @@ async def build(req: BuildRequest):
220
  async def generate(req: GenerateRequest):
221
  """Combined plan + build endpoint (backward compatible)."""
222
  prompt = req.prompt.strip()
223
- framework_name = req.framework.strip()
224
- provider_name = req.provider or get_settings().provider
225
- mcp = req.mcp
226
-
227
- if not prompt or framework_name not in FRAMEWORKS:
228
- raise HTTPException(
229
- status_code=400,
230
- detail="Missing prompt or unknown framework.",
231
- )
232
 
233
- try:
234
- settings = Settings(
235
- provider=provider_name,
236
- model=req.model or get_settings().model,
237
- temperature=req.temperature if req.temperature is not None else get_settings().temperature,
238
- max_tokens=req.max_tokens or get_settings().max_tokens,
239
- )
240
- except ValidationError as exc:
241
- raise HTTPException(status_code=400, detail=str(exc.errors()))
242
 
243
- workflow = parse_natural_language_to_workflow(prompt)
244
- generator_cls = FRAMEWORKS[framework_name]
245
- generator = generator_cls()
246
- code = generator.generate_code(workflow, settings, mcp=mcp)
247
 
248
- warnings: list[str] = []
249
- if len(code) < 50:
250
- warnings.append("Generated code is very short.")
 
 
 
 
 
251
 
252
  return GenerateResponse(
253
  code=code,
254
- diagram=to_mermaid(workflow),
255
- framework=framework_name,
256
- warnings=warnings,
257
  )
258
 
259
 
 
24
  from fastapi.responses import JSONResponse
25
  from pydantic import BaseModel, Field, ValidationError
26
 
27
+ from agent_generator.application.planning_service import plan as plan_spec
28
+ from agent_generator.application.build_service import build_dict
29
  from agent_generator.config import Settings, get_settings
30
  from agent_generator.frameworks import FRAMEWORKS
 
 
31
  from agent_generator.web.inference import (
32
  get_inference_client,
33
  get_inference_settings,
 
121
  if not prompt:
122
  raise HTTPException(status_code=400, detail="Prompt is required.")
123
 
124
+ fw = req.framework if req.framework in FRAMEWORKS else None
125
+ spec, warnings = plan_spec(prompt, framework=fw)
126
+
127
+ agents = [AgentInfo(role=a.role, goal=a.goal, tools=a.tools) for a in spec.agents]
128
+ tasks = [TaskInfo(description=t.description, agent_role=t.agent_id,
129
+ expected_output=t.expected_output, depends_on=t.depends_on) for t in spec.tasks]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  return ProjectPlan(
132
+ name=spec.name,
133
+ description=spec.description,
134
+ framework=spec.framework.value,
135
+ artifact_mode=req.artifact_mode or spec.artifact_mode.value,
136
  agents=agents,
137
  tasks=tasks,
138
  warnings=warnings,
 
142
  @router.post("/build", response_model=BuildResponse)
143
  async def build(req: BuildRequest):
144
  """Take a structured plan and produce code artifacts."""
145
+ p = req.plan
146
+ fw = p.framework if p.framework in FRAMEWORKS else "crewai"
147
 
148
+ spec, _ = plan_spec(p.description, framework=fw)
149
+ result = build_dict(spec, mcp=req.mcp)
150
+
151
+ files = [FileArtifact(path=path, content=content, language="python" if path.endswith(".py") else "yaml")
152
+ for path, content in result.get("files", {}).items()]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
  return BuildResponse(
155
+ project_name=spec.name,
156
+ framework=fw,
157
  files=files,
158
+ diagram=result.get("diagram", ""),
159
+ errors=result.get("errors", []),
160
+ warnings=result.get("warnings", []) + list(p.warnings),
161
+ validation_passed=result.get("valid", True),
162
  )
163
 
164
 
 
166
  async def generate(req: GenerateRequest):
167
  """Combined plan + build endpoint (backward compatible)."""
168
  prompt = req.prompt.strip()
169
+ fw = req.framework.strip() if req.framework.strip() in FRAMEWORKS else None
 
 
 
 
 
 
 
 
170
 
171
+ if not prompt:
172
+ raise HTTPException(status_code=400, detail="Prompt is required.")
 
 
 
 
 
 
 
173
 
174
+ spec, warnings = plan_spec(prompt, framework=fw)
175
+ result = build_dict(spec, mcp=req.mcp)
 
 
176
 
177
+ # Pick the main code file
178
+ code = ""
179
+ for path, content in result.get("files", {}).items():
180
+ if path.endswith((".py", ".yaml")) and ("main" in path or path.endswith(".yaml")):
181
+ code = content
182
+ break
183
+ if not code:
184
+ code = next(iter(result.get("files", {}).values()), "")
185
 
186
  return GenerateResponse(
187
  code=code,
188
+ diagram=result.get("diagram", ""),
189
+ framework=spec.framework.value,
190
+ warnings=warnings + result.get("warnings", []),
191
  )
192
 
193
 
agent_generator_pkg/src/agent_generator/web/routes/pages.py CHANGED
@@ -1,14 +1,13 @@
1
  """
2
  HTML page routes for the Agent Generator web UI -- 4-step wizard.
3
 
4
- Step 1: Describe (home)
5
- Step 2: Plan & Edit
6
- Step 3: Configure (framework, mode, tools)
7
- Step 4: Generate & Export
8
  """
9
  from __future__ import annotations
10
 
11
- import ast
12
  import io
13
  import json
14
  import uuid
@@ -21,11 +20,9 @@ from fastapi import APIRouter, Form, HTTPException, Request
21
  from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
22
  from fastapi.templating import Jinja2Templates
23
 
24
- from agent_generator.config import Settings, get_settings_lenient
 
25
  from agent_generator.frameworks import FRAMEWORKS
26
- from agent_generator.planners.keyword_planner import KeywordPlanner
27
- from agent_generator.utils.parser import parse_natural_language_to_workflow
28
- from agent_generator.utils.visualizer import to_mermaid
29
  from agent_generator.web.inference import (
30
  get_inference_client,
31
  get_inference_settings,
@@ -108,7 +105,6 @@ TOOL_CATEGORIES = {
108
  # Helpers
109
  # ---------------------------------------------------------------------------
110
  def _build_file_tree(files: dict) -> list:
111
- """Build a hierarchical file tree from a flat dict of paths."""
112
  tree = []
113
  dirs_seen: set[str] = set()
114
  for filepath in sorted(files.keys()):
@@ -123,22 +119,15 @@ def _build_file_tree(files: dict) -> list:
123
 
124
 
125
  def _validate_files(files: dict[str, str]) -> list[dict]:
126
- """Run basic validation on generated files.
127
-
128
- Returns a list of {file, status, message} dicts.
129
- """
130
  results = []
131
  for filepath, content in files.items():
132
  if filepath.endswith(".py"):
133
  try:
134
- ast.parse(content, filename=filepath)
135
  results.append({"file": filepath, "status": "ok", "message": "Valid Python"})
136
  except SyntaxError as exc:
137
- results.append({
138
- "file": filepath,
139
- "status": "error",
140
- "message": f"SyntaxError line {exc.lineno}: {exc.msg}",
141
- })
142
  elif filepath.endswith((".yaml", ".yml")):
143
  try:
144
  yaml.safe_load(content)
@@ -148,98 +137,39 @@ def _validate_files(files: dict[str, str]) -> list[dict]:
148
  return results
149
 
150
 
151
- def _merge_llm_plan(base_plan: dict, llm_plan: dict) -> dict:
152
- """Merge an LLM-generated plan into the keyword-based plan.
153
-
154
- The LLM plan is authoritative for agents/tasks/tools when present,
155
- but we fall back to the keyword plan for any missing fields.
156
- """
157
- merged = dict(base_plan)
158
- if llm_plan.get("name"):
159
- merged["name"] = llm_plan["name"]
160
- if llm_plan.get("description"):
161
- merged["description"] = llm_plan["description"]
162
- if llm_plan.get("framework") and llm_plan["framework"] in FRAMEWORK_LABELS:
163
- merged["framework"] = llm_plan["framework"]
164
- if llm_plan.get("agents"):
165
- merged["agents"] = llm_plan["agents"]
166
- if llm_plan.get("tasks"):
167
- merged["tasks"] = llm_plan["tasks"]
168
- if llm_plan.get("tools"):
169
- merged["tools"] = llm_plan["tools"]
170
- return merged
171
-
172
-
173
- def _keyword_plan(prompt: str) -> dict:
174
- """Build a plan using the KeywordPlanner from the main project."""
175
- planner = KeywordPlanner()
176
- classification = planner.classify(prompt)
177
-
178
- framework = classification.get("suggested_framework", "crewai")
179
- if framework not in FRAMEWORKS:
180
- framework = "crewai"
181
-
182
- tools = classification.get("suggested_tools", [])
183
- roles = classification.get("suggested_roles", [])
184
- if not roles:
185
- roles = ["assistant"]
186
-
187
- import re
188
- agents = []
189
- for role in roles:
190
- agents.append({
191
- "id": role,
192
- "role": role.replace("_", " ").title(),
193
- "goal": f"Handle all {role}-related tasks effectively",
194
- "backstory": f"An experienced {role} with deep domain expertise.",
195
- "tools": tools[:2] if tools else [],
196
- })
197
-
198
- sentences = [s.strip() for s in re.split(r"[.!?]+", prompt) if s.strip() and len(s.strip()) > 5]
199
- if not sentences:
200
- sentences = [prompt.strip()]
201
-
202
- tasks = []
203
- for i, sentence in enumerate(sentences[:6]):
204
- agent_idx = i % len(agents)
205
- task_id = f"task_{i + 1}"
206
- tasks.append({
207
- "id": task_id,
208
- "description": sentence,
209
- "agent_id": agents[agent_idx]["id"],
210
- "expected_output": f"Completed: {sentence[:80]}",
211
- "depends_on": [f"task_{i}"] if i > 0 else [],
212
- })
213
-
214
- slug = re.sub(r"[^a-z0-9\s-]", "", prompt[:50].lower())
215
- slug = re.sub(r"[\s_]+", "-", slug.strip())[:40] or "my-agent"
216
-
217
  return {
218
- "name": slug,
219
- "description": prompt[:200],
220
- "framework": framework,
221
- "artifact_mode": "code_and_yaml",
222
- "agents": agents,
223
- "tasks": tasks,
224
- "tools": [{"id": t, "template": t} for t in tools],
225
  }
226
 
227
 
 
 
 
 
 
 
 
 
 
 
 
228
  # ---------------------------------------------------------------------------
229
  # Routes -- Step 1: Describe
230
  # ---------------------------------------------------------------------------
231
  @router.get("/", response_class=HTMLResponse)
232
  async def home(request: Request):
233
- """Step 1 -- prompt input with examples."""
234
  inference = get_inference_client()
235
  return templates.TemplateResponse(
236
- request=request,
237
- name="home.html",
238
- context={
239
- "request": request,
240
- "examples": EXAMPLES,
241
- "inference_available": inference.available,
242
- },
243
  )
244
 
245
 
@@ -248,75 +178,42 @@ async def home(request: Request):
248
  # ---------------------------------------------------------------------------
249
  @router.post("/plan", response_class=HTMLResponse)
250
  async def plan(request: Request, prompt: str = Form(...)):
251
- """Step 2 -- generate editable plan from prompt."""
252
  try:
253
- # Keyword-based baseline
254
- base_plan = _keyword_plan(prompt)
255
 
256
- # Try LLM enhancement
257
  llm_enhanced = False
258
  inference = get_inference_client()
259
  if inference.available:
260
  llm_plan = inference.generate_plan(prompt)
261
  if llm_plan:
262
- base_plan = _merge_llm_plan(base_plan, llm_plan)
 
 
263
  llm_enhanced = True
264
 
265
  return templates.TemplateResponse(
266
- request=request,
267
- name="plan.html",
268
- context={
269
- "request": request,
270
- "plan": base_plan,
271
- "plan_json": json.dumps(base_plan, indent=2),
272
- "prompt": prompt,
273
- "llm_enhanced": llm_enhanced,
274
- "frameworks": FRAMEWORK_LABELS,
275
- },
276
  )
277
  except Exception as e:
278
  return templates.TemplateResponse(
279
- request=request,
280
- name="home.html",
281
- context={
282
- "request": request,
283
- "examples": EXAMPLES,
284
- "error": f"Planning failed: {e}",
285
- "inference_available": False,
286
- },
287
  )
288
 
289
 
290
  @router.post("/edit-plan", response_class=JSONResponse)
291
  async def edit_plan(request: Request):
292
- """AJAX endpoint -- re-plan with edits applied."""
293
  body = await request.json()
294
  prompt = body.get("prompt", "")
295
  edits = body.get("edits", "")
296
- current_plan = body.get("current_plan", {})
297
-
298
- combined_prompt = f"{prompt}. Additionally: {edits}" if edits else prompt
299
-
300
  try:
301
- base_plan = _keyword_plan(combined_prompt)
302
-
303
- # Preserve framework choice from current plan if set
304
- if current_plan.get("framework") and current_plan["framework"] != "auto":
305
- base_plan["framework"] = current_plan["framework"]
306
-
307
- inference = get_inference_client()
308
- llm_enhanced = False
309
- if inference.available:
310
- llm_plan = inference.generate_plan(combined_prompt)
311
- if llm_plan:
312
- base_plan = _merge_llm_plan(base_plan, llm_plan)
313
- llm_enhanced = True
314
-
315
- return JSONResponse(content={
316
- "ok": True,
317
- "plan": base_plan,
318
- "llm_enhanced": llm_enhanced,
319
- })
320
  except Exception as e:
321
  return JSONResponse(content={"ok": False, "error": str(e)}, status_code=500)
322
 
@@ -325,60 +222,36 @@ async def edit_plan(request: Request):
325
  # Routes -- Step 3: Configure
326
  # ---------------------------------------------------------------------------
327
  @router.post("/configure", response_class=HTMLResponse)
328
- async def configure(
329
- request: Request,
330
- plan_json: str = Form(...),
331
- prompt: str = Form(""),
332
- ):
333
- """Step 3 -- framework / mode / provider / tools configuration."""
334
  try:
335
  plan_data = json.loads(plan_json)
336
  except json.JSONDecodeError:
337
  raise HTTPException(status_code=400, detail="Invalid plan JSON")
338
 
339
- # Validate framework against FRAMEWORKS registry
340
- fw = plan_data.get("framework", "crewai")
341
- if fw not in FRAMEWORKS:
342
- fw = "crewai"
343
- plan_data["framework"] = fw
344
-
345
- # Build a preview file tree using the real generator
346
- preview_plan = dict(plan_data)
347
- preview_plan.setdefault("artifact_mode", "code_and_yaml")
348
-
349
  try:
350
- settings = get_settings_lenient()
351
- workflow = parse_natural_language_to_workflow(
352
- preview_plan.get("description", "preview")
353
  )
354
- generator_cls = FRAMEWORKS[fw]
355
- generator = generator_cls()
356
- code = generator.generate_code(workflow, settings, mcp=False)
357
- preview_files = {"src/main.py": code}
358
- if hasattr(generator, "generate_yaml"):
359
- try:
360
- yaml_code = generator.generate_yaml(workflow, settings)
361
- preview_files["config/agents.yaml"] = yaml_code
362
- except Exception:
363
- pass
364
  except Exception:
365
- preview_files = {"src/main.py": "# preview"}
366
 
367
- preview_tree = _build_file_tree(preview_files)
 
 
 
 
 
368
 
369
  return templates.TemplateResponse(
370
- request=request,
371
- name="configure.html",
372
  context={
373
- "request": request,
374
- "plan": plan_data,
375
- "plan_json": json.dumps(plan_data),
376
- "prompt": prompt,
377
- "frameworks": FRAMEWORK_LABELS,
378
- "framework_capabilities": FRAMEWORK_CAPABILITIES,
379
- "tool_categories": TOOL_CATEGORIES,
380
- "preview_tree": preview_tree,
381
- "selected_tools": [t["id"] for t in plan_data.get("tools", [])],
382
  },
383
  )
384
 
@@ -396,133 +269,60 @@ async def generate(
396
  provider: str = Form("watsonx"),
397
  tools: list[str] = Form(default=[]),
398
  ):
399
- """Step 4 -- run generator, show results."""
400
  try:
401
  plan_data = json.loads(plan_json)
402
  except json.JSONDecodeError:
403
  raise HTTPException(status_code=400, detail="Invalid plan JSON")
404
 
405
- # Apply config overrides
406
  if framework and framework != "auto":
407
  plan_data["framework"] = framework
408
- plan_data["artifact_mode"] = artifact_mode
409
-
410
- # Validate framework
411
- fw = plan_data.get("framework", "crewai")
412
- if fw not in FRAMEWORKS:
413
- fw = "crewai"
414
- plan_data["framework"] = fw
415
-
416
- # Override tools if user changed selection
417
- tool_list = [t for t in tools if t] if tools else None
418
- if tool_list is not None:
419
- plan_data["tools"] = [{"id": t, "template": t} for t in tool_list]
420
- for agent in plan_data.get("agents", []):
421
- agent["tools"] = tool_list[:2]
422
 
423
  try:
424
  effective_prompt = plan_data.get("description", prompt or "agent project")
425
- settings = get_settings_lenient()
426
- workflow = parse_natural_language_to_workflow(effective_prompt)
427
-
428
- generator_cls = FRAMEWORKS[fw]
429
- generator = generator_cls()
430
- code = generator.generate_code(workflow, settings, mcp=False)
431
-
432
- # Build file dict
433
- project_name = plan_data.get("name", "agent-project")
434
- files: dict[str, str] = {}
435
-
436
- if artifact_mode in ("code_only", "code_and_yaml"):
437
- files[f"src/{project_name}/main.py"] = code
438
- files[f"src/{project_name}/__init__.py"] = f'"""Generated {project_name} package."""\n'
439
- files["requirements.txt"] = _make_requirements(fw, provider)
440
- files["README.md"] = _make_readme(project_name, plan_data.get("description", ""), fw)
441
-
442
- if artifact_mode in ("yaml_only", "code_and_yaml"):
443
- if hasattr(generator, "generate_yaml"):
444
- try:
445
- yaml_code = generator.generate_yaml(workflow, settings)
446
- files[f"config/{project_name}.yaml"] = yaml_code
447
- except Exception:
448
- pass
449
- else:
450
- files[f"config/{project_name}.yaml"] = yaml.dump({
451
- "name": project_name,
452
- "framework": fw,
453
- "agents": [{"role": a.get("role", ""), "goal": a.get("goal", "")} for a in plan_data.get("agents", [])],
454
- }, default_flow_style=False)
455
-
456
- # Merge back metadata from original plan
457
- plan_result = dict(plan_data)
458
- plan_result["name"] = plan_data.get("name", project_name)
459
- plan_result["description"] = plan_data.get("description", "")
460
- plan_result["framework"] = fw
461
-
462
- # Validate
463
  validation = _validate_files(files)
464
  errors = [v for v in validation if v["status"] == "error"]
465
  ok_count = len([v for v in validation if v["status"] == "ok"])
466
 
467
- # Store project
468
  project_id = str(uuid.uuid4())[:8]
469
  projects[project_id] = {"plan": plan_result, "files": files, "prompt": prompt}
470
 
471
  file_tree = _build_file_tree(files)
472
- fw_label = FRAMEWORK_LABELS.get(plan_result["framework"], plan_result["framework"])
473
 
474
  return templates.TemplateResponse(
475
- request=request,
476
- name="result.html",
477
- context={
478
- "request": request,
479
- "project_id": project_id,
480
- "plan": plan_result,
481
- "files": files,
482
- "file_tree": file_tree,
483
- "fw_label": fw_label,
484
- "prompt": prompt,
485
- "validation": validation,
486
- "validation_errors": errors,
487
- "validation_ok": ok_count,
488
- },
489
  )
490
  except Exception as e:
491
  return templates.TemplateResponse(
492
- request=request,
493
- name="home.html",
494
- context={
495
- "request": request,
496
- "examples": EXAMPLES,
497
- "error": f"Generation failed: {e}",
498
- "inference_available": False,
499
- },
500
  )
501
 
502
 
503
  # ---------------------------------------------------------------------------
504
- # Routes -- Download & File Access
505
  # ---------------------------------------------------------------------------
506
  @router.get("/download/{project_id}")
507
- async def download_zip(project_id: str):
508
  if project_id not in projects:
509
- raise HTTPException(status_code=404, detail="Project not found")
510
-
511
  project = projects[project_id]
512
- files = project["files"]
513
- name = project["plan"].get("name", "agent-project")
514
-
515
  buf = io.BytesIO()
516
  with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
517
- for filepath, content in files.items():
518
- zf.writestr(f"{name}/{filepath}", content)
519
  buf.seek(0)
520
-
521
- return StreamingResponse(
522
- buf,
523
- media_type="application/zip",
524
- headers={"Content-Disposition": f'attachment; filename="{name}.zip"'},
525
- )
526
 
527
 
528
  @router.get("/api/file/{project_id}/{filepath:path}")
@@ -533,48 +333,3 @@ async def get_file(project_id: str, filepath: str):
533
  if filepath not in project["files"]:
534
  return JSONResponse({"error": "File not found"}, status_code=404)
535
  return {"filepath": filepath, "content": project["files"][filepath]}
536
-
537
-
538
- # ---------------------------------------------------------------------------
539
- # Helpers
540
- # ---------------------------------------------------------------------------
541
- def _make_requirements(framework: str, provider: str) -> str:
542
- lines = []
543
- if framework in ("crewai", "crewai_flow"):
544
- lines.append("crewai>=0.80.0")
545
- lines.append("crewai-tools>=0.14.0")
546
- elif framework == "langgraph":
547
- lines.append("langgraph>=0.2.0")
548
- lines.append("langchain>=0.3.0")
549
- lines.append("langchain-community>=0.3.0")
550
- elif framework == "react":
551
- lines.append("langchain>=0.3.0")
552
- lines.append("langchain-community>=0.3.0")
553
- elif framework == "watsonx_orchestrate":
554
- lines.append("ibm-watsonx-ai>=1.1.0")
555
-
556
- if provider == "watsonx":
557
- lines.append("ibm-watsonx-ai>=1.1.0")
558
- elif provider == "openai":
559
- lines.append("openai>=1.0.0")
560
-
561
- lines.extend(["python-dotenv>=1.0.0", "pydantic>=2.0.0"])
562
- return "\n".join(sorted(set(lines))) + "\n"
563
-
564
-
565
- def _make_readme(name: str, desc: str, framework: str) -> str:
566
- return f"""# {name}
567
-
568
- {desc}
569
-
570
- ## Framework
571
- {framework}
572
-
573
- ## Quick Start
574
- ```bash
575
- pip install -r requirements.txt
576
- python -m src.{name}.main
577
- ```
578
-
579
- Generated by [Agent Generator](https://github.com/ruslanmv/agent-generator).
580
- """
 
1
  """
2
  HTML page routes for the Agent Generator web UI -- 4-step wizard.
3
 
4
+ Uses the production infrastructure:
5
+ - PlanningService for spec creation
6
+ - BuildService for code generation
7
+ - SecurityValidator for artifact checks
8
  """
9
  from __future__ import annotations
10
 
 
11
  import io
12
  import json
13
  import uuid
 
20
  from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
21
  from fastapi.templating import Jinja2Templates
22
 
23
+ from agent_generator.application.planning_service import plan as plan_spec
24
+ from agent_generator.application.build_service import build_dict
25
  from agent_generator.frameworks import FRAMEWORKS
 
 
 
26
  from agent_generator.web.inference import (
27
  get_inference_client,
28
  get_inference_settings,
 
105
  # Helpers
106
  # ---------------------------------------------------------------------------
107
  def _build_file_tree(files: dict) -> list:
 
108
  tree = []
109
  dirs_seen: set[str] = set()
110
  for filepath in sorted(files.keys()):
 
119
 
120
 
121
  def _validate_files(files: dict[str, str]) -> list[dict]:
122
+ import ast as _ast
 
 
 
123
  results = []
124
  for filepath, content in files.items():
125
  if filepath.endswith(".py"):
126
  try:
127
+ _ast.parse(content, filename=filepath)
128
  results.append({"file": filepath, "status": "ok", "message": "Valid Python"})
129
  except SyntaxError as exc:
130
+ results.append({"file": filepath, "status": "error", "message": f"SyntaxError line {exc.lineno}: {exc.msg}"})
 
 
 
 
131
  elif filepath.endswith((".yaml", ".yml")):
132
  try:
133
  yaml.safe_load(content)
 
137
  return results
138
 
139
 
140
+ def _spec_to_plan_dict(spec) -> dict:
141
+ """Convert a ProjectSpec to the dict format templates expect."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  return {
143
+ "name": spec.name,
144
+ "description": spec.description,
145
+ "framework": spec.framework.value,
146
+ "artifact_mode": spec.artifact_mode.value,
147
+ "agents": [a.model_dump() for a in spec.agents],
148
+ "tasks": [t.model_dump() for t in spec.tasks],
149
+ "tools": [t.model_dump() for t in spec.tools],
150
  }
151
 
152
 
153
+ def _plan_and_build(prompt: str, framework: str = "auto",
154
+ artifact_mode: str = "code_and_yaml") -> tuple[dict, dict[str, str]]:
155
+ """Use production pipeline to plan and build. Returns (plan_dict, files_dict)."""
156
+ fw = framework if framework != "auto" else None
157
+ spec, warnings = plan_spec(prompt, framework=fw)
158
+ result = build_dict(spec)
159
+ plan_dict = _spec_to_plan_dict(spec)
160
+ plan_dict["warnings"] = warnings + result.get("warnings", [])
161
+ return plan_dict, result.get("files", {})
162
+
163
+
164
  # ---------------------------------------------------------------------------
165
  # Routes -- Step 1: Describe
166
  # ---------------------------------------------------------------------------
167
  @router.get("/", response_class=HTMLResponse)
168
  async def home(request: Request):
 
169
  inference = get_inference_client()
170
  return templates.TemplateResponse(
171
+ request=request, name="home.html",
172
+ context={"request": request, "examples": EXAMPLES, "inference_available": inference.available},
 
 
 
 
 
173
  )
174
 
175
 
 
178
  # ---------------------------------------------------------------------------
179
  @router.post("/plan", response_class=HTMLResponse)
180
  async def plan(request: Request, prompt: str = Form(...)):
 
181
  try:
182
+ spec, warnings = plan_spec(prompt)
183
+ plan_data = _spec_to_plan_dict(spec)
184
 
185
+ # Try LLM enhancement via inference client
186
  llm_enhanced = False
187
  inference = get_inference_client()
188
  if inference.available:
189
  llm_plan = inference.generate_plan(prompt)
190
  if llm_plan:
191
+ for key in ("agents", "tasks", "tools"):
192
+ if llm_plan.get(key):
193
+ plan_data[key] = llm_plan[key]
194
  llm_enhanced = True
195
 
196
  return templates.TemplateResponse(
197
+ request=request, name="plan.html",
198
+ context={"request": request, "plan": plan_data, "plan_json": json.dumps(plan_data, indent=2),
199
+ "prompt": prompt, "llm_enhanced": llm_enhanced, "frameworks": FRAMEWORK_LABELS},
 
 
 
 
 
 
 
200
  )
201
  except Exception as e:
202
  return templates.TemplateResponse(
203
+ request=request, name="home.html",
204
+ context={"request": request, "examples": EXAMPLES, "error": f"Planning failed: {e}", "inference_available": False},
 
 
 
 
 
 
205
  )
206
 
207
 
208
  @router.post("/edit-plan", response_class=JSONResponse)
209
  async def edit_plan(request: Request):
 
210
  body = await request.json()
211
  prompt = body.get("prompt", "")
212
  edits = body.get("edits", "")
213
+ combined = f"{prompt}. Additionally: {edits}" if edits else prompt
 
 
 
214
  try:
215
+ spec, warnings = plan_spec(combined)
216
+ return JSONResponse(content={"ok": True, "plan": _spec_to_plan_dict(spec), "llm_enhanced": False})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  except Exception as e:
218
  return JSONResponse(content={"ok": False, "error": str(e)}, status_code=500)
219
 
 
222
  # Routes -- Step 3: Configure
223
  # ---------------------------------------------------------------------------
224
  @router.post("/configure", response_class=HTMLResponse)
225
+ async def configure(request: Request, plan_json: str = Form(...), prompt: str = Form("")):
 
 
 
 
 
226
  try:
227
  plan_data = json.loads(plan_json)
228
  except json.JSONDecodeError:
229
  raise HTTPException(status_code=400, detail="Invalid plan JSON")
230
 
231
+ # Build preview using production infrastructure
 
 
 
 
 
 
 
 
 
232
  try:
233
+ _, preview_files = _plan_and_build(
234
+ plan_data.get("description", "preview"),
235
+ plan_data.get("framework", "crewai"),
236
  )
237
+ preview_tree = _build_file_tree(preview_files)
 
 
 
 
 
 
 
 
 
238
  except Exception:
239
+ preview_tree = []
240
 
241
+ tool_ids = []
242
+ for t in plan_data.get("tools", []):
243
+ if isinstance(t, dict):
244
+ tool_ids.append(t.get("id", ""))
245
+ elif isinstance(t, str):
246
+ tool_ids.append(t)
247
 
248
  return templates.TemplateResponse(
249
+ request=request, name="configure.html",
 
250
  context={
251
+ "request": request, "plan": plan_data, "plan_json": json.dumps(plan_data),
252
+ "prompt": prompt, "frameworks": FRAMEWORK_LABELS,
253
+ "framework_capabilities": FRAMEWORK_CAPABILITIES, "tool_categories": TOOL_CATEGORIES,
254
+ "preview_tree": preview_tree, "selected_tools": tool_ids,
 
 
 
 
 
255
  },
256
  )
257
 
 
269
  provider: str = Form("watsonx"),
270
  tools: list[str] = Form(default=[]),
271
  ):
 
272
  try:
273
  plan_data = json.loads(plan_json)
274
  except json.JSONDecodeError:
275
  raise HTTPException(status_code=400, detail="Invalid plan JSON")
276
 
 
277
  if framework and framework != "auto":
278
  plan_data["framework"] = framework
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
 
280
  try:
281
  effective_prompt = plan_data.get("description", prompt or "agent project")
282
+ plan_result, files = _plan_and_build(effective_prompt, plan_data.get("framework", "crewai"), artifact_mode)
283
+
284
+ plan_result["name"] = plan_data.get("name", plan_result.get("name", "agent-project"))
285
+ plan_result["description"] = plan_data.get("description", plan_result.get("description", ""))
286
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  validation = _validate_files(files)
288
  errors = [v for v in validation if v["status"] == "error"]
289
  ok_count = len([v for v in validation if v["status"] == "ok"])
290
 
 
291
  project_id = str(uuid.uuid4())[:8]
292
  projects[project_id] = {"plan": plan_result, "files": files, "prompt": prompt}
293
 
294
  file_tree = _build_file_tree(files)
295
+ fw_label = FRAMEWORK_LABELS.get(plan_result.get("framework", ""), plan_result.get("framework", ""))
296
 
297
  return templates.TemplateResponse(
298
+ request=request, name="result.html",
299
+ context={"request": request, "project_id": project_id, "plan": plan_result,
300
+ "files": files, "file_tree": file_tree, "fw_label": fw_label,
301
+ "prompt": prompt, "validation": validation,
302
+ "validation_errors": errors, "validation_ok": ok_count},
 
 
 
 
 
 
 
 
 
303
  )
304
  except Exception as e:
305
  return templates.TemplateResponse(
306
+ request=request, name="home.html",
307
+ context={"request": request, "examples": EXAMPLES, "error": f"Generation failed: {e}", "inference_available": False},
 
 
 
 
 
 
308
  )
309
 
310
 
311
  # ---------------------------------------------------------------------------
312
+ # Routes -- Download
313
  # ---------------------------------------------------------------------------
314
  @router.get("/download/{project_id}")
315
+ async def download(project_id: str):
316
  if project_id not in projects:
317
+ return JSONResponse({"error": "Project not found"}, status_code=404)
 
318
  project = projects[project_id]
 
 
 
319
  buf = io.BytesIO()
320
  with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
321
+ for filepath, content in project["files"].items():
322
+ zf.writestr(f"{project['plan']['name']}/{filepath}", content)
323
  buf.seek(0)
324
+ return StreamingResponse(buf, media_type="application/zip",
325
+ headers={"Content-Disposition": f"attachment; filename={project['plan']['name']}.zip"})
 
 
 
 
326
 
327
 
328
  @router.get("/api/file/{project_id}/{filepath:path}")
 
333
  if filepath not in project["files"]:
334
  return JSONResponse({"error": "File not found"}, status_code=404)
335
  return {"filepath": filepath, "content": project["files"][filepath]}