diff --git a/docs/tech-specs/flow-configurable-parameters.md b/docs/tech-specs/flow-configurable-parameters.md index 65d431be..546d66b8 100644 --- a/docs/tech-specs/flow-configurable-parameters.md +++ b/docs/tech-specs/flow-configurable-parameters.md @@ -100,7 +100,24 @@ Parameter definitions are stored centrally in the schema and config system with "type": "string", "description": "LLM model to use", "default": "gpt-4", - "enum": ["gpt-4", "gpt-3.5-turbo", "claude-3", "gemma3:8b"], + "enum": [ + { + "id": "gpt-4", + "description": "OpenAI GPT-4 (Most Capable)" + }, + { + "id": "gpt-3.5-turbo", + "description": "OpenAI GPT-3.5 Turbo (Fast & Efficient)" + }, + { + "id": "claude-3", + "description": "Anthropic Claude 3 (Thoughtful & Safe)" + }, + { + "id": "gemma3:8b", + "description": "Google Gemma 3 8B (Open Source)" + } + ], "required": false }, "model-size": { @@ -137,25 +154,36 @@ Flow classes define parameter metadata with type references, descriptions, and o { "flow_class": "document-analysis", "parameters": { - "model": { + "llm-model": { "type": "llm-model", - "description": "LLM model to use for document analysis", + "description": "Primary LLM model for text completion", "order": 1 }, - "size": { - "type": "model-size", - "description": "Model size variant to use", - "order": 2 + "llm-rag-model": { + "type": "llm-model", + "description": "LLM model for RAG operations", + "order": 2, + "advanced": true, + "controlled-by": "llm-model" }, - "temp": { + "llm-temperature": { "type": "temperature", "description": "Generation temperature for creativity control", - "order": 3 + "order": 3, + "advanced": true }, - "chunk": { + "chunk-size": { "type": "chunk-size", "description": "Document chunk size for processing", - "order": 4 + "order": 4, + "advanced": true + }, + "chunk-overlap": { + "type": "integer", + "description": "Overlap between document chunks", + "order": 5, + "advanced": true, + "controlled-by": "chunk-size" } }, "class": { @@ -163,8 +191,16 @@ Flow classes define parameter metadata with type references, descriptions, and o "request": "non-persistent://tg/request/text-completion:{class}", "response": "non-persistent://tg/response/text-completion:{class}", "parameters": { - "model": "{model}", - "temperature": "{temp}" + "model": "{llm-model}", + "temperature": "{llm-temperature}" + } + }, + "rag-completion:{class}": { + "request": "non-persistent://tg/request/rag-completion:{class}", + "response": "non-persistent://tg/response/rag-completion:{class}", + "parameters": { + "model": "{llm-rag-model}", + "temperature": "{llm-temperature}" } } }, @@ -173,8 +209,8 @@ Flow classes define parameter metadata with type references, descriptions, and o "input": "persistent://tg/flow/chunk:{id}", "output": "persistent://tg/flow/chunk-load:{id}", "parameters": { - "chunk_size": "{chunk}", - "chunk_overlap": 100 + "chunk_size": "{chunk-size}", + "chunk_overlap": "{chunk-overlap}" } } } @@ -185,6 +221,8 @@ The `parameters` section maps flow-specific parameter names (keys) to parameter - `type`: Reference to centrally-defined parameter definition (e.g., "llm-model") - `description`: Human-readable description for UI display - `order`: Display order for parameter forms (lower numbers appear first) +- `advanced` (optional): Boolean flag indicating if this is an advanced parameter (default: false). When set to true, the UI may hide this parameter by default or place it in an "Advanced" section +- `controlled-by` (optional): Name of another parameter that controls this parameter's value when in simple mode. When specified, this parameter inherits its value from the controlling parameter unless explicitly overridden This approach allows: - Reusable parameter type definitions across multiple flow classes @@ -193,6 +231,8 @@ This approach allows: - Enhanced UI experience with descriptive parameter forms - Consistent parameter validation across flows - Easy addition of new standard parameter types +- Simplified UI with basic/advanced mode separation +- Parameter value inheritance for related settings #### Flow Launch Request @@ -203,32 +243,62 @@ The flow launch API accepts parameters using the flow's parameter names: "flow_class": "document-analysis", "flow_id": "customer-A-flow", "parameters": { - "model": "claude-3", - "size": "12b", - "temp": 0.5, - "chunk": 1024 + "llm-model": "claude-3", + "llm-temperature": 0.5, + "chunk-size": 1024 } } ``` +Note: In this example, `llm-rag-model` is not explicitly provided but will inherit the value "claude-3" from `llm-model` due to its `controlled-by` relationship. Similarly, `chunk-overlap` could inherit a calculated value based on `chunk-size`. + The system will: 1. Extract parameter metadata from flow class definition -2. Map flow parameter names to their type definitions (e.g., `model` → `llm-model` type) -3. Validate user-provided values against the parameter type definitions -4. Substitute validated values into processor parameters during flow instantiation +2. Map flow parameter names to their type definitions (e.g., `llm-model` → `llm-model` type) +3. Resolve controlled-by relationships (e.g., `llm-rag-model` inherits from `llm-model`) +4. Validate user-provided and inherited values against the parameter type definitions +5. Substitute resolved values into processor parameters during flow instantiation ### Implementation Details #### Parameter Resolution Process 1. **Flow Class Loading**: Load flow class and extract parameter metadata -2. **Metadata Extraction**: Extract `type`, `description`, and `order` for each parameter +2. **Metadata Extraction**: Extract `type`, `description`, `order`, `advanced`, and `controlled-by` for each parameter 3. **Type Definition Lookup**: Retrieve parameter type definitions from schema/config store using `type` field -4. **UI Form Generation**: Use `description` and `order` fields to create ordered parameter forms -5. **Validation**: Validate user-provided parameters against type definitions -6. **Default Application**: Apply default values for missing parameters from type definitions -7. **Template Substitution**: Replace parameter placeholders in processor parameters with validated values -8. **Processor Instantiation**: Create processors with substituted parameters +4. **UI Form Generation**: + - Use `description` and `order` fields to create ordered parameter forms + - Parameters with `advanced: true` are hidden in basic mode or grouped in an "Advanced" section + - Parameters with `controlled-by` may be hidden in simple mode if they inherit from their controller +5. **Parameter Inheritance Resolution**: + - For parameters with `controlled-by` field, check if a value was explicitly provided + - If no explicit value provided, inherit the value from the controlling parameter + - If the controlling parameter also has no value, use the default from the type definition +6. **Validation**: Validate user-provided and inherited parameters against type definitions +7. **Default Application**: Apply default values for missing parameters from type definitions +8. **Template Substitution**: Replace parameter placeholders in processor parameters with resolved values +9. **Processor Instantiation**: Create processors with substituted parameters + +#### Parameter Inheritance with controlled-by + +The `controlled-by` field enables parameter value inheritance, particularly useful for simplifying user interfaces while maintaining flexibility: + +**Example Scenario**: +- `llm-model` parameter controls the primary LLM model +- `llm-rag-model` parameter has `"controlled-by": "llm-model"` +- In simple mode, setting `llm-model` to "gpt-4" automatically sets `llm-rag-model` to "gpt-4" as well +- In advanced mode, users can override `llm-rag-model` with a different value + +**Resolution Rules**: +1. If a parameter has an explicitly provided value, use that value +2. If no explicit value and `controlled-by` is set, use the controlling parameter's value +3. If the controlling parameter has no value, fall back to the default from the type definition +4. Circular dependencies in `controlled-by` relationships result in a validation error + +**UI Behavior**: +- In basic/simple mode: Parameters with `controlled-by` may be hidden or shown as read-only with inherited value +- In advanced mode: All parameters are shown and can be individually configured +- When a controlling parameter changes, dependent parameters update automatically unless explicitly overridden #### Pulsar Integration diff --git a/trustgraph-cli/pyproject.toml b/trustgraph-cli/pyproject.toml index 70b0a1b8..ae69abdd 100644 --- a/trustgraph-cli/pyproject.toml +++ b/trustgraph-cli/pyproject.toml @@ -72,6 +72,7 @@ tg-show-kg-cores = "trustgraph.cli.show_kg_cores:main" tg-show-library-documents = "trustgraph.cli.show_library_documents:main" tg-show-library-processing = "trustgraph.cli.show_library_processing:main" tg-show-mcp-tools = "trustgraph.cli.show_mcp_tools:main" +tg-show-parameter-types = "trustgraph.cli.show_parameter_types:main" tg-show-processor-state = "trustgraph.cli.show_processor_state:main" tg-show-prompts = "trustgraph.cli.show_prompts:main" tg-show-token-costs = "trustgraph.cli.show_token_costs:main" diff --git a/trustgraph-cli/trustgraph/cli/show_flow_classes.py b/trustgraph-cli/trustgraph/cli/show_flow_classes.py index 4cf6fc2f..d9ce96a7 100644 --- a/trustgraph-cli/trustgraph/cli/show_flow_classes.py +++ b/trustgraph-cli/trustgraph/cli/show_flow_classes.py @@ -5,38 +5,93 @@ Shows all defined flow classes. import argparse import os import tabulate -from trustgraph.api import Api +from trustgraph.api import Api, ConfigKey import json default_url = os.getenv("TRUSTGRAPH_URL", 'http://localhost:8088/') +def format_parameters(params_metadata, config_api): + """ + Format parameter metadata for display + + Args: + params_metadata: Parameter definitions from flow class + config_api: API client to get parameter type information + + Returns: + Formatted string describing parameters + """ + if not params_metadata: + return "None" + + param_list = [] + + # Sort parameters by order if available + sorted_params = sorted( + params_metadata.items(), + key=lambda x: x[1].get("order", 999) + ) + + for param_name, param_meta in sorted_params: + description = param_meta.get("description", param_name) + param_type = param_meta.get("type", "unknown") + + # Get type information if available + type_info = param_type + if config_api: + try: + key = ConfigKey("parameter-types", param_type) + type_def_value = config_api.get([key])[0].value + param_type_def = json.loads(type_def_value) + + # Add default value if available + default = param_type_def.get("default") + if default is not None: + type_info = f"{param_type} (default: {default})" + + except: + # If we can't get type definition, just show the type name + pass + + param_list.append(f" {param_name}: {description} [{type_info}]") + + return "\n".join(param_list) + def show_flow_classes(url): - api = Api(url).flow() + api = Api(url) + flow_api = api.flow() + config_api = api.config() - class_names = api.list_classes() + class_names = flow_api.list_classes() if len(class_names) == 0: - print("No flows.") + print("No flow classes.") return - classes = [] - for class_name in class_names: - cls = api.get_class(class_name) - classes.append(( - class_name, - cls.get("description", ""), - ", ".join(cls.get("tags", [])), - )) + cls = flow_api.get_class(class_name) - print(tabulate.tabulate( - classes, - tablefmt="pretty", - maxcolwidths=[None, 40, 20], - stralign="left", - headers = ["flow class", "description", "tags"], - )) + table = [] + table.append(("name", class_name)) + table.append(("description", cls.get("description", ""))) + + tags = cls.get("tags", []) + if tags: + table.append(("tags", ", ".join(tags))) + + # Show parameters if they exist + parameters = cls.get("parameters", {}) + if parameters: + param_str = format_parameters(parameters, config_api) + table.append(("parameters", param_str)) + + print(tabulate.tabulate( + table, + tablefmt="pretty", + stralign="left", + )) + print() def main(): diff --git a/trustgraph-cli/trustgraph/cli/show_flows.py b/trustgraph-cli/trustgraph/cli/show_flows.py index 06fbfbe8..18c1234e 100644 --- a/trustgraph-cli/trustgraph/cli/show_flows.py +++ b/trustgraph-cli/trustgraph/cli/show_flows.py @@ -45,6 +45,89 @@ def describe_interfaces(intdefs, flow): return "\n".join(lst) +def get_enum_description(param_value, param_type_def): + """ + Get the human-readable description for an enum value + + Args: + param_value: The actual parameter value (e.g., "gpt-4") + param_type_def: The parameter type definition containing enum objects + + Returns: + Human-readable description or the original value if not found + """ + enum_list = param_type_def.get("enum", []) + + # Handle both old format (strings) and new format (objects with id/description) + for enum_item in enum_list: + if isinstance(enum_item, dict): + if enum_item.get("id") == param_value: + return enum_item.get("description", param_value) + elif enum_item == param_value: + return param_value + + # If not found in enum, return original value + return param_value + +def format_parameters(flow_params, class_params_metadata, config_api): + """ + Format flow parameters with their human-readable descriptions + + Args: + flow_params: The actual parameter values used in the flow + class_params_metadata: The parameter metadata from the flow class definition + config_api: API client to retrieve parameter type definitions + + Returns: + Formatted string of parameters with descriptions + """ + if not flow_params: + return "None" + + param_list = [] + + # Sort parameters by order if available + sorted_params = sorted( + class_params_metadata.items(), + key=lambda x: x[1].get("order", 999) + ) + + for param_name, param_meta in sorted_params: + if param_name in flow_params: + value = flow_params[param_name] + description = param_meta.get("description", param_name) + param_type = param_meta.get("type", "") + controlled_by = param_meta.get("controlled-by", None) + + # Try to get enum description if this parameter has a type definition + display_value = value + if param_type and config_api: + try: + from trustgraph.api import ConfigKey + key = ConfigKey("parameter-types", param_type) + type_def_value = config_api.get([key])[0].value + param_type_def = json.loads(type_def_value) + display_value = get_enum_description(value, param_type_def) + except: + # If we can't get the type definition, just use the original value + display_value = value + + # Format the parameter line + line = f"• {description}: {display_value}" + + # Add controlled-by indicator if present + if controlled_by: + line += f" (controlled by {controlled_by})" + + param_list.append(line) + + # Add any parameters that aren't in the class metadata (shouldn't happen normally) + for param_name, value in flow_params.items(): + if param_name not in class_params_metadata: + param_list.append(f"• {param_name}: {value} (undefined)") + + return "\n".join(param_list) if param_list else "None" + def show_flows(url): api = Api(url) @@ -75,10 +158,23 @@ def show_flows(url): table.append(("class", flow.get("class-name", ""))) table.append(("desc", flow.get("description", ""))) - # Display parameters if they exist + # Display parameters with human-readable descriptions parameters = flow.get("parameters", {}) if parameters: - param_str = json.dumps(parameters, indent=2) + # Try to get the flow class definition for parameter metadata + class_name = flow.get("class-name", "") + if class_name: + try: + flow_class = flow_api.get_class(class_name) + class_params_metadata = flow_class.get("parameters", {}) + param_str = format_parameters(parameters, class_params_metadata, config_api) + except Exception as e: + # Fallback to JSON if we can't get the class definition + param_str = json.dumps(parameters, indent=2) + else: + # No class name, fallback to JSON + param_str = json.dumps(parameters, indent=2) + table.append(("parameters", param_str)) table.append(("queue", describe_interfaces(interface_defs, flow))) diff --git a/trustgraph-cli/trustgraph/cli/start_flow.py b/trustgraph-cli/trustgraph/cli/start_flow.py index b34f7b8b..317276fc 100644 --- a/trustgraph-cli/trustgraph/cli/start_flow.py +++ b/trustgraph-cli/trustgraph/cli/start_flow.py @@ -1,5 +1,10 @@ """ -Starts a processing flow using a defined flow class +Starts a processing flow using a defined flow class. + +Parameters can be provided in three ways: +1. As key=value pairs: --param model=gpt-4 --param temp=0.7 +2. As JSON string: -p '{"model": "gpt-4", "temp": 0.7}' +3. As JSON file: --parameters-file params.json """ import argparse @@ -62,6 +67,12 @@ def main(): help='Path to JSON file containing flow parameters', ) + parser.add_argument( + '--param', + action='append', + help='Flow parameter as key=value pair (can be used multiple times, e.g., --param model=gpt-4 --param temp=0.7)', + ) + args = parser.parse_args() try: @@ -73,6 +84,34 @@ def main(): parameters = json.load(f) elif args.parameters: parameters = json.loads(args.parameters) + elif args.param: + # Parse key=value pairs + parameters = {} + for param in args.param: + if '=' not in param: + raise ValueError(f"Invalid parameter format: {param}. Expected key=value") + + key, value = param.split('=', 1) + key = key.strip() + value = value.strip() + + # Try to parse value as JSON first (for numbers, booleans, etc.) + try: + # Handle common cases where we want to preserve the string + if value.lower() in ['true', 'false']: + parameters[key] = value.lower() == 'true' + elif value.replace('.', '').replace('-', '').isdigit(): + # Check if it's a number + if '.' in value: + parameters[key] = float(value) + else: + parameters[key] = int(value) + else: + # Keep as string + parameters[key] = value + except ValueError: + # If JSON parsing fails, treat as string + parameters[key] = value start_flow( url = args.api_url,