livrare lot 2
This commit is contained in:
commit
8ecc78e729
763 changed files with 164593 additions and 0 deletions
384
backend/admin-dashboard/scripts/generate_node_types.py
Normal file
384
backend/admin-dashboard/scripts/generate_node_types.py
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Generate TypeScript types from Pydantic node schemas.
|
||||
|
||||
This script introspects the Pydantic models in shared.node_schemas and generates:
|
||||
1. TypeScript interfaces for each node spec
|
||||
2. Validation rules extracted from Field constraints (ge, le, min_length, etc.)
|
||||
3. Discriminated union type for all node specs
|
||||
|
||||
Usage:
|
||||
python generate_node_types.py
|
||||
|
||||
Output:
|
||||
../src/components/orchestrator/types/nodeSpecs.generated.ts
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import get_type_hints, get_origin, get_args, Any, Optional, List, Dict, Literal, Union
|
||||
from datetime import datetime
|
||||
|
||||
# Add the orchestration-layer to the path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "services" / "orchestration-layer"))
|
||||
|
||||
from shared.node_schemas import (
|
||||
NODE_SPEC_SCHEMAS,
|
||||
NODE_OUTPUT_SCHEMAS,
|
||||
LLMNodeSpec,
|
||||
APINodeSpec,
|
||||
TransformNodeSpec,
|
||||
StorageNodeSpec,
|
||||
FunctionNodeSpec,
|
||||
PythonREPLNodeSpec,
|
||||
AudioTranscriptionNodeSpec,
|
||||
ConditionalNodeSpec,
|
||||
SubagentNodeSpec,
|
||||
AggregatorNodeSpec,
|
||||
InputNodeSpec,
|
||||
OutputNodeSpec,
|
||||
ForEachNodeSpec,
|
||||
JoinNodeSpec,
|
||||
)
|
||||
from pydantic import BaseModel
|
||||
from pydantic.fields import FieldInfo
|
||||
|
||||
|
||||
def python_type_to_ts(python_type: Any) -> str:
|
||||
"""Convert Python type annotation to TypeScript type."""
|
||||
origin = get_origin(python_type)
|
||||
args = get_args(python_type)
|
||||
|
||||
# Handle None/NoneType
|
||||
if python_type is type(None):
|
||||
return "null"
|
||||
|
||||
# Handle Optional (Union with None)
|
||||
if origin is Union:
|
||||
non_none_args = [a for a in args if a is not type(None)]
|
||||
if len(non_none_args) == 1:
|
||||
# Optional[X] -> X | null
|
||||
return f"{python_type_to_ts(non_none_args[0])} | null"
|
||||
else:
|
||||
# Union of multiple types
|
||||
return " | ".join(python_type_to_ts(a) for a in args)
|
||||
|
||||
# Handle Literal
|
||||
if origin is Literal:
|
||||
return " | ".join(f"'{a}'" if isinstance(a, str) else str(a) for a in args)
|
||||
|
||||
# Handle List
|
||||
if origin is list:
|
||||
inner = python_type_to_ts(args[0]) if args else "any"
|
||||
return f"{inner}[]"
|
||||
|
||||
# Handle Dict
|
||||
if origin is dict:
|
||||
key_type = python_type_to_ts(args[0]) if args else "string"
|
||||
value_type = python_type_to_ts(args[1]) if len(args) > 1 else "any"
|
||||
return f"Record<{key_type}, {value_type}>"
|
||||
|
||||
# Handle basic types
|
||||
type_map = {
|
||||
str: "string",
|
||||
int: "number",
|
||||
float: "number",
|
||||
bool: "boolean",
|
||||
Any: "any",
|
||||
type(None): "null",
|
||||
}
|
||||
|
||||
if python_type in type_map:
|
||||
return type_map[python_type]
|
||||
|
||||
# Handle Pydantic models (nested)
|
||||
if isinstance(python_type, type) and issubclass(python_type, BaseModel):
|
||||
return python_type.__name__
|
||||
|
||||
# Fallback
|
||||
return "any"
|
||||
|
||||
|
||||
def extract_field_constraints(field: FieldInfo) -> Dict[str, Any]:
|
||||
"""Extract validation constraints from a Pydantic field."""
|
||||
constraints = {}
|
||||
|
||||
# Get default value
|
||||
if field.default is not None and field.default is not ...:
|
||||
constraints["default"] = field.default
|
||||
|
||||
# Extract from metadata (Pydantic v2 style)
|
||||
for constraint in field.metadata:
|
||||
constraint_type = type(constraint).__name__
|
||||
if constraint_type == "Ge":
|
||||
constraints["min"] = constraint.ge
|
||||
elif constraint_type == "Le":
|
||||
constraints["max"] = constraint.le
|
||||
elif constraint_type == "Gt":
|
||||
constraints["min"] = constraint.gt
|
||||
constraints["exclusiveMin"] = True
|
||||
elif constraint_type == "Lt":
|
||||
constraints["max"] = constraint.lt
|
||||
constraints["exclusiveMax"] = True
|
||||
elif constraint_type == "MinLen":
|
||||
constraints["minLength"] = constraint.min_length
|
||||
elif constraint_type == "MaxLen":
|
||||
constraints["maxLength"] = constraint.max_length
|
||||
|
||||
# Check if required
|
||||
constraints["required"] = field.is_required()
|
||||
|
||||
return constraints
|
||||
|
||||
|
||||
def generate_interface(model: type[BaseModel], model_name: str) -> tuple[str, Dict[str, Dict[str, Any]]]:
|
||||
"""Generate TypeScript interface and validation rules for a Pydantic model."""
|
||||
lines = [f"export interface {model_name} {{"]
|
||||
validation_rules = {}
|
||||
|
||||
type_hints = get_type_hints(model)
|
||||
|
||||
for field_name, field_info in model.model_fields.items():
|
||||
python_type = type_hints.get(field_name, Any)
|
||||
ts_type = python_type_to_ts(python_type)
|
||||
|
||||
# Check if optional
|
||||
is_optional = not field_info.is_required()
|
||||
optional_marker = "?" if is_optional else ""
|
||||
|
||||
# Get description for JSDoc
|
||||
description = field_info.description or ""
|
||||
|
||||
# Add JSDoc comment
|
||||
if description:
|
||||
lines.append(f" /** {description} */")
|
||||
|
||||
lines.append(f" {field_name}{optional_marker}: {ts_type};")
|
||||
|
||||
# Extract validation constraints
|
||||
constraints = extract_field_constraints(field_info)
|
||||
if constraints:
|
||||
validation_rules[field_name] = constraints
|
||||
|
||||
lines.append("}")
|
||||
return "\n".join(lines), validation_rules
|
||||
|
||||
|
||||
def generate_nested_models() -> str:
|
||||
"""Generate interfaces for nested models used in specs."""
|
||||
# Import nested models
|
||||
from shared.node_schemas.llm_node import ToolConfig
|
||||
from shared.node_schemas.api_node import FallbackEndpoint
|
||||
from shared.node_schemas.subagent_node import SubagentPass
|
||||
from shared.node_schemas.aggregator_node import SignalConfig, ThresholdConfig
|
||||
from shared.node_schemas.conditional_node import ConditionalBranch
|
||||
|
||||
nested_models = [
|
||||
ToolConfig,
|
||||
FallbackEndpoint,
|
||||
SubagentPass,
|
||||
SignalConfig,
|
||||
ThresholdConfig,
|
||||
ConditionalBranch,
|
||||
]
|
||||
|
||||
interfaces = []
|
||||
for model in nested_models:
|
||||
interface, _ = generate_interface(model, model.__name__)
|
||||
interfaces.append(interface)
|
||||
|
||||
return "\n\n".join(interfaces)
|
||||
|
||||
|
||||
def generate_output_fields(model: type[BaseModel]) -> List[str]:
|
||||
"""Extract output field names from an output schema."""
|
||||
return list(model.model_fields.keys())
|
||||
|
||||
|
||||
def extract_conditional_output_fields(model: type[BaseModel]) -> Dict[str, Dict[str, List[Any]]]:
|
||||
"""
|
||||
Extract conditional field availability from json_schema_extra.
|
||||
Returns: { "field_name": { "spec_field": [allowed_values] } }
|
||||
"""
|
||||
conditionals = {}
|
||||
for field_name, field_info in model.model_fields.items():
|
||||
if field_name in ['error', 'error_type', 'error_message', 'details']:
|
||||
continue
|
||||
extra = field_info.json_schema_extra or {}
|
||||
if isinstance(extra, dict) and 'available_when' in extra:
|
||||
conditionals[field_name] = extra['available_when']
|
||||
return conditionals
|
||||
|
||||
|
||||
def main():
|
||||
output_path = Path(__file__).parent.parent / "src" / "components" / "orchestrator" / "types" / "nodeSpecs.generated.ts"
|
||||
|
||||
# Node specs to generate
|
||||
node_specs = {
|
||||
"llm": LLMNodeSpec,
|
||||
"api": APINodeSpec,
|
||||
"transform": TransformNodeSpec,
|
||||
"storage": StorageNodeSpec,
|
||||
"function": FunctionNodeSpec,
|
||||
"python_repl": PythonREPLNodeSpec,
|
||||
"audio_transcription": AudioTranscriptionNodeSpec,
|
||||
"conditional": ConditionalNodeSpec,
|
||||
"subagent": SubagentNodeSpec,
|
||||
"aggregator": AggregatorNodeSpec,
|
||||
"input": InputNodeSpec,
|
||||
"output": OutputNodeSpec,
|
||||
"foreach": ForEachNodeSpec,
|
||||
"join": JoinNodeSpec,
|
||||
}
|
||||
|
||||
all_interfaces = []
|
||||
all_validation_rules = {}
|
||||
all_output_fields = {}
|
||||
|
||||
# Header
|
||||
header = f"""/**
|
||||
* Auto-generated TypeScript types from Pydantic node schemas
|
||||
*
|
||||
* DO NOT EDIT MANUALLY - run 'npm run generate:types' to regenerate
|
||||
* Generated: {datetime.now().isoformat()}
|
||||
*
|
||||
* Source: backend/services/orchestration-layer/shared/node_schemas/
|
||||
*/
|
||||
|
||||
"""
|
||||
|
||||
# Generate nested model interfaces first
|
||||
nested_interfaces = generate_nested_models()
|
||||
all_interfaces.append(nested_interfaces)
|
||||
|
||||
# Generate spec interfaces
|
||||
for node_type, spec_class in node_specs.items():
|
||||
interface_name = f"{spec_class.__name__}"
|
||||
interface, validation = generate_interface(spec_class, interface_name)
|
||||
all_interfaces.append(interface)
|
||||
|
||||
# Convert to TypeScript-friendly key (snake_case -> UPPER_SNAKE_CASE)
|
||||
validation_key = f"{node_type.upper()}_VALIDATION"
|
||||
all_validation_rules[validation_key] = validation
|
||||
|
||||
# Get output fields from corresponding output schema
|
||||
output_class = NODE_OUTPUT_SCHEMAS.get(node_type.replace("_", ""))
|
||||
if output_class:
|
||||
all_output_fields[node_type] = generate_output_fields(output_class)
|
||||
|
||||
# Generate discriminated union
|
||||
union_members = []
|
||||
for node_type, spec_class in node_specs.items():
|
||||
union_members.append(f" | {{ type: '{node_type}' }} & {spec_class.__name__}")
|
||||
|
||||
discriminated_union = f"""/**
|
||||
* Discriminated union of all node specs.
|
||||
* Use with type narrowing: if (spec.type === 'llm') {{ spec.model_ref... }}
|
||||
*/
|
||||
export type NodeSpec =
|
||||
{chr(10).join(union_members)};
|
||||
"""
|
||||
|
||||
# Generate validation rules export
|
||||
validation_export = "/**\n * Validation rules extracted from Pydantic Field constraints\n */\n"
|
||||
for key, rules in all_validation_rules.items():
|
||||
validation_export += f"export const {key} = {{\n"
|
||||
for field_name, constraints in rules.items():
|
||||
constraints_str = ", ".join(
|
||||
f"{k}: {repr(v) if isinstance(v, str) else str(v).lower() if isinstance(v, bool) else v}"
|
||||
for k, v in constraints.items()
|
||||
)
|
||||
validation_export += f" {field_name}: {{ {constraints_str} }},\n"
|
||||
validation_export += "} as const;\n\n"
|
||||
|
||||
# Generate output fields export
|
||||
output_fields_export = "/**\n * Output fields available for each node type (for NodeConfig.Header)\n */\n"
|
||||
output_fields_export += "export const NODE_OUTPUT_FIELDS: Record<string, string[]> = {\n"
|
||||
for node_type, fields in all_output_fields.items():
|
||||
fields_str = ", ".join(f"'{f}'" for f in fields if f not in ['error', 'error_type', 'error_message', 'details'])
|
||||
output_fields_export += f" {node_type}: [{fields_str}],\n"
|
||||
output_fields_export += "};\n"
|
||||
|
||||
# Generate conditional output fields
|
||||
all_conditional_fields = {}
|
||||
for node_type in node_specs.keys():
|
||||
output_class = NODE_OUTPUT_SCHEMAS.get(node_type.replace("_", ""))
|
||||
if output_class:
|
||||
conditionals = extract_conditional_output_fields(output_class)
|
||||
if conditionals:
|
||||
all_conditional_fields[node_type] = conditionals
|
||||
|
||||
conditional_export = """
|
||||
/**
|
||||
* Conditional output field availability based on node spec values.
|
||||
* Maps: node_type -> { field_name -> { spec_field -> allowed_values[] } }
|
||||
* Fields without conditions are always available.
|
||||
*/
|
||||
export const CONDITIONAL_OUTPUT_FIELDS: Record<string, Record<string, Record<string, unknown[]>>> = """
|
||||
conditional_export += json.dumps(all_conditional_fields, indent=2) + ";\n"
|
||||
|
||||
helper_function = """
|
||||
/**
|
||||
* Get context-aware output fields based on node spec configuration.
|
||||
* Filters NODE_OUTPUT_FIELDS using CONDITIONAL_OUTPUT_FIELDS metadata.
|
||||
*
|
||||
* @param nodeType - The node type
|
||||
* @param spec - The node's spec configuration
|
||||
* @returns Array of field names relevant for the current configuration
|
||||
*/
|
||||
export function getContextAwareOutputFields(
|
||||
nodeType: NodeType,
|
||||
spec?: Record<string, unknown> | null
|
||||
): string[] {
|
||||
const staticFields = NODE_OUTPUT_FIELDS[nodeType] || ['result'];
|
||||
const conditions = CONDITIONAL_OUTPUT_FIELDS[nodeType];
|
||||
|
||||
// No conditions for this node type - return all fields
|
||||
if (!conditions || !spec) {
|
||||
return staticFields;
|
||||
}
|
||||
|
||||
// Filter fields based on conditions
|
||||
return staticFields.filter(field => {
|
||||
const fieldConditions = conditions[field];
|
||||
// No conditions for this field - always include
|
||||
if (!fieldConditions) return true;
|
||||
|
||||
// Check if current spec values satisfy any condition
|
||||
for (const [specField, allowedValues] of Object.entries(fieldConditions)) {
|
||||
const specValue = spec[specField];
|
||||
if (specValue !== undefined && (allowedValues as unknown[]).includes(specValue)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
"""
|
||||
|
||||
# Generate node type literal
|
||||
node_types_literal = "/**\n * All valid node types\n */\n"
|
||||
node_types_literal += "export type NodeType = " + " | ".join(f"'{t}'" for t in node_specs.keys()) + ";\n"
|
||||
|
||||
# Combine all
|
||||
content = header
|
||||
content += "\n".join(all_interfaces)
|
||||
content += "\n\n"
|
||||
content += discriminated_union
|
||||
content += "\n"
|
||||
content += node_types_literal
|
||||
content += "\n"
|
||||
content += validation_export
|
||||
content += output_fields_export
|
||||
content += conditional_export
|
||||
content += helper_function
|
||||
|
||||
# Write output
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_text(content)
|
||||
print(f"Generated: {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue