livrare lot 2

This commit is contained in:
EVOTECH IT SRL 2026-07-10 03:39:53 -07:00
commit 8ecc78e729
763 changed files with 164593 additions and 0 deletions

View 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()