Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions fastapi_gen/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,18 @@ def new(output: Path | None, no_input: bool, name: str | None, minimal: bool) ->
help="Enable AntV advanced-diagram tools (flowchart, mind-map, sankey, ...) via an "
"mcp-server-chart sidecar, plus an interactive Leaflet/OpenStreetMap map tool",
)
@click.option(
"--code-execution",
is_flag=True,
default=False,
help="Enable the run_python code-execution tool backed by the Monty sandbox (PydanticAI only)",
)
@click.option(
"--skills",
is_flag=True,
default=False,
help="Enable the skills system (SkillsToolset loads SKILL.md files from backend/skills/, PydanticAI only)",
)
@click.option("--session-management", is_flag=True, help="Enable session management")
@click.option(
"--reverse-proxy",
Expand Down Expand Up @@ -754,6 +766,8 @@ def create(
web_fetch: bool,
charts: bool,
antv_charts: bool,
code_execution: bool,
skills: bool,
session_management: bool,
reverse_proxy: str,
kubernetes: bool,
Expand Down Expand Up @@ -1183,6 +1197,8 @@ def create(
enable_web_fetch=web_fetch,
enable_charts=charts,
enable_antv_charts=antv_charts,
enable_code_execution=code_execution,
enable_skills=skills,
enable_session_management=session_management,
reverse_proxy=_rp_map[reverse_proxy],
enable_kubernetes=kubernetes,
Expand Down
4 changes: 4 additions & 0 deletions fastapi_gen/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@ class ProjectConfig(BaseModel):
enable_web_fetch: bool = False
enable_charts: bool = False
enable_antv_charts: bool = False
enable_code_execution: bool = False
enable_skills: bool = False
use_telegram: bool = False
use_slack: bool = False
enable_cors: bool = True
Expand Down Expand Up @@ -813,6 +815,8 @@ def to_cookiecutter_context(self) -> dict[str, Any]:
"enable_charts": self.enable_charts,
"charts_channel_png": self.enable_charts and (self.use_slack or self.use_telegram),
"enable_antv_charts": self.enable_antv_charts,
"enable_code_execution": self.enable_code_execution,
"enable_skills": self.enable_skills,
"enable_webhooks": self.enable_webhooks,
# Legacy fixed values (WebSocket always uses JWT)
"websocket_auth": "jwt",
Expand Down
66 changes: 66 additions & 0 deletions fastapi_gen/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,6 +924,52 @@ def prompt_antv_charts() -> bool:
)


def prompt_code_execution() -> bool:
"""Prompt for the Monty-backed run_python code-execution tool (PydanticAI only)."""
console.print()
console.print("[bold cyan]Code Execution (Monty sandbox)[/]")
console.print(
"Adds a `run_python` tool that lets the agent execute Python in a sandboxed "
"interpreter (pydantic-monty). The model can compute projections, run "
"aggregations, and call create_chart/create_map directly from inside the "
"code. Restricted stdlib: math, asyncio, json, datetime, re — no numpy/pandas. "
"PydanticAI only. Activate at runtime with ENABLE_CODE_EXECUTION=true."
)
console.print()

return cast(
bool,
_check_cancelled(
questionary.confirm(
"Enable the run_python code-execution tool (PydanticAI only)?",
default=False,
).ask()
),
)


def prompt_skills() -> bool:
"""Prompt for the skills system (SkillsToolset, PydanticAI only)."""
console.print()
console.print("[bold cyan]Skills System (pydantic-ai-skills)[/]")
console.print(
"Adds a SkillsToolset that loads SKILL.md files from `backend/skills/` as "
"agent tools. Drop in your own skills; pair with code execution (Monty "
"sandbox) for skills that compute. PydanticAI only."
)
console.print()

return cast(
bool,
_check_cancelled(
questionary.confirm(
"Enable the skills system (PydanticAI only)?",
default=False,
).ask()
),
)


def prompt_langsmith() -> bool:
"""Prompt for LangSmith observability."""
return cast(
Expand Down Expand Up @@ -1434,6 +1480,8 @@ def run_interactive_prompts() -> ProjectConfig:
"enable_web_fetch": False,
"enable_charts": False,
"enable_antv_charts": False,
"enable_code_execution": False,
"enable_skills": False,
"rag_features": RAGFeatures(),
"orm_type": OrmType.SQLALCHEMY,
"sandbox_backend": "state",
Expand Down Expand Up @@ -1598,6 +1646,18 @@ def step_antv_charts() -> None:
else:
state["enable_antv_charts"] = prompt_antv_charts()

def step_code_execution() -> None:
if state["ai_framework"] == AIFrameworkType.PYDANTIC_AI:
state["enable_code_execution"] = prompt_code_execution()
else:
state["enable_code_execution"] = False

def step_skills() -> None:
if state["ai_framework"] == AIFrameworkType.PYDANTIC_AI:
state["enable_skills"] = prompt_skills()
else:
state["enable_skills"] = False

def step_langsmith() -> None:
if state["ai_framework"] in (
AIFrameworkType.LANGCHAIN,
Expand Down Expand Up @@ -1669,6 +1729,8 @@ def step_marketing() -> None:
("RAG", step_rag_config),
("Chart Tool", step_charts),
("AntV Diagrams & Maps", step_antv_charts),
("Code Execution", step_code_execution),
("Skills System", step_skills),
("LangSmith", step_langsmith),
("Messaging Channels", step_channels),
("Teams & Billing", step_teams_billing),
Expand Down Expand Up @@ -1716,6 +1778,8 @@ def step_marketing() -> None:
enable_web_fetch = state["enable_web_fetch"]
enable_charts = state["enable_charts"]
enable_antv_charts = state["enable_antv_charts"]
enable_code_execution = state["enable_code_execution"]
enable_skills = state["enable_skills"]
rag_features = state["rag_features"]
enable_langsmith = state["enable_langsmith"]
use_telegram = state["use_telegram"]
Expand Down Expand Up @@ -1759,6 +1823,8 @@ def step_marketing() -> None:
enable_web_fetch=enable_web_fetch,
enable_charts=enable_charts,
enable_antv_charts=enable_antv_charts,
enable_code_execution=enable_code_execution,
enable_skills=enable_skills,
use_telegram=use_telegram,
use_slack=use_slack,
rate_limit_requests=rate_limit_requests,
Expand Down
2 changes: 2 additions & 0 deletions template/VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,8 @@ These variables are set automatically by the generator.
| `enable_charts` | bool | `false` | Enable the chart-generation tool (line/bar/pie/area/scatter); interactive in web chat, PNG on Slack/Telegram | Requires an AI framework |
| `charts_channel_png` | bool | `false` | Computed: render charts to PNG for messaging channels | `enable_charts` and (`use_slack` or `use_telegram`) |
| `enable_antv_charts` | bool | `false` | Adds the interactive `create_map` tool (Leaflet/OpenStreetMap, works out of the box) plus AntV advanced-diagram tools (flowchart, mind-map, org-chart, sankey, ...) via an `mcp-server-chart` Docker sidecar. Wired into all 5 frameworks. The AntV diagrams need the sidecar running (`docker compose --profile antv up -d`) and `ENABLE_ANTV_CHARTS=true` at runtime; maps work without it. Present in the production compose but profile-gated — off by default and publishes no host port, so it's opt-in per environment; for prod, self-host GPT-Vis-SSR via `ANTV_VIS_REQUEST_SERVER` rather than AntV's public render backend | Requires an AI framework |
| `enable_code_execution` | bool | `false` | Adds a `run_python` tool backed by the Monty sandboxed Python interpreter (`pydantic-monty`). The model can compute projections, run aggregations, and call `create_chart`/`create_map` from inside the sandbox — charts render as interactive Recharts cards immediately. Sandbox allows `math`, `asyncio`, `json`, `datetime`, `re`; no `statistics`, `random`, `itertools`, numpy/pandas. Activated at runtime with `ENABLE_CODE_EXECUTION=true`. **PydanticAI only.** Temporary shim — swap to the official `CodeExecutionToolset` when it ships in pydantic-ai | Requires `use_pydantic_ai` |
| `enable_skills` | bool | `false` | Adds a `pydantic-ai-skills` `SkillsToolset` that loads `SKILL.md` files from `backend/skills/` as agent tools (the model picks a skill, then follows its instructions). Ships the loader only — drop your own skills into `backend/skills/`; the toolset no-ops when the directory is empty. Pairs well with `enable_code_execution` for skills that compute. **PydanticAI only.** | Requires `use_pydantic_ai` |

**Notes:**

Expand Down
2 changes: 2 additions & 0 deletions template/cookiecutter.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@
"enable_charts": false,
"charts_channel_png": false,
"enable_antv_charts": false,
"enable_code_execution": false,
"enable_skills": false,
"enable_webhooks": false,
"websocket_auth": "jwt",
"websocket_auth_jwt": true,
Expand Down
4 changes: 4 additions & 0 deletions template/hooks/post_gen_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
enable_charts = "{{ cookiecutter.enable_charts }}" == "True"
charts_channel_png = "{{ cookiecutter.charts_channel_png }}" == "True"
enable_antv_charts = "{{ cookiecutter.enable_antv_charts }}" == "True"
enable_code_execution = "{{ cookiecutter.enable_code_execution }}" == "True"
use_pydantic_deep = "{{ cookiecutter.use_pydantic_deep }}" == "True"
use_telegram = "{{ cookiecutter.use_telegram }}" == "True"
use_slack = "{{ cookiecutter.use_slack }}" == "True"
Expand Down Expand Up @@ -118,6 +119,7 @@ def remove_dir(path: str) -> None:
# --- AI Agent files (remove unused framework-specific files) ---
if not use_pydantic_ai:
remove_file(os.path.join(backend_app, "agents", "assistant.py"))
remove_file(os.path.join(backend_app, "agents", "tools", "ask_user_tool.py"))
if not use_langchain:
remove_file(os.path.join(backend_app, "agents", "langchain_assistant.py"))
if not use_langgraph:
Expand Down Expand Up @@ -152,6 +154,8 @@ def remove_dir(path: str) -> None:
frontend_src = os.path.join(os.getcwd(), "frontend", "src")
remove_file(os.path.join(frontend_src, "components", "chat", "map-leaflet.tsx"))
remove_file(os.path.join(frontend_src, "components", "chat", "map-message.tsx"))
if not enable_code_execution:
remove_file(os.path.join(backend_app, "agents", "tools", "code_execution.py"))

# --- No-AI mode: remove all AI/chat/conversation files ---
if not use_ai:
Expand Down
7 changes: 7 additions & 0 deletions template/{{cookiecutter.project_slug}}/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,13 @@ ANTV_VIS_REQUEST_SERVER=
# default (drops basic charts that overlap create_chart, and China-only maps).
ANTV_DISABLED_TOOLS=
{%- endif %}
{%- if cookiecutter.enable_code_execution %}

# === Code execution (Monty sandbox) ===
ENABLE_CODE_EXECUTION=true
CODE_EXECUTION_TIMEOUT_SECS=10
CODE_EXECUTION_MAX_ALLOCATIONS=50000000
{%- endif %}
{%- if cookiecutter.enable_langsmith %}

# === LangSmith Observability ===
Expand Down
108 changes: 106 additions & 2 deletions template/{{cookiecutter.project_slug}}/backend/app/agents/assistant.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
"""

import logging
from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from typing import Any

from pydantic_ai import Agent, ModelRetry{%- if cookiecutter.enable_rag %}, RunContext{%- endif %}
from pydantic_ai import Agent{%- if cookiecutter.enable_rag %}, ModelRetry{%- endif %}, RunContext
from pydantic_ai.capabilities import (
ReinjectSystemPrompt,
Thinking,
Expand Down Expand Up @@ -48,6 +49,7 @@
from app.agents.prompts import get_system_prompt_with_rag
{%- endif %}
from app.agents.tools import get_current_datetime
from app.agents.tools.ask_user_tool import MAX_QUESTIONS, QuestionItem, format_answers
{%- if cookiecutter.enable_rag %}
from app.agents.tools.rag_tool import search_knowledge_base
{%- endif %}
Expand All @@ -58,6 +60,15 @@
from app.agents.tools.antv_chart import get_antv_toolset
from app.agents.tools.map_tool import MapMarker, create_map
{%- endif %}
{%- if cookiecutter.enable_code_execution %}
from app.agents.tools.code_execution import EmitToolEvent
from app.agents.tools.code_execution import run_python as run_python_code
{%- endif %}
{%- if cookiecutter.enable_skills %}
from pathlib import Path

from pydantic_ai_skills import SkillsToolset
{%- endif %}
from app.core.config import settings

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -135,6 +146,9 @@ def _build_model(model_name: str):
{%- endif %}


AskUserCallback = Callable[[list[dict[str, Any]]], Awaitable[list[dict[str, Any]]]]


@dataclass
class Deps:
"""Dependencies for the assistant agent.
Expand All @@ -149,6 +163,10 @@ class Deps:
kb_collection_names: list[str] = field(default_factory=list)
{%- endif %}
metadata: dict[str, Any] = field(default_factory=dict)
ask_user: AskUserCallback | None = None
{%- if cookiecutter.enable_code_execution %}
emit_tool_event: EmitToolEvent | None = None
{%- endif %}


class AssistantAgent:
Expand Down Expand Up @@ -212,14 +230,24 @@ def _create_agent(self) -> Agent[Deps, str]:
# None when AntV is disabled or the sidecar is unavailable.
antv_toolset = get_antv_toolset()
toolsets = [antv_toolset] if antv_toolset is not None else []
{%- else %}
{%- if cookiecutter.enable_skills %}
toolsets: list = []
{%- endif %}
{%- endif %}
{%- if cookiecutter.enable_skills %}

skills_dir = Path(__file__).parent.parent.parent / "skills"
if skills_dir.exists():
toolsets.append(SkillsToolset(directories=[str(skills_dir)]))
{%- endif %}

agent = Agent[Deps, str](
model=model,
model_settings=model_settings,
system_prompt=self.system_prompt,
capabilities=capabilities,
{%- if cookiecutter.enable_antv_charts %}
{%- if cookiecutter.enable_antv_charts or cookiecutter.enable_skills %}
toolsets=toolsets,
{%- endif %}
)
Expand Down Expand Up @@ -337,6 +365,82 @@ def create_map_tool(
)
{%- endif %}

@agent.tool
async def ask_user(ctx: RunContext[Deps], questions: list[QuestionItem]) -> str:
"""Ask the user one or more questions and wait for their answers.

Use this when a decision or missing detail would materially change what
you do next and you can't reasonably assume it. You may pass several
questions at once — the user answers them one after another and you get
all the answers back together (good for an intake/setup flow). You can
also call this again later to follow up on what they said. Prefer
answering directly when the request is already clear.

Args:
questions: The questions to ask. Each has the question text, optional
suggested `options`, and `allow_custom` (whether a free-form
answer is allowed, default True).

Returns:
The user's answers as a Q/A transcript, with skipped questions marked.
"""
if ctx.deps.ask_user is None:
return (
"User interaction is unavailable here; proceed with a reasonable "
"assumption and state it briefly."
)
if not questions:
return "No questions were provided."
payload = [q.model_dump() for q in questions[:MAX_QUESTIONS]]
answers = await ctx.deps.ask_user(payload)
return format_answers(payload, answers)

{%- if cookiecutter.enable_code_execution %}

if settings.ENABLE_CODE_EXECUTION:

@agent.tool
async def run_python(ctx: RunContext[Deps], code: str) -> str:
"""Run Python in a sandbox to compute and to build visualizations.

Use this for multi-step number-crunching (projections, aggregations,
simulations) and whenever you want to produce several charts at once.
{%- if cookiecutter.enable_charts or cookiecutter.enable_antv_charts %}
Inside the code you can call:
- ``create_chart(chart_type, title, data, series=None, x_key="x", style=None)``
{%- if cookiecutter.enable_antv_charts %}
- ``create_map(title, markers, center=None, zoom=None)``
{%- endif %}
- ``current_datetime()``
``create_chart``{%- if cookiecutter.enable_antv_charts %}/``create_map``{%- endif %} are async — call them with ``await``,
and run several in parallel with ``await asyncio.gather(...)``. Each one
renders to the user immediately as an interactive chart/map.
{%- else %}
Inside the code you can call ``current_datetime()``.
{%- endif %}
SANDBOX LIMITATIONS — violating these causes "Execution failed" errors:
- NO comma thousands separator in f-strings: ``{x:,}`` or ``{x:,.2f}``
CRASHES. Use ``f"${int(x)}"`` or ``f"{x:.2f}"`` instead.
- NO ``statistics``, ``random``, ``itertools``, ``collections``,
numpy, pandas — compute stats manually with loops/math.
- NO file I/O, network calls, or OS access.
- NO ``import`` of any module not in: math, asyncio, json, datetime, re.
- Walrus operator ``:=`` is unsupported.
- f-string expressions must be simple: no ``!r``, no ``=`` suffix
(``{x=}`` debug format crashes). Use ``print(f"x = {x}")``.

Print intermediate values you want to keep; don't paste the returned
chart JSON back to the user.

Args:
code: The Python source to execute.

Returns:
The captured stdout plus the final expression value, or an error
message you can read and fix.
"""
return await run_python_code(code, emit=ctx.deps.emit_tool_event)
{%- endif %}

@staticmethod
def _build_model_history(
Expand Down
Loading
Loading