mirror of
https://github.com/katanemo/plano.git
synced 2026-06-11 15:05:14 +02:00
Merge remote-tracking branch 'origin/main' into stoff81/chatgpt5.5
This commit is contained in:
commit
1538349367
39 changed files with 325 additions and 635 deletions
23
.github/workflows/ci.yml
vendored
23
.github/workflows/ci.yml
vendored
|
|
@ -110,6 +110,25 @@ jobs:
|
|||
# ── Zero-config path: `planoai up` with no args, no plano.yaml in cwd.
|
||||
# Exercises the synthesize_default_config branch in cli/planoai/main.py
|
||||
# which is otherwise never hit by the smoke test above.
|
||||
#
|
||||
# Pre-seed ~/.plano/ from the freshly-built artifacts so the CLI's
|
||||
# cached-download path hits in step (2) of ensure_wasm_plugins /
|
||||
# ensure_brightstaff_binary. Without this, running from outside the
|
||||
# repo means find_repo_root() returns None, the local-build short-
|
||||
# circuit is skipped, and the CLI tries to download from a GitHub
|
||||
# release that does not yet exist for the in-flight version on
|
||||
# release-bump PRs (e.g. 0.4.23 before publish-binaries has run).
|
||||
- name: Seed ~/.plano cache for zero-config test
|
||||
run: |
|
||||
VERSION=$(sed -nE 's/^__version__ = "(.*)"$/\1/p' cli/planoai/__init__.py)
|
||||
mkdir -p ~/.plano/plugins ~/.plano/bin
|
||||
cp crates/target/wasm32-wasip1/release/prompt_gateway.wasm ~/.plano/plugins/
|
||||
cp crates/target/wasm32-wasip1/release/llm_gateway.wasm ~/.plano/plugins/
|
||||
cp crates/target/release/brightstaff ~/.plano/bin/
|
||||
chmod +x ~/.plano/bin/brightstaff
|
||||
echo "$VERSION" > ~/.plano/plugins/wasm.version
|
||||
echo "$VERSION" > ~/.plano/bin/brightstaff.version
|
||||
|
||||
- name: Zero-config smoke test
|
||||
env:
|
||||
OPENAI_API_KEY: test-key-not-used
|
||||
|
|
@ -164,13 +183,13 @@ jobs:
|
|||
load: true
|
||||
tags: |
|
||||
${{ env.PLANO_DOCKER_IMAGE }}
|
||||
${{ env.DOCKER_IMAGE }}:0.4.22
|
||||
${{ env.DOCKER_IMAGE }}:0.4.23
|
||||
${{ env.DOCKER_IMAGE }}:latest
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Save image as artifact
|
||||
run: docker save ${{ env.PLANO_DOCKER_IMAGE }} ${{ env.DOCKER_IMAGE }}:0.4.22 ${{ env.DOCKER_IMAGE }}:latest -o /tmp/plano-image.tar
|
||||
run: docker save ${{ env.PLANO_DOCKER_IMAGE }} ${{ env.DOCKER_IMAGE }}:0.4.23 ${{ env.DOCKER_IMAGE }}:latest -o /tmp/plano-image.tar
|
||||
|
||||
- name: Upload image artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ Plano solves this by moving core delivery concerns into a unified, out-of-proces
|
|||
Plano pulls rote plumbing out of your framework so you can stay focused on what matters most: the core product logic of your agentic applications. Plano is backed by [industry-leading LLM research](https://planoai.dev/research) and built on [Envoy](https://envoyproxy.io) by its core contributors, who built critical infrastructure at scale for modern worklaods.
|
||||
|
||||
**High-Level Network Sequence Diagram**:
|
||||

|
||||

|
||||
|
||||
**Jump to our [docs](https://docs.planoai.dev)** to learn how you can use Plano to improve the speed, safety and obervability of your agentic applications.
|
||||
|
||||
|
|
@ -156,7 +156,7 @@ curl http://localhost:8001/v1/chat/completions \
|
|||
|
||||
Every request is traced end-to-end with OpenTelemetry - no instrumentation code needed.
|
||||
|
||||

|
||||

|
||||
|
||||
### What You Didn't Have to Build
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export function Hero() {
|
|||
>
|
||||
<div className="inline-flex flex-wrap items-center gap-1.5 sm:gap-2 px-3 sm:px-4 py-1 rounded-full bg-[rgba(185,191,255,0.4)] border border-[var(--secondary)] shadow backdrop-blur hover:bg-[rgba(185,191,255,0.6)] transition-colors cursor-pointer">
|
||||
<span className="text-xs sm:text-sm font-medium text-black/65">
|
||||
v0.4.22
|
||||
v0.4.23
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm font-medium text-black ">
|
||||
—
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
docker build -f Dockerfile . -t katanemo/plano -t katanemo/plano:0.4.22
|
||||
docker build -f Dockerfile . -t katanemo/plano -t katanemo/plano:0.4.23
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
"""Plano CLI - Intelligent Prompt Gateway."""
|
||||
|
||||
__version__ = "0.4.22"
|
||||
__version__ = "0.4.23"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,42 @@ CHATGPT_API_BASE = "https://chatgpt.com/backend-api/codex"
|
|||
CHATGPT_DEFAULT_ORIGINATOR = "codex_cli_rs"
|
||||
CHATGPT_DEFAULT_USER_AGENT = "codex_cli_rs/0.0.0 (Unknown 0; unknown) unknown"
|
||||
|
||||
KIMI_CODE_API_HOST = "api.kimi.com"
|
||||
KIMI_CODE_DEFAULT_USER_AGENT = "KimiCLI/1.3"
|
||||
|
||||
|
||||
def normalize_kimi_code_base_url(base_url: str) -> str:
|
||||
"""Ensure Kimi Code API base URLs include the /v1 suffix."""
|
||||
parsed = urlparse(base_url)
|
||||
if parsed.hostname != KIMI_CODE_API_HOST:
|
||||
return base_url
|
||||
path = parsed.path.rstrip("/")
|
||||
if path.endswith("/coding"):
|
||||
return f"{parsed.scheme}://{parsed.netloc}{path}/v1"
|
||||
return base_url
|
||||
|
||||
|
||||
def apply_kimi_code_provider_defaults(model_provider: dict) -> None:
|
||||
"""Inject Kimi Code API defaults (User-Agent, normalized base URL)."""
|
||||
base_url = model_provider.get("base_url")
|
||||
if not base_url:
|
||||
return
|
||||
parsed = urlparse(base_url)
|
||||
model_id = model_provider.get("model", "")
|
||||
is_kimi_code = (
|
||||
parsed.hostname == KIMI_CODE_API_HOST or model_id == "kimi-for-coding"
|
||||
)
|
||||
if not is_kimi_code:
|
||||
return
|
||||
|
||||
normalized = normalize_kimi_code_base_url(base_url)
|
||||
if normalized != base_url:
|
||||
model_provider["base_url"] = normalized
|
||||
|
||||
headers = model_provider.setdefault("headers", {})
|
||||
headers.setdefault("User-Agent", KIMI_CODE_DEFAULT_USER_AGENT)
|
||||
|
||||
|
||||
SUPPORTED_PROVIDERS = (
|
||||
SUPPORTED_PROVIDERS_WITHOUT_BASE_URL + SUPPORTED_PROVIDERS_WITH_BASE_URL
|
||||
)
|
||||
|
|
@ -463,6 +499,8 @@ def validate_and_render_schema():
|
|||
headers.setdefault("session_id", str(uuid.uuid4()))
|
||||
model_provider["headers"] = headers
|
||||
|
||||
apply_kimi_code_provider_defaults(model_provider)
|
||||
|
||||
updated_model_providers.append(model_provider)
|
||||
|
||||
if model_provider.get("base_url", None):
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ PLANO_COLOR = "#969FF4"
|
|||
|
||||
SERVICE_NAME_ARCHGW = "plano"
|
||||
PLANO_DOCKER_NAME = "plano"
|
||||
PLANO_DOCKER_IMAGE = os.getenv("PLANO_DOCKER_IMAGE", "katanemo/plano:0.4.22")
|
||||
PLANO_DOCKER_IMAGE = os.getenv("PLANO_DOCKER_IMAGE", "katanemo/plano:0.4.23")
|
||||
DEFAULT_OTEL_TRACING_GRPC_ENDPOINT = "http://localhost:4317"
|
||||
|
||||
# Native mode constants
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -624,28 +623,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",
|
||||
|
|
@ -743,7 +720,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
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
[project]
|
||||
name = "planoai"
|
||||
version = "0.4.22"
|
||||
version = "0.4.23"
|
||||
description = "Python-based CLI tool to manage Plano."
|
||||
authors = [{name = "Katanemo Labs, Inc."}]
|
||||
readme = "README.md"
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import pytest
|
|||
import yaml
|
||||
from unittest import mock
|
||||
from planoai.config_generator import (
|
||||
validate_and_render_schema,
|
||||
apply_kimi_code_provider_defaults,
|
||||
migrate_inline_routing_preferences,
|
||||
normalize_kimi_code_base_url,
|
||||
validate_and_render_schema,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -795,3 +797,29 @@ model_providers:
|
|||
migrate_inline_routing_preferences(config_yaml)
|
||||
|
||||
assert config_yaml["version"] == "v0.5.0"
|
||||
|
||||
|
||||
def test_normalize_kimi_code_base_url_appends_v1_suffix():
|
||||
assert (
|
||||
normalize_kimi_code_base_url("https://api.kimi.com/coding")
|
||||
== "https://api.kimi.com/coding/v1"
|
||||
)
|
||||
assert (
|
||||
normalize_kimi_code_base_url("https://api.kimi.com/coding/")
|
||||
== "https://api.kimi.com/coding/v1"
|
||||
)
|
||||
assert (
|
||||
normalize_kimi_code_base_url("https://api.kimi.com/coding/v1")
|
||||
== "https://api.kimi.com/coding/v1"
|
||||
)
|
||||
|
||||
|
||||
def test_apply_kimi_code_provider_defaults_injects_user_agent():
|
||||
provider = {
|
||||
"model": "kimi-for-coding",
|
||||
"base_url": "https://api.kimi.com/coding",
|
||||
"access_key": "$MOONSHOTAI_API_KEY",
|
||||
}
|
||||
apply_kimi_code_provider_defaults(provider)
|
||||
assert provider["base_url"] == "https://api.kimi.com/coding/v1"
|
||||
assert provider["headers"]["User-Agent"] == "KimiCLI/1.3"
|
||||
|
|
|
|||
2
cli/uv.lock
generated
2
cli/uv.lock
generated
|
|
@ -337,7 +337,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "planoai"
|
||||
version = "0.4.22"
|
||||
version = "0.4.23"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
|
|
|
|||
|
|
@ -194,6 +194,7 @@ properties:
|
|||
- digitalocean
|
||||
- vercel
|
||||
- openrouter
|
||||
- moonshotai
|
||||
headers:
|
||||
type: object
|
||||
additionalProperties:
|
||||
|
|
@ -252,6 +253,7 @@ properties:
|
|||
- digitalocean
|
||||
- vercel
|
||||
- openrouter
|
||||
- moonshotai
|
||||
headers:
|
||||
type: object
|
||||
additionalProperties:
|
||||
|
|
|
|||
4
crates/Cargo.lock
generated
4
crates/Cargo.lock
generated
|
|
@ -2552,9 +2552,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proxy-wasm"
|
||||
version = "0.2.4"
|
||||
version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8d35d9e2bc5104e2e954b149aa1d5f9fa3bb27f73b45b2706020fed101db685"
|
||||
checksum = "de8f6564bd52c2f4ff79fa5d1bd3bc10d8f822162af8d527e121e46703496aa0"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"log",
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ impl Serialize for FunctionParameters {
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
// select all requried parameters
|
||||
// select all required parameters
|
||||
let required: Vec<&String> = self
|
||||
.properties
|
||||
.iter()
|
||||
|
|
|
|||
|
|
@ -400,6 +400,10 @@ pub enum LlmProviderType {
|
|||
Vercel,
|
||||
#[serde(rename = "openrouter")]
|
||||
OpenRouter,
|
||||
#[serde(rename = "astraflow")]
|
||||
Astraflow,
|
||||
#[serde(rename = "astraflow_cn")]
|
||||
AstraflowCN,
|
||||
}
|
||||
|
||||
impl Display for LlmProviderType {
|
||||
|
|
@ -425,6 +429,8 @@ impl Display for LlmProviderType {
|
|||
LlmProviderType::DigitalOcean => write!(f, "digitalocean"),
|
||||
LlmProviderType::Vercel => write!(f, "vercel"),
|
||||
LlmProviderType::OpenRouter => write!(f, "openrouter"),
|
||||
LlmProviderType::Astraflow => write!(f, "astraflow"),
|
||||
LlmProviderType::AstraflowCN => write!(f, "astraflow_cn"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use log::warn;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use serde_with::skip_serializing_none;
|
||||
|
|
@ -136,6 +137,37 @@ impl ChatCompletionsRequest {
|
|||
self.temperature = Some(1.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Strip request fields that Kimi Code API (`kimi-for-coding`) rejects or mishandles.
|
||||
pub fn normalize_for_kimi_code_api(&mut self) {
|
||||
if self.stream_options.is_some() {
|
||||
warn!("kimi-for-coding: stripping unsupported stream_options from upstream request");
|
||||
self.stream_options = None;
|
||||
}
|
||||
if self.reasoning_effort.is_some() {
|
||||
warn!("kimi-for-coding: stripping unsupported reasoning_effort from upstream request");
|
||||
self.reasoning_effort = None;
|
||||
}
|
||||
if self.web_search_options.is_some() {
|
||||
warn!(
|
||||
"kimi-for-coding: stripping unsupported web_search_options from upstream request"
|
||||
);
|
||||
self.web_search_options = None;
|
||||
}
|
||||
if self.service_tier.is_some() {
|
||||
warn!("kimi-for-coding: stripping unsupported service_tier from upstream request");
|
||||
self.service_tier = None;
|
||||
}
|
||||
if self.store.is_some() {
|
||||
warn!("kimi-for-coding: stripping unsupported store from upstream request");
|
||||
self.store = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// True when the upstream model id is Moonshot's Kimi Code endpoint model.
|
||||
pub fn is_kimi_code_model(model: &str) -> bool {
|
||||
model == "kimi-for-coding"
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -313,6 +313,7 @@ providers:
|
|||
- deepseek/deepseek-chat
|
||||
- deepseek/deepseek-reasoner
|
||||
moonshotai:
|
||||
- moonshotai/kimi-for-coding
|
||||
- moonshotai/kimi-k2-thinking
|
||||
- moonshotai/moonshot-v1-auto
|
||||
- moonshotai/moonshot-v1-32k-vision-preview
|
||||
|
|
|
|||
|
|
@ -500,6 +500,19 @@ mod tests {
|
|||
"/custom/api/v2/chat/completions"
|
||||
);
|
||||
|
||||
// Kimi Code API: base_url path prefix already includes /coding/v1
|
||||
assert_eq!(
|
||||
api.target_endpoint_for_provider(
|
||||
&ProviderId::Moonshotai,
|
||||
"/v1/messages",
|
||||
"kimi-for-coding",
|
||||
false,
|
||||
Some("/coding/v1"),
|
||||
false
|
||||
),
|
||||
"/coding/v1/chat/completions"
|
||||
);
|
||||
|
||||
// Test Groq with custom prefix
|
||||
assert_eq!(
|
||||
api.target_endpoint_for_provider(
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ pub enum ProviderId {
|
|||
DigitalOcean,
|
||||
Vercel,
|
||||
OpenRouter,
|
||||
Astraflow,
|
||||
AstraflowCN,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for ProviderId {
|
||||
|
|
@ -81,6 +83,8 @@ impl TryFrom<&str> for ProviderId {
|
|||
"do_ai" => Ok(ProviderId::DigitalOcean), // alias
|
||||
"vercel" => Ok(ProviderId::Vercel),
|
||||
"openrouter" => Ok(ProviderId::OpenRouter),
|
||||
"astraflow" => Ok(ProviderId::Astraflow),
|
||||
"astraflow_cn" => Ok(ProviderId::AstraflowCN),
|
||||
_ => Err(format!("Unknown provider: {}", value)),
|
||||
}
|
||||
}
|
||||
|
|
@ -107,6 +111,7 @@ impl ProviderId {
|
|||
ProviderId::Qwen => "qwen",
|
||||
ProviderId::ChatGPT => "chatgpt",
|
||||
ProviderId::DigitalOcean => "digitalocean",
|
||||
ProviderId::Astraflow | ProviderId::AstraflowCN => return Vec::new(),
|
||||
_ => return Vec::new(),
|
||||
};
|
||||
|
||||
|
|
@ -174,7 +179,9 @@ impl ProviderId {
|
|||
| ProviderId::Qwen
|
||||
| ProviderId::DigitalOcean
|
||||
| ProviderId::OpenRouter
|
||||
| ProviderId::ChatGPT,
|
||||
| ProviderId::ChatGPT
|
||||
| ProviderId::Astraflow
|
||||
| ProviderId::AstraflowCN,
|
||||
SupportedAPIsFromClient::AnthropicMessagesAPI(_),
|
||||
) => SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions),
|
||||
|
||||
|
|
@ -196,7 +203,9 @@ impl ProviderId {
|
|||
| ProviderId::Qwen
|
||||
| ProviderId::DigitalOcean
|
||||
| ProviderId::OpenRouter
|
||||
| ProviderId::ChatGPT,
|
||||
| ProviderId::ChatGPT
|
||||
| ProviderId::Astraflow
|
||||
| ProviderId::AstraflowCN,
|
||||
SupportedAPIsFromClient::OpenAIChatCompletions(_),
|
||||
) => SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions),
|
||||
|
||||
|
|
@ -267,6 +276,8 @@ impl Display for ProviderId {
|
|||
ProviderId::DigitalOcean => write!(f, "digitalocean"),
|
||||
ProviderId::Vercel => write!(f, "vercel"),
|
||||
ProviderId::OpenRouter => write!(f, "openrouter"),
|
||||
ProviderId::Astraflow => write!(f, "astraflow"),
|
||||
ProviderId::AstraflowCN => write!(f, "astraflow_cn"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use crate::apis::anthropic::MessagesRequest;
|
||||
use crate::apis::openai::ChatCompletionsRequest;
|
||||
use crate::apis::openai::{is_kimi_code_model, ChatCompletionsRequest};
|
||||
use log::warn;
|
||||
|
||||
use crate::apis::amazon_bedrock::{ConverseRequest, ConverseStreamRequest};
|
||||
use crate::apis::openai_responses::ResponsesAPIRequest;
|
||||
|
|
@ -90,6 +91,24 @@ impl ProviderRequestType {
|
|||
}
|
||||
}
|
||||
|
||||
if matches!(
|
||||
upstream_api,
|
||||
SupportedUpstreamAPIs::OpenAIChatCompletions(_)
|
||||
) {
|
||||
if let Self::ChatCompletionsRequest(req) = self {
|
||||
if is_kimi_code_model(req.model()) {
|
||||
req.normalize_for_kimi_code_api();
|
||||
}
|
||||
} else if let Self::MessagesRequest(req) = self {
|
||||
if is_kimi_code_model(req.model.as_str()) && req.thinking.is_some() {
|
||||
warn!(
|
||||
"kimi-for-coding: stripping unsupported thinking config from upstream request"
|
||||
);
|
||||
req.thinking = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ChatGPT requires instructions, store=false, and input as a list
|
||||
if provider_id == ProviderId::ChatGPT {
|
||||
if let Self::ResponsesAPIRequest(req) = self {
|
||||
|
|
@ -879,6 +898,42 @@ mod tests {
|
|||
assert!(req.web_search_options.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_for_upstream_kimi_code_strips_unsupported_chat_fields() {
|
||||
use crate::apis::openai::{Message, MessageContent, OpenAIApi, Role, StreamOptions};
|
||||
|
||||
let mut request = ProviderRequestType::ChatCompletionsRequest(ChatCompletionsRequest {
|
||||
model: "kimi-for-coding".to_string(),
|
||||
messages: vec![Message {
|
||||
role: Role::User,
|
||||
content: Some(MessageContent::Text("hello".to_string())),
|
||||
name: None,
|
||||
tool_calls: None,
|
||||
tool_call_id: None,
|
||||
}],
|
||||
stream_options: Some(StreamOptions {
|
||||
include_usage: Some(true),
|
||||
}),
|
||||
reasoning_effort: Some("high".to_string()),
|
||||
web_search_options: Some(serde_json::json!({"search_context_size":"medium"})),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
request
|
||||
.normalize_for_upstream(
|
||||
ProviderId::Moonshotai,
|
||||
&SupportedUpstreamAPIs::OpenAIChatCompletions(OpenAIApi::ChatCompletions),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ProviderRequestType::ChatCompletionsRequest(req) = request else {
|
||||
panic!("expected chat request");
|
||||
};
|
||||
assert!(req.stream_options.is_none());
|
||||
assert!(req.reasoning_effort.is_none());
|
||||
assert!(req.web_search_options.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_for_upstream_non_xai_keeps_chat_web_search_options() {
|
||||
use crate::apis::openai::{Message, MessageContent, OpenAIApi, Role};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ This demo shows how you can use user preferences to route user prompts to approp
|
|||
|
||||
## How to start the demo
|
||||
|
||||
Make sure you have Plano CLI installed (`pip install planoai==0.4.22` or `uv tool install planoai==0.4.22`).
|
||||
Make sure you have Plano CLI installed (`pip install planoai==0.4.23` or `uv tool install planoai==0.4.23`).
|
||||
|
||||
```bash
|
||||
cd demos/llm_routing/preference_based_routing
|
||||
|
|
|
|||
|
|
@ -432,6 +432,9 @@ Moonshot AI
|
|||
* - Model Name
|
||||
- Model ID for Config
|
||||
- Description
|
||||
* - Kimi for Coding
|
||||
- ``moonshotai/kimi-for-coding``
|
||||
- Kimi Code API model for agentic coding (use with ``base_url: https://api.kimi.com/coding/v1``)
|
||||
* - Kimi K2 Preview
|
||||
- ``moonshotai/kimi-k2-0905-preview``
|
||||
- Foundation model optimized for agentic tasks with 32B activated parameters
|
||||
|
|
@ -447,6 +450,13 @@ Moonshot AI
|
|||
.. code-block:: yaml
|
||||
|
||||
llm_providers:
|
||||
# Kimi Code API (Claude Code / agentic clients via Plano translation)
|
||||
- model: moonshotai/kimi-for-coding
|
||||
access_key: $MOONSHOTAI_API_KEY
|
||||
base_url: https://api.kimi.com/coding/v1
|
||||
headers:
|
||||
User-Agent: "KimiCLI/1.3"
|
||||
|
||||
# Latest K2 models for agentic tasks
|
||||
- model: moonshotai/kimi-k2-0905-preview
|
||||
access_key: $MOONSHOTAI_API_KEY
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ from sphinxawesome_theme.postprocess import Icons
|
|||
project = "Plano Docs"
|
||||
copyright = "2026, Katanemo Labs, a DigitalOcean Company"
|
||||
author = "Katanemo Labs, Inc"
|
||||
release = " v0.4.22"
|
||||
release = " v0.4.23"
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ Plano's CLI allows you to manage and interact with the Plano efficiently. To ins
|
|||
|
||||
.. code-block:: console
|
||||
|
||||
$ uv tool install planoai==0.4.22
|
||||
$ uv tool install planoai==0.4.23
|
||||
|
||||
**Option 2: Install with pip (Traditional)**
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ Plano's CLI allows you to manage and interact with the Plano efficiently. To ins
|
|||
|
||||
$ python -m venv venv
|
||||
$ source venv/bin/activate # On Windows, use: venv\Scripts\activate
|
||||
$ pip install planoai==0.4.22
|
||||
$ pip install planoai==0.4.23
|
||||
|
||||
|
||||
.. _llm_routing_quickstart:
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -7,6 +7,29 @@ The following is a complete reference of the ``plano_config.yml`` that controls
|
|||
the Plano gateway. This where you enable capabilities like routing to upstream LLm providers, defining prompt_targets
|
||||
where prompts get routed to, apply guardrails, and enable critical agent observability features.
|
||||
|
||||
Model provider headers
|
||||
----------------------
|
||||
|
||||
Each entry under ``model_providers`` (or the legacy ``llm_providers`` alias) may include a ``headers`` map of extra
|
||||
HTTP headers that Plano adds to upstream LLM requests. Plano applies these headers after it sets authentication from
|
||||
``access_key`` or ``passthrough_auth``, so you can supply provider-specific metadata without replacing the configured
|
||||
credentials.
|
||||
|
||||
- **Type:** map of strings (header name → value)
|
||||
- **Optional:** yes
|
||||
- **Common uses:** required ``User-Agent`` values, organization or account identifiers, or other headers some APIs expect
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
model_providers:
|
||||
- model: moonshotai/kimi-for-coding
|
||||
access_key: $MOONSHOTAI_API_KEY
|
||||
base_url: https://api.kimi.com/coding/v1
|
||||
headers:
|
||||
User-Agent: "KimiCLI/1.3"
|
||||
|
||||
The example below includes this and other provider options in context.
|
||||
|
||||
.. literalinclude:: includes/plano_config_full_reference.yaml
|
||||
:language: yaml
|
||||
:linenos:
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ Create a ``docker-compose.yml`` file with the following configuration:
|
|||
# docker-compose.yml
|
||||
services:
|
||||
plano:
|
||||
image: katanemo/plano:0.4.22
|
||||
image: katanemo/plano:0.4.23
|
||||
container_name: plano
|
||||
ports:
|
||||
- "10000:10000" # ingress (client -> plano)
|
||||
|
|
@ -153,7 +153,7 @@ Create a ``plano-deployment.yaml``:
|
|||
spec:
|
||||
containers:
|
||||
- name: plano
|
||||
image: katanemo/plano:0.4.22
|
||||
image: katanemo/plano:0.4.23
|
||||
ports:
|
||||
- containerPort: 12000 # LLM gateway (chat completions, model routing)
|
||||
name: llm-gateway
|
||||
|
|
|
|||
|
|
@ -47,6 +47,14 @@ model_providers:
|
|||
http_host: api.custom-provider.com
|
||||
access_key: $CUSTOM_API_KEY
|
||||
|
||||
# headers: optional map of extra HTTP headers sent on upstream requests (after auth).
|
||||
# Use for provider-specific requirements such as User-Agent, org IDs, or account headers.
|
||||
- model: moonshotai/kimi-for-coding
|
||||
access_key: $MOONSHOTAI_API_KEY
|
||||
base_url: https://api.kimi.com/coding/v1
|
||||
headers:
|
||||
User-Agent: "KimiCLI/1.3"
|
||||
|
||||
# Model aliases - use friendly names instead of full provider model names
|
||||
model_aliases:
|
||||
fast-llm:
|
||||
|
|
|
|||
|
|
@ -88,6 +88,18 @@ listeners:
|
|||
port: 443
|
||||
protocol: https
|
||||
provider_interface: openai
|
||||
- access_key: $MOONSHOTAI_API_KEY
|
||||
base_url: https://api.kimi.com/coding/v1
|
||||
base_url_path_prefix: /coding/v1
|
||||
cluster_name: moonshotai_api.kimi.com
|
||||
endpoint: api.kimi.com
|
||||
headers:
|
||||
User-Agent: KimiCLI/1.3
|
||||
model: kimi-for-coding
|
||||
name: moonshotai/kimi-for-coding
|
||||
port: 443
|
||||
protocol: https
|
||||
provider_interface: moonshotai
|
||||
name: model_1
|
||||
output_filters:
|
||||
- input_guards
|
||||
|
|
@ -144,6 +156,18 @@ model_providers:
|
|||
port: 443
|
||||
protocol: https
|
||||
provider_interface: openai
|
||||
- access_key: $MOONSHOTAI_API_KEY
|
||||
base_url: https://api.kimi.com/coding/v1
|
||||
base_url_path_prefix: /coding/v1
|
||||
cluster_name: moonshotai_api.kimi.com
|
||||
endpoint: api.kimi.com
|
||||
headers:
|
||||
User-Agent: KimiCLI/1.3
|
||||
model: kimi-for-coding
|
||||
name: moonshotai/kimi-for-coding
|
||||
port: 443
|
||||
protocol: https
|
||||
provider_interface: moonshotai
|
||||
- internal: true
|
||||
model: Plano-Orchestrator
|
||||
name: plano-orchestrator
|
||||
|
|
|
|||
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