Merge branch 'main' into musa/cli

This commit is contained in:
Musa 2026-02-24 18:31:06 -08:00
commit 2159d06915
No known key found for this signature in database
31 changed files with 707 additions and 199 deletions

View file

@ -71,6 +71,17 @@ uv run planoai logs --follow
uv run planoai <command> [options]
```
### CI: Keep CLI templates and demos in sync
The CLI templates in `cli/planoai/templates/` are the source of truth for mapped
demo `config.yaml` files.
Use the sync utility to write mapped demo configs from templates:
```bash
uv run python -m planoai.template_sync
```
### Optional: Manual Virtual Environment Activation
While `uv run` handles the virtual environment automatically, you can activate it manually if needed:
@ -80,4 +91,4 @@ source .venv/bin/activate
planoai build # No need for 'uv run' when activated
```
**Note:** For end-user installation instructions, see the [plano documentation](https://docs.planoai.dev).
**Note:** For end-user installation instructions, see the [Plano documentation](https://docs.planoai.dev).

View file

@ -1,3 +1,3 @@
"""Plano CLI - Intelligent Prompt Gateway."""
__version__ = "0.4.7"
__version__ = "0.4.8"

View file

@ -460,6 +460,12 @@ def validate_and_render_schema():
print("agent_orchestrator: ", agent_orchestrator)
overrides = config_yaml.get("overrides", {})
upstream_connect_timeout = overrides.get("upstream_connect_timeout", "5s")
upstream_tls_ca_path = overrides.get(
"upstream_tls_ca_path", "/etc/ssl/certs/ca-certificates.crt"
)
data = {
"prompt_gateway_listener": prompt_gateway,
"llm_gateway_listener": llm_gateway,
@ -471,6 +477,8 @@ def validate_and_render_schema():
"local_llms": llms_with_endpoint,
"agent_orchestrator": agent_orchestrator,
"listeners": listeners,
"upstream_connect_timeout": upstream_connect_timeout,
"upstream_tls_ca_path": upstream_tls_ca_path,
}
rendered = template.render(data)

View file

@ -5,5 +5,5 @@ PLANO_COLOR = "#969FF4"
SERVICE_NAME_ARCHGW = "plano"
PLANO_DOCKER_NAME = "plano"
PLANO_DOCKER_IMAGE = os.getenv("PLANO_DOCKER_IMAGE", "katanemo/plano:0.4.7")
PLANO_DOCKER_IMAGE = os.getenv("PLANO_DOCKER_IMAGE", "katanemo/plano:0.4.8")
DEFAULT_OTEL_TRACING_GRPC_ENDPOINT = "http://host.docker.internal:4317"

View file

@ -0,0 +1,122 @@
from __future__ import annotations
import argparse
from dataclasses import dataclass
from pathlib import Path
import yaml
from planoai.init_cmd import BUILTIN_TEMPLATES
@dataclass(frozen=True)
class SyncEntry:
template_id: str
template_file: str
demo_configs: tuple[str, ...]
transform: str = "none"
REPO_ROOT = Path(__file__).resolve().parents[2]
TEMPLATES_DIR = REPO_ROOT / "cli" / "planoai" / "templates"
SYNC_MAP_PATH = TEMPLATES_DIR / "template_sync_map.yaml"
def _load_sync_entries() -> list[SyncEntry]:
payload = yaml.safe_load(SYNC_MAP_PATH.read_text(encoding="utf-8")) or {}
rows = payload.get("templates", [])
entries: list[SyncEntry] = []
for row in rows:
entries.append(
SyncEntry(
template_id=row["template_id"],
template_file=row["template_file"],
demo_configs=tuple(row.get("demo_configs", [])),
transform=row.get("transform", "none"),
)
)
return entries
def _render_for_demo(template_text: str, transform: str) -> str:
if transform == "none":
rendered = template_text
else:
raise ValueError(f"Unknown transform profile: {transform}")
return rendered if rendered.endswith("\n") else f"{rendered}\n"
def _validate_manifest(entries: list[SyncEntry]) -> list[str]:
errors: list[str] = []
builtin_ids = {t.id for t in BUILTIN_TEMPLATES}
manifest_ids = {entry.template_id for entry in entries}
missing = sorted(builtin_ids - manifest_ids)
extra = sorted(manifest_ids - builtin_ids)
if missing:
errors.append(f"Missing template IDs in sync map: {', '.join(missing)}")
if extra:
errors.append(f"Unknown template IDs in sync map: {', '.join(extra)}")
for entry in entries:
template_path = TEMPLATES_DIR / entry.template_file
if not template_path.exists():
errors.append(
f"template_file does not exist for '{entry.template_id}': {template_path}"
)
for demo_rel_path in entry.demo_configs:
demo_path = REPO_ROOT / demo_rel_path
if not demo_path.exists():
errors.append(
f"demo config does not exist for '{entry.template_id}': {demo_path}"
)
return errors
def write_mapped_demo_configs(*, verbose: bool = False) -> int:
entries = _load_sync_entries()
manifest_errors = _validate_manifest(entries)
if manifest_errors:
for error in manifest_errors:
print(f"[manifest] {error}")
return 2
write_count = 0
for entry in entries:
template_text = (TEMPLATES_DIR / entry.template_file).read_text(
encoding="utf-8"
)
expected_text = _render_for_demo(template_text, entry.transform)
for demo_rel_path in entry.demo_configs:
demo_path = REPO_ROOT / demo_rel_path
# Keep this as a write-only sync step so CI behavior is deterministic.
demo_path.write_text(expected_text, encoding="utf-8")
write_count += 1
if verbose:
print(
f"[wrote] {demo_rel_path} <- {entry.template_id} ({entry.template_file})"
)
print(f"Wrote {write_count} mapped demo config(s) from CLI templates.")
return 0
def main() -> int:
parser = argparse.ArgumentParser(
description="Sync CLI templates to mapped demo config.yaml files (write-only)."
)
parser.add_argument(
"--verbose",
action="store_true",
help="Print each file written during sync.",
)
args = parser.parse_args()
return write_mapped_demo_configs(verbose=bool(args.verbose))
if __name__ == "__main__":
raise SystemExit(main())

View file

@ -0,0 +1,29 @@
templates:
- template_id: sub_agent_orchestration
template_file: sub_agent_orchestration.yaml
demo_configs:
- demos/agent_orchestration/multi_agent_crewai_langchain/config.yaml
transform: none
- template_id: coding_agent_routing
template_file: coding_agent_routing.yaml
demo_configs:
- demos/llm_routing/claude_code_router/config.yaml
transform: none
- template_id: preference_aware_routing
template_file: preference_aware_routing.yaml
demo_configs:
- demos/llm_routing/preference_based_routing/config.yaml
transform: none
- template_id: filter_chain_guardrails
template_file: filter_chain_guardrails.yaml
demo_configs:
- demos/filter_chains/http_filter/config.yaml
transform: none
- template_id: conversational_state_v1_responses
template_file: conversational_state_v1_responses.yaml
demo_configs: []
transform: none

View file

@ -1,6 +1,6 @@
[project]
name = "planoai"
version = "0.4.7"
version = "0.4.8"
description = "Python-based CLI tool to manage Plano."
authors = [{name = "Katanemo Labs, Inc."}]
readme = "README.md"