mirror of
https://github.com/katanemo/plano.git
synced 2026-06-17 15:25:17 +02:00
Break the 507-line monolithic validate_and_render_schema() into focused modules with pure validation functions, proper error handling, and clean I/O separation: - config_providers.py: provider constants, ConfigValidationError, unified URL parsing (replaces 3 different inline implementations) - config_validator.py: 11 pure validation functions (no I/O, no print/exit) - config_generator.py: thin 146-line I/O orchestrator, reads files once (was twice), uses logging instead of print() Also cleans up module responsibilities: - Move stream_access_logs from utils.py to docker_cli.py (Docker operation) - Deduplicate llm_providers->model_providers migration - Fix "Model alias 2 -" debug artifact in error message - Update docker-compose.dev.yaml volume mounts for new files - Rewrite tests: 53 tests calling pure functions directly (no mock_open chains), up from 10 brittle mock-dependent tests Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
145 lines
4.3 KiB
Python
145 lines
4.3 KiB
Python
"""Config generator: loads config files, validates, and renders Envoy template.
|
|
|
|
This module is the I/O boundary. It reads files, calls pure validation
|
|
functions from config_validator and config_providers, then writes output.
|
|
|
|
Entry point: ``python -m planoai.config_generator`` (called by supervisord).
|
|
"""
|
|
|
|
import logging
|
|
import os
|
|
|
|
import yaml
|
|
from copy import deepcopy
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
from planoai.config_providers import (
|
|
ConfigValidationError,
|
|
# Re-export for backward compatibility
|
|
SUPPORTED_PROVIDERS,
|
|
SUPPORTED_PROVIDERS_WITH_BASE_URL,
|
|
SUPPORTED_PROVIDERS_WITHOUT_BASE_URL,
|
|
)
|
|
from planoai.config_validator import (
|
|
build_clusters,
|
|
build_template_data,
|
|
migrate_legacy_providers,
|
|
process_model_providers,
|
|
resolve_agent_orchestrator,
|
|
validate_agents,
|
|
validate_listeners,
|
|
validate_model_aliases,
|
|
validate_prompt_targets,
|
|
validate_schema,
|
|
validate_tracing,
|
|
)
|
|
from planoai.consts import DEFAULT_OTEL_TRACING_GRPC_ENDPOINT
|
|
from planoai.utils import convert_legacy_listeners
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def load_yaml_file(path):
|
|
"""Read a YAML file and return the parsed dict."""
|
|
with open(path, "r") as f:
|
|
raw = f.read()
|
|
return yaml.safe_load(raw)
|
|
|
|
|
|
def validate_and_render_schema():
|
|
"""Main orchestrator: load -> validate -> process -> render -> write.
|
|
|
|
Reads env vars for file paths (Docker integration).
|
|
Raises ConfigValidationError on validation failure.
|
|
"""
|
|
# --- Read environment config ---
|
|
template_file = os.getenv("ENVOY_CONFIG_TEMPLATE_FILE", "envoy.template.yaml")
|
|
config_path = os.getenv("ARCH_CONFIG_FILE", "/app/arch_config.yaml")
|
|
rendered_config_path = os.getenv(
|
|
"ARCH_CONFIG_FILE_RENDERED", "/app/arch_config_rendered.yaml"
|
|
)
|
|
envoy_rendered_path = os.getenv(
|
|
"ENVOY_CONFIG_FILE_RENDERED", "/etc/envoy/envoy.yaml"
|
|
)
|
|
schema_path = os.getenv("ARCH_CONFIG_SCHEMA_FILE", "arch_config_schema.yaml")
|
|
template_root = os.getenv("TEMPLATE_ROOT", "./")
|
|
|
|
# --- Load files (each read exactly once) ---
|
|
config = load_yaml_file(config_path)
|
|
schema = load_yaml_file(schema_path)
|
|
|
|
env = Environment(loader=FileSystemLoader(template_root))
|
|
template = env.get_template(template_file)
|
|
|
|
# --- Validate and process ---
|
|
validate_schema(config, schema)
|
|
config = migrate_legacy_providers(config)
|
|
|
|
listeners, llm_gateway, prompt_gateway = convert_legacy_listeners(
|
|
config.get("listeners"), config.get("model_providers")
|
|
)
|
|
config["listeners"] = listeners
|
|
|
|
agent_endpoints = validate_agents(
|
|
config.get("agents", []), config.get("filters", [])
|
|
)
|
|
clusters = build_clusters(config.get("endpoints", {}), agent_endpoints)
|
|
log.info("Defined clusters: %s", clusters)
|
|
|
|
validate_prompt_targets(config, clusters)
|
|
|
|
tracing = validate_tracing(
|
|
config.get("tracing", {}), DEFAULT_OTEL_TRACING_GRPC_ENDPOINT
|
|
)
|
|
|
|
updated_providers, llms_with_endpoint, model_name_keys = process_model_providers(
|
|
listeners, config.get("routing", {})
|
|
)
|
|
config["model_providers"] = deepcopy(updated_providers)
|
|
|
|
validate_listeners(listeners)
|
|
|
|
if "model_aliases" in config:
|
|
validate_model_aliases(config["model_aliases"], model_name_keys)
|
|
|
|
agent_orchestrator = resolve_agent_orchestrator(
|
|
config, config.get("endpoints", {})
|
|
)
|
|
|
|
data = build_template_data(
|
|
prompt_gateway,
|
|
llm_gateway,
|
|
config,
|
|
clusters,
|
|
updated_providers,
|
|
tracing,
|
|
llms_with_endpoint,
|
|
agent_orchestrator,
|
|
listeners,
|
|
)
|
|
|
|
# --- Render and write ---
|
|
rendered = template.render(data)
|
|
log.info("Writing Envoy config to %s", envoy_rendered_path)
|
|
|
|
with open(envoy_rendered_path, "w") as f:
|
|
f.write(rendered)
|
|
|
|
config_string = yaml.dump(config)
|
|
with open(rendered_config_path, "w") as f:
|
|
f.write(config_string)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(
|
|
level=logging.INFO,
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
)
|
|
try:
|
|
validate_and_render_schema()
|
|
except ConfigValidationError as e:
|
|
log.error(str(e))
|
|
exit(1)
|
|
except Exception as e:
|
|
log.error("Unexpected error: %s", e)
|
|
exit(1)
|