mirror of
https://github.com/katanemo/plano.git
synced 2026-06-05 14:45:15 +02:00
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:
parent
b5ebb1beea
commit
1d869641ff
14 changed files with 32 additions and 612 deletions
|
|
@ -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/)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -63,9 +63,5 @@ def configure_rich_click(plano_color: str) -> None:
|
|||
"name": "Observability",
|
||||
"commands": ["trace", "obs"],
|
||||
},
|
||||
{
|
||||
"name": "Utilities",
|
||||
"commands": ["generate-prompt-targets"],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
-------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
101
skills/AGENTS.md
101
skills/AGENTS.md
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue