Spaces:
Sleeping
Sleeping
v0.2.1 production RC — AST security validator, OutputTier, reference validation
Browse files- agent_generator_pkg/pyproject.toml +5 -1
- agent_generator_pkg/src/agent_generator/__init__.py +1 -1
- 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/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/build_service.py +4 -2
- agent_generator_pkg/src/agent_generator/application/planning_service.py +12 -6
- agent_generator_pkg/src/agent_generator/cli.py +1 -1
- agent_generator_pkg/src/agent_generator/domain/__pycache__/project_spec.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/domain/project_spec.py +36 -27
- agent_generator_pkg/src/agent_generator/validators/__pycache__/security_validator.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/validators/security_validator.py +52 -15
- agent_generator_pkg/src/agent_generator/web/routes/__pycache__/api.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/web/routes/__pycache__/pages.cpython-311.pyc +0 -0
- agent_generator_pkg/src/agent_generator/web/routes/api.py +41 -107
- agent_generator_pkg/src/agent_generator/web/routes/pages.py +83 -328
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.
|
| 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.
|
| 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
|
|
|
|
| 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.
|
| 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.
|
| 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,
|
| 115 |
-
validation =
|
| 116 |
-
return spec,
|
| 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 =
|
| 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.
|
| 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
|
| 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
|
| 25 |
-
|
|
|
|
|
|
|
| 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="
|
| 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:
|
| 84 |
-
metadata: dict[str, str] = Field(default_factory=dict
|
| 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 |
-
"""
|
| 2 |
from __future__ import annotations
|
| 3 |
|
|
|
|
|
|
|
| 4 |
from agent_generator.domain.artifact_bundle import ArtifactBundle, ValidationIssue
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
("
|
| 10 |
-
("
|
| 11 |
-
("
|
| 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 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 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 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 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 |
-
|
| 171 |
-
|
| 172 |
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 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=
|
| 210 |
-
framework=
|
| 211 |
files=files,
|
| 212 |
-
diagram=diagram,
|
| 213 |
-
errors=errors,
|
| 214 |
-
warnings=warnings,
|
| 215 |
-
validation_passed=
|
| 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 |
-
|
| 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 |
-
|
| 234 |
-
|
| 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 |
-
|
| 244 |
-
|
| 245 |
-
generator = generator_cls()
|
| 246 |
-
code = generator.generate_code(workflow, settings, mcp=mcp)
|
| 247 |
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
return GenerateResponse(
|
| 253 |
code=code,
|
| 254 |
-
diagram=
|
| 255 |
-
framework=
|
| 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 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 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.
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 152 |
-
"""
|
| 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":
|
| 219 |
-
"description":
|
| 220 |
-
"framework": framework,
|
| 221 |
-
"artifact_mode":
|
| 222 |
-
"agents": agents,
|
| 223 |
-
"tasks": tasks,
|
| 224 |
-
"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 |
-
|
| 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 |
-
|
| 254 |
-
|
| 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 |
-
|
|
|
|
|
|
|
| 263 |
llm_enhanced = True
|
| 264 |
|
| 265 |
return templates.TemplateResponse(
|
| 266 |
-
request=request,
|
| 267 |
-
|
| 268 |
-
|
| 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 |
-
|
| 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 |
-
|
| 297 |
-
|
| 298 |
-
combined_prompt = f"{prompt}. Additionally: {edits}" if edits else prompt
|
| 299 |
-
|
| 300 |
try:
|
| 301 |
-
|
| 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 |
-
#
|
| 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 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
)
|
| 354 |
-
|
| 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 |
-
|
| 366 |
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
|
| 369 |
return templates.TemplateResponse(
|
| 370 |
-
request=request,
|
| 371 |
-
name="configure.html",
|
| 372 |
context={
|
| 373 |
-
"request": request,
|
| 374 |
-
"
|
| 375 |
-
"
|
| 376 |
-
"
|
| 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 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 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
|
| 473 |
|
| 474 |
return templates.TemplateResponse(
|
| 475 |
-
request=request,
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 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 |
-
|
| 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
|
| 505 |
# ---------------------------------------------------------------------------
|
| 506 |
@router.get("/download/{project_id}")
|
| 507 |
-
async def
|
| 508 |
if project_id not in projects:
|
| 509 |
-
|
| 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 |
-
|
| 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]}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|