docs+cli: deprecate prompt targets and remove generate_prompt_targets command (#944)

Prompt targets are no longer actively maintained. Mark them as deprecated in
the docs and remove the `planoai generate_prompt_targets` CLI command that
existed only to scaffold them.

Docs
- Add `.. deprecated::` banner to the Prompt Target concept page and to the
  function-calling guide / quickstart sections that walk users through
  configuring prompt targets.
- Relabel the Prompt Target card on the overview page as deprecated.
- Drop the Prompt Targets bullet from the README's Getting Started list.

CLI
- Remove the `generate_prompt_targets` Click command, its registration, and
  the `Utilities` rich-click command group.
- Delete `cli/planoai/targets.py` (the command's only consumer).
- Drop the `planoai prompt_targets` section from the CLI reference page.

Skills
- Delete the `cli-generate` rule, drop it from `plano-cli-operations`
  (description, when-to-use, rules list, execution checklist), and update
  the skills README. Hand-edit AGENTS.md to remove section 6.2 and renumber
  6.3/6.4 so the commit stays scoped (regenerating pulled in unrelated
  drift between rules/ and AGENTS.md).

The runtime gateway, schema, and existing demo configs still accept
`prompt_targets` blocks; this is deprecation, not removal of behavior.
This commit is contained in:
Musa 2026-06-03 13:38:51 -07:00 committed by GitHub
parent b5ebb1beea
commit 1d869641ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 32 additions and 612 deletions

View file

@ -49,7 +49,7 @@ Client → Envoy (prompt_gateway.wasm → llm_gateway.wasm) → Agents/LLM Provi
### Python CLI (cli/planoai/)
Entry point: `main.py`. Built with `rich-click`. Commands: `up`, `down`, `build`, `logs`, `trace`, `init`, `cli_agent`, `generate_prompt_targets`.
Entry point: `main.py`. Built with `rich-click`. Commands: `up`, `down`, `build`, `logs`, `trace`, `init`, `cli_agent`.
### Config (config/)

View file

@ -183,7 +183,6 @@ Ready to try Plano? Check out our comprehensive documentation:
- **[LLM Routing](https://docs.planoai.dev/guides/llm_router.html)** - Route by model name, alias, or intelligent preferences
- **[Agent Orchestration](https://docs.planoai.dev/guides/orchestration.html)** - Build multi-agent workflows
- **[Filter Chains](https://docs.planoai.dev/concepts/filter_chain.html)** - Add guardrails, moderation, and memory hooks
- **[Prompt Targets](https://docs.planoai.dev/concepts/prompt_target.html)** - Turn prompts into deterministic API calls
- **[Observability](https://docs.planoai.dev/guides/observability/observability.html)** - Traces, metrics, and logs
## Contribution

View file

@ -7,7 +7,6 @@ import contextlib
import logging
import rich_click as click
import yaml
from planoai import targets
from planoai.defaults import (
DEFAULT_LLM_LISTENER_PORT,
detect_providers,
@ -622,28 +621,6 @@ def down(docker, verbose):
)
@click.command()
@click.option(
"--f",
"--file",
type=click.Path(exists=True),
required=True,
help="Path to the Python file",
)
def generate_prompt_targets(file):
"""Generats prompt_targets from python methods.
Note: This works for simple data types like ['int', 'float', 'bool', 'str', 'list', 'tuple', 'set', 'dict']:
If you have a complex pydantic data type, you will have to flatten those manually until we add support for it.
"""
print(f"Processing file: {file}")
if not file.endswith(".py"):
print("Error: Input file must be a .py file")
sys.exit(1)
targets.generate_prompt_targets(file)
@click.command()
@click.option(
"--debug",
@ -741,7 +718,6 @@ main.add_command(down)
main.add_command(build)
main.add_command(logs)
main.add_command(cli_agent)
main.add_command(generate_prompt_targets)
main.add_command(init_cmd, name="init")
main.add_command(trace_cmd, name="trace")
main.add_command(chatgpt_cmd, name="chatgpt")

View file

@ -63,9 +63,5 @@ def configure_rich_click(plano_color: str) -> None:
"name": "Observability",
"commands": ["trace", "obs"],
},
{
"name": "Utilities",
"commands": ["generate-prompt-targets"],
},
],
}

View file

@ -1,365 +0,0 @@
import ast
import sys
import yaml
from typing import Any
FLASK_ROUTE_DECORATORS = ["route", "get", "post", "put", "delete", "patch"]
FASTAPI_ROUTE_DECORATORS = ["get", "post", "put", "delete", "patch"]
def detect_framework(tree: Any) -> str:
"""Detect whether the file is using Flask or FastAPI based on imports."""
for node in ast.walk(tree):
if isinstance(node, ast.ImportFrom):
if node.module == "flask":
return "flask"
elif node.module == "fastapi":
return "fastapi"
return "unknown"
def get_route_decorators(node: Any, framework: str) -> list:
"""Extract route decorators based on the framework."""
decorators = []
for decorator in node.decorator_list:
if isinstance(decorator, ast.Call) and isinstance(
decorator.func, ast.Attribute
):
if framework == "flask" and decorator.func.attr in FLASK_ROUTE_DECORATORS:
decorators.append(decorator.func.attr)
elif (
framework == "fastapi"
and decorator.func.attr in FASTAPI_ROUTE_DECORATORS
):
decorators.append(decorator.func.attr)
return decorators
def get_route_path(node: Any, framework: str) -> str:
"""Extract route path based on the framework."""
for decorator in node.decorator_list:
if isinstance(decorator, ast.Call) and decorator.args:
return decorator.args[0].s # Assuming it's a string literal
def is_pydantic_model(annotation: ast.expr, tree: ast.AST) -> bool:
"""Check if a given type annotation is a Pydantic model."""
# We walk through the AST to find class definitions and check if they inherit from Pydantic's BaseModel
if isinstance(annotation, ast.Name):
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef) and node.name == annotation.id:
for base in node.bases:
if isinstance(base, ast.Name) and base.id == "BaseModel":
return True
return False
def get_pydantic_model_fields(model_name: str, tree: ast.AST) -> list:
"""Extract fields from a Pydantic model, handling list, tuple, set, dict types, and direct default values."""
fields = []
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef) and node.name == model_name:
for stmt in node.body:
if isinstance(stmt, ast.AnnAssign):
# Initialize the default field description
field_type = "Unknown: Please Fix This!"
description = "Field, description not present. Please fix."
default_value = None
required = True # Assume the field is required initially
# Check if the field uses Field() with required status and description
if (
stmt.value
and isinstance(stmt.value, ast.Call)
and isinstance(stmt.value.func, ast.Name)
and stmt.value.func.id == "Field"
):
# Extract the description argument inside the Field call
for keyword in stmt.value.keywords:
if keyword.arg == "description" and isinstance(
keyword.value, ast.Str
):
description = keyword.value.s
if keyword.arg == "default":
default_value = keyword.value
# If Ellipsis (...) is used, it means the field is required
if (
stmt.value.args
and isinstance(stmt.value.args[0], ast.Constant)
and stmt.value.args[0].value is Ellipsis
):
required = True
else:
required = False
# Handle direct default values (e.g., name: str = "John Doe")
elif stmt.value is not None:
if isinstance(stmt.value, ast.Constant):
# Set the default value from the assignment (e.g., name: str = "John Doe")
default_value = stmt.value.value
required = (
False # Not required since it has a default value
)
# Always extract the field type, even if there's a default value
if isinstance(stmt.annotation, ast.Subscript):
# Get the base type (list, tuple, set, dict)
base_type = (
stmt.annotation.value.id
if isinstance(stmt.annotation.value, ast.Name)
else "Unknown"
)
# Handle only list, tuple, set, dict and ignore the inner types
if base_type.lower() in ["list", "tuple", "set", "dict"]:
field_type = base_type.lower()
# Handle the ellipsis '...' for required fields if no Field() call
elif (
isinstance(stmt.value, ast.Constant)
and stmt.value.value is Ellipsis
):
required = True
# Handle simple types like str, int, etc.
if isinstance(stmt.annotation, ast.Name):
field_type = stmt.annotation.id
field_info = {
"name": stmt.target.id,
"type": field_type, # Always set the field type
"description": description,
"default": default_value, # Handle direct default values
"required": required,
}
fields.append(field_info)
return fields
def get_function_parameters(node: ast.FunctionDef, tree: ast.AST) -> list:
"""Extract the parameters and their types from the function definition."""
parameters = []
# Extract docstring to find descriptions
docstring = ast.get_docstring(node)
arg_descriptions = extract_arg_descriptions_from_docstring(docstring)
# Extract default values
defaults = [None] * (
len(node.args.args) - len(node.args.defaults)
) + node.args.defaults # Align defaults with args
for arg, default in zip(node.args.args, defaults):
if arg.arg != "self": # Skip 'self' or 'cls' in class methods
param_info = {
"name": arg.arg,
"description": arg_descriptions.get(arg.arg, "[ADD DESCRIPTION]"),
}
# Handle Pydantic model types
if hasattr(arg, "annotation") and is_pydantic_model(arg.annotation, tree):
# Extract and flatten Pydantic model fields
pydantic_fields = get_pydantic_model_fields(arg.annotation.id, tree)
parameters.extend(
pydantic_fields
) # Flatten the model fields into the parameters list
continue # Skip adding the current param_info for the model since we expand the fields
# Handle standard Python types (int, float, str, etc.)
elif hasattr(arg, "annotation") and isinstance(arg.annotation, ast.Name):
if arg.annotation.id in [
"int",
"float",
"bool",
"str",
"list",
"tuple",
"set",
"dict",
]:
param_info["type"] = arg.annotation.id
else:
param_info["type"] = "[UNKNOWN - PLEASE FIX]"
# Handle generic subscript types (e.g., Optional, List[Type], etc.)
elif hasattr(arg, "annotation") and isinstance(
arg.annotation, ast.Subscript
):
if isinstance(
arg.annotation.value, ast.Name
) and arg.annotation.value.id in ["list", "tuple", "set", "dict"]:
param_info["type"] = (
f"{arg.annotation.value.id}" # e.g., "List", "Tuple", etc.
)
else:
param_info["type"] = "[UNKNOWN - PLEASE FIX]"
# Default for unknown types
else:
param_info["type"] = (
"[UNKNOWN - PLEASE FIX]" # If unable to detect type
)
# Handle default values
if default is not None:
if isinstance(default, ast.Constant) or isinstance(
default, ast.NameConstant
):
param_info["default"] = (
default.value
) # Use the default value directly
else:
param_info["default"] = "[UNKNOWN DEFAULT]" # Unknown default type
param_info["required"] = False # Optional since it has a default value
else:
param_info["default"] = None
param_info["required"] = True # Required if no default value
parameters.append(param_info)
return parameters
def get_function_docstring(node: Any) -> str:
"""Extract the function's docstring description if present."""
# Check if the first node is a docstring
if isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Str):
# Get the entire docstring
full_docstring = node.body[0].value.s.strip()
# Split the docstring by double newlines (to separate description from fields like Args)
description = full_docstring.split("\n\n")[0].strip()
return description
return "No description provided."
def extract_arg_descriptions_from_docstring(docstring: str) -> dict:
"""Extract descriptions for function parameters from the 'Args' section of the docstring."""
descriptions = {}
if not docstring:
return descriptions
in_args_section = False
current_param = None
for line in docstring.splitlines():
line = line.strip()
# Detect the start of the 'Args' section
if line.startswith("Args:"):
in_args_section = True
continue # Proceed to the next line after 'Args:'
# End of 'Args' section if no indentation and no colon
if in_args_section and not line.startswith(" ") and ":" not in line:
break # Stop processing if we reach a new section
# Process lines in the 'Args' section
if in_args_section:
if ":" in line:
# Extract parameter name and description
param_name, description = line.split(":", 1)
descriptions[param_name.strip()] = description.strip()
current_param = param_name.strip()
elif current_param and line.startswith(" "):
# Handle multiline descriptions (indented lines)
descriptions[current_param] += f" {line.strip()}"
return descriptions
def generate_prompt_targets(input_file_path: str) -> None:
"""Introspect routes and generate YAML for either Flask or FastAPI."""
with open(input_file_path, "r") as source:
tree = ast.parse(source.read())
# Detect the framework (Flask or FastAPI)
framework = detect_framework(tree)
if framework == "unknown":
print("Could not detect Flask or FastAPI in the file.")
return
# Extract routes
routes = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
route_decorators = get_route_decorators(node, framework)
if route_decorators:
route_path = get_route_path(node, framework)
function_params = get_function_parameters(
node, tree
) # Get parameters for the route
function_docstring = get_function_docstring(node) # Extract docstring
routes.append(
{
"name": node.name,
"path": route_path,
"methods": route_decorators,
"parameters": function_params, # Add parameters to the route
"description": function_docstring, # Add the docstring as the description
}
)
# Generate YAML structure
output_structure = {"prompt_targets": []}
for route in routes:
target = {
"name": route["name"],
"endpoint": [
{
"name": "app_server",
"path": route["path"],
}
],
"description": route["description"], # Use extracted docstring
"parameters": [
{
"name": param["name"],
"type": param["type"],
"description": f"{param['description']}",
**(
{"default": param["default"]}
if "default" in param and param["default"] is not None
else {}
), # Only add default if it's set
"required": param["required"],
}
for param in route["parameters"]
],
}
if route["name"] == "default":
# Special case for `information_extraction` based on your YAML format
target["type"] = "default"
target["auto-llm-dispatch-on-response"] = True
output_structure["prompt_targets"].append(target)
# Output as YAML
print(
yaml.dump(output_structure, sort_keys=False, default_flow_style=False, indent=3)
)
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python targets.py <input_file>")
sys.exit(1)
input_file = sys.argv[1]
# Automatically generate the output file name
if input_file.endswith(".py"):
output_file = input_file.replace(".py", "_prompt_targets.yml")
else:
print("Error: Input file must be a .py file")
sys.exit(1)
# Call the function with the input and generated output file names
generate_prompt_targets(input_file, output_file)
# Example usage:
# python targets.py api.yaml

View file

@ -2,6 +2,15 @@
Prompt Target
=============
.. deprecated:: v0.4.22
**Prompt Targets are deprecated and no longer actively maintained.** This concept is
retained for existing users on older Plano configurations, but new applications should
not adopt it. For deterministic, task-specific workloads, use :ref:`Agents <agents>`
together with :ref:`Function Calling <function_calling>` instead. The
``prompt_targets`` configuration block and related CLI commands will continue to
function for now, but may be removed in a future release.
A Prompt Target is a deterministic, task-specific backend function or API endpoint that your application calls via Plano.
Unlike agents (which handle wide-ranging, open-ended tasks), prompt targets are designed for focused, specific workloads where Plano can add value through input clarification and validation.

View file

@ -57,10 +57,10 @@ Deep dive into essential ideas and mechanisms behind Plano:
Explore Plano's LLM integration options
.. grid-item-card:: :octicon:`workflow` Prompt Target
.. grid-item-card:: :octicon:`workflow` Prompt Target (Deprecated)
:link: ../concepts/prompt_target.html
Understand how Plano handles prompts
Deprecated — kept for existing users. New apps should use Agents.
Guides

View file

@ -247,6 +247,11 @@ You can then ask a follow-up like "Also book me a hotel near JFK" and Plano-Orch
Deterministic API calls with prompt targets
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. deprecated:: v0.4.22
:ref:`Prompt Targets <prompt_target>` are deprecated and no longer actively
maintained. The walkthrough below is preserved for users on existing configs;
new applications should use :ref:`Agents <agents>` instead.
Next, we'll show Plano's deterministic API calling using a single prompt target. We'll build a currency exchange backend powered by `https://api.frankfurter.dev/`, assuming USD as the base currency.
Step 1. Create plano config file

View file

@ -6,6 +6,12 @@ Function Calling
**Function Calling** is a powerful feature in Plano that allows your application to dynamically execute backend functions or services based on user prompts.
This enables seamless integration between natural language interactions and backend operations, turning user inputs into actionable results.
.. deprecated:: v0.4.22
The prompt-target based workflow shown below (see :ref:`Step 2 <function_calling>`)
is deprecated. :ref:`Prompt Targets <prompt_target>` are no longer actively
maintained and may be removed in a future release. For new function-calling
workloads, prefer :ref:`Agents <agents>` with tool definitions.
What is Function Calling?
-------------------------

View file

@ -16,7 +16,6 @@ Quick Navigation
- :ref:`cli_reference_logs`
- :ref:`cli_reference_init`
- :ref:`cli_reference_trace`
- :ref:`cli_reference_prompt_targets`
- :ref:`cli_reference_cli_agent`
@ -260,24 +259,6 @@ Inspect request traces from the local OTLP listener.
- ``--list`` cannot be combined with a specific trace-id target.
.. _cli_reference_prompt_targets:
planoai prompt_targets
----------------------
Generate prompt-target metadata from Python methods.
**Synopsis**
.. code-block:: console
$ planoai prompt_targets --file <python-file>
**Options**
- ``--file, --f <python-file>``: required path to a ``.py`` source file.
.. _cli_reference_cli_agent:
planoai cli_agent

View file

@ -31,9 +31,8 @@
- [5.3 Use `planoai trace` to Inspect Routing Decisions](#use-planoai-trace-to-inspect-routing-decisions)
- [Section 6: CLI Operations](#section-6)
- [6.1 Follow the `planoai up` Validation Workflow Before Debugging Runtime Issues](#follow-the-planoai-up-validation-workflow-before-debugging-runtime-issues)
- [6.2 Generate Prompt Targets from Python Functions with `planoai generate_prompt_targets`](#generate-prompt-targets-from-python-functions-with-planoai-generateprompttargets)
- [6.3 Use `planoai cli_agent` to Connect Claude Code Through Plano](#use-planoai-cliagent-to-connect-claude-code-through-plano)
- [6.4 Use `planoai init` Templates to Bootstrap New Projects Correctly](#use-planoai-init-templates-to-bootstrap-new-projects-correctly)
- [6.2 Use `planoai cli_agent` to Connect Claude Code Through Plano](#use-planoai-cliagent-to-connect-claude-code-through-plano)
- [6.3 Use `planoai init` Templates to Bootstrap New Projects Correctly](#use-planoai-init-templates-to-bootstrap-new-projects-correctly)
- [Section 7: Deployment & Security](#section-7)
- [7.1 Understand Plano's Docker Network Topology for Agent URL Configuration](#understand-planos-docker-network-topology-for-agent-url-configuration)
- [7.2 Use PostgreSQL State Storage for Multi-Turn Conversations in Production](#use-postgresql-state-storage-for-multi-turn-conversations-in-production)
@ -1377,99 +1376,7 @@ Reference: https://github.com/katanemo/archgw
---
### 6.2 Generate Prompt Targets from Python Functions with `planoai generate_prompt_targets`
**Impact:** `MEDIUM` — Manually writing prompt_targets YAML for existing Python APIs is error-prone — the generator introspects function signatures and produces correct YAML automatically
**Tags:** `cli`, `generate`, `prompt-targets`, `python`, `code-generation`
## Generate Prompt Targets from Python Functions with `planoai generate_prompt_targets`
`planoai generate_prompt_targets` introspects Python function signatures and docstrings to generate `prompt_targets` YAML for your Plano config. This is the fastest way to expose existing Python APIs as LLM-callable functions without manually writing the YAML schema.
**Python function requirements for generation:**
- Use simple type annotations: `int`, `float`, `bool`, `str`, `list`, `tuple`, `set`, `dict`
- Include a docstring describing what the function does (becomes the `description`)
- Complex Pydantic models must be flattened into primitive typed parameters first
**Example Python file:**
```python
# api.py
def get_stock_quote(symbol: str, exchange: str = "NYSE") -> dict:
"""Get the current stock price and trading data for a given stock symbol.
Returns price, volume, market cap, and 24h change percentage.
"""
# Implementation calls stock API
pass
def get_weather_forecast(city: str, days: int = 3, units: str = "celsius") -> dict:
"""Get the weather forecast for a city.
Returns temperature, precipitation, and conditions for the specified number of days.
"""
pass
def search_flights(origin: str, destination: str, date: str, passengers: int = 1) -> list:
"""Search for available flights between two airports on a given date.
Date format: YYYY-MM-DD. Returns list of flight options with prices.
"""
pass
```
**Running the generator:**
```bash
planoai generate_prompt_targets --file api.py
```
**Generated output (add to your config.yaml):**
```yaml
prompt_targets:
- name: get_stock_quote
description: Get the current stock price and trading data for a given stock symbol.
parameters:
- name: symbol
type: str
required: true
- name: exchange
type: str
required: false
default: NYSE
# Add endpoint manually:
endpoint:
name: stock_api
path: /quote?symbol={symbol}&exchange={exchange}
- name: get_weather_forecast
description: Get the weather forecast for a city.
parameters:
- name: city
type: str
required: true
- name: days
type: int
required: false
default: 3
- name: units
type: str
required: false
default: celsius
endpoint:
name: weather_api
path: /forecast?city={city}&days={days}&units={units}
```
After generation, manually add the `endpoint` blocks pointing to your actual API. The generator produces the schema; you wire in the connectivity.
Reference: https://github.com/katanemo/archgw
---
### 6.3 Use `planoai cli_agent` to Connect Claude Code Through Plano
### 6.2 Use `planoai cli_agent` to Connect Claude Code Through Plano
**Impact:** `MEDIUM-HIGH` — Running Claude Code directly against provider APIs bypasses Plano's routing, observability, and guardrails — cli_agent routes all Claude Code traffic through your configured Plano instance
**Tags:** `cli`, `cli-agent`, `claude`, `coding-agent`, `integration`
@ -1562,7 +1469,7 @@ Reference: [https://github.com/katanemo/archgw](https://github.com/katanemo/arch
---
### 6.4 Use `planoai init` Templates to Bootstrap New Projects Correctly
### 6.3 Use `planoai init` Templates to Bootstrap New Projects Correctly
**Impact:** `MEDIUM` — Starting from a blank config.yaml leads to missing required fields and common structural mistakes — templates provide validated, idiomatic starting points
**Tags:** `cli`, `init`, `templates`, `getting-started`, `project-setup`

View file

@ -63,7 +63,7 @@ After installation, these skills are available to your coding agent and can be i
- `plano-agent-orchestration` - Agent registration and routing descriptions
- `plano-filter-guardrails` - MCP filters, guardrail messaging, filter ordering
- `plano-observability-debugging` - Tracing setup, span attributes, trace analysis
- `plano-cli-operations` - `planoai up`, `cli_agent`, init, prompt target generation
- `plano-cli-operations` - `planoai up`, `cli_agent`, init
- `plano-deployment-security` - Docker networking, health checks, state storage
- `plano-advanced-patterns` - Multi-listener architecture and prompt target schema design
@ -110,7 +110,7 @@ skills/
| 3 | `agent-` | Agent Orchestration | Descriptions, agent registration |
| 4 | `filter-` | Filter Chains & Guardrails | Ordering, MCP integration, guardrails |
| 5 | `observe-` | Observability & Debugging | Tracing, trace inspection, span attributes |
| 6 | `cli-` | CLI Operations | Startup, CLI agent, init, code generation |
| 6 | `cli-` | CLI Operations | Startup, CLI agent, init |
| 7 | `deploy-` | Deployment & Security | Docker networking, state storage, health checks |
| 8 | `advanced-` | Advanced Patterns | Prompt targets, rate limits, multi-listener |

View file

@ -1,6 +1,6 @@
---
name: plano-cli-operations
description: Apply Plano CLI best practices. Use for startup troubleshooting, cli_agent workflows, prompt target generation, and template-based project bootstrapping.
description: Apply Plano CLI best practices. Use for startup troubleshooting, cli_agent workflows, and template-based project bootstrapping.
license: Apache-2.0
metadata:
author: katanemo
@ -15,20 +15,17 @@ Use this skill when the task is primarily operational and CLI-driven.
- "Fix `planoai up` failures"
- "Use `planoai cli_agent` with coding agents"
- "Generate prompt targets from Python functions"
- "Bootstrap a project with `planoai init` templates"
## Apply These Rules
- `cli-startup`
- `cli-agent`
- `cli-generate`
- `cli-init`
## Execution Checklist
1. Follow startup validation order before deep debugging.
2. Use `cli_agent` to route coding-agent traffic through Plano.
3. Generate prompt target schema, then wire endpoint details explicitly.
4. Start from templates for reliable first-time setup.
5. Provide a compact runbook with exact CLI commands.
3. Start from templates for reliable first-time setup.
4. Provide a compact runbook with exact CLI commands.

View file

@ -1,91 +0,0 @@
---
title: Generate Prompt Targets from Python Functions with `planoai generate_prompt_targets`
impact: MEDIUM
impactDescription: Manually writing prompt_targets YAML for existing Python APIs is error-prone — the generator introspects function signatures and produces correct YAML automatically
tags: cli, generate, prompt-targets, python, code-generation
---
## Generate Prompt Targets from Python Functions with `planoai generate_prompt_targets`
`planoai generate_prompt_targets` introspects Python function signatures and docstrings to generate `prompt_targets` YAML for your Plano config. This is the fastest way to expose existing Python APIs as LLM-callable functions without manually writing the YAML schema.
**Python function requirements for generation:**
- Use simple type annotations: `int`, `float`, `bool`, `str`, `list`, `tuple`, `set`, `dict`
- Include a docstring describing what the function does (becomes the `description`)
- Complex Pydantic models must be flattened into primitive typed parameters first
**Example Python file:**
```python
# api.py
def get_stock_quote(symbol: str, exchange: str = "NYSE") -> dict:
"""Get the current stock price and trading data for a given stock symbol.
Returns price, volume, market cap, and 24h change percentage.
"""
# Implementation calls stock API
pass
def get_weather_forecast(city: str, days: int = 3, units: str = "celsius") -> dict:
"""Get the weather forecast for a city.
Returns temperature, precipitation, and conditions for the specified number of days.
"""
pass
def search_flights(origin: str, destination: str, date: str, passengers: int = 1) -> list:
"""Search for available flights between two airports on a given date.
Date format: YYYY-MM-DD. Returns list of flight options with prices.
"""
pass
```
**Running the generator:**
```bash
planoai generate_prompt_targets --file api.py
```
**Generated output (add to your config.yaml):**
```yaml
prompt_targets:
- name: get_stock_quote
description: Get the current stock price and trading data for a given stock symbol.
parameters:
- name: symbol
type: str
required: true
- name: exchange
type: str
required: false
default: NYSE
# Add endpoint manually:
endpoint:
name: stock_api
path: /quote?symbol={symbol}&exchange={exchange}
- name: get_weather_forecast
description: Get the weather forecast for a city.
parameters:
- name: city
type: str
required: true
- name: days
type: int
required: false
default: 3
- name: units
type: str
required: false
default: celsius
endpoint:
name: weather_api
path: /forecast?city={city}&days={days}&units={units}
```
After generation, manually add the `endpoint` blocks pointing to your actual API. The generator produces the schema; you wire in the connectivity.
Reference: https://github.com/katanemo/archgw