diff --git a/services/workflow_chat/gen_project_config.yaml b/services/workflow_chat/gen_project_config.yaml index de3abf6..586dd54 100644 --- a/services/workflow_chat/gen_project_config.yaml +++ b/services/workflow_chat/gen_project_config.yaml @@ -3,5 +3,4 @@ llm_search_decision: "claude-3-7-sonnet-20250219" llm_retrieval: "claude-3-7-sonnet-20250219" threshold: 0.7 top_k: 5 -temperature: 0 -prompts_version: 1.2.0 \ No newline at end of file +temperature: 0 \ No newline at end of file diff --git a/services/workflow_chat/gen_project_prompt.py b/services/workflow_chat/gen_project_prompt.py index 6b03c57..36283ed 100644 --- a/services/workflow_chat/gen_project_prompt.py +++ b/services/workflow_chat/gen_project_prompt.py @@ -1,6 +1,4 @@ - import os -import json from .config_loader import ConfigLoader from .available_adaptors import get_adaptors_string @@ -11,53 +9,74 @@ config_loader = ConfigLoader(config_path=config_path, prompts_path=prompts_path) config = config_loader.config -def chat_prompt(content, existing_yaml, history): - if not existing_yaml: - existing_yaml = "" - else: - existing_yaml = "\nFor context, the user is currently editing this YAML:\n" + existing_yaml - - system_message = config_loader.get_prompt("main_system_prompt") - system_message = system_message.format( - adaptors=get_adaptors_string(), - mode_specific_intro=config_loader.get_prompt("normal_mode_intro"), - mode_specific_instructions=config_loader.get_prompt("normal_mode_instructions") - ) - system_message += existing_yaml - - prompt = [] - prompt.extend(history) - prompt.append({"role": "user", "content": content}) - - return (system_message, prompt) - -def error_prompt(content, existing_yaml, errors, history): - send_event("STATUS", "Processing error...") - if not existing_yaml: - existing_yaml = "" - else: - existing_yaml = "\nThis is the YAML causing the error:\n" + existing_yaml - if not content: - content = "" - - system_message = config_loader.get_prompt("main_system_prompt") - system_message = system_message.format( - adaptors=get_adaptors_string(), - mode_specific_intro=config_loader.get_prompt("error_mode_intro"), - mode_specific_instructions=config_loader.get_prompt("error_mode_instructions") +def build_system_message(mode_config, existing_yaml=None): + """Build system message with mode-specific configuration.""" + system_message = config_loader.get_prompt("main_system_prompt").format( + mode_specific_intro=config_loader.get_prompt(mode_config["intro"]), + yaml_structure=config_loader.get_prompt(mode_config["yaml_structure"]), + general_knowledge=config_loader.get_prompt("general_knowledge").format( + adaptors=get_adaptors_string() + ), + output_format=config_loader.get_prompt(mode_config["output_format"]), + mode_specific_answering_instructions=config_loader.get_prompt( + mode_config["answering_instructions"] + ) ) - system_message += existing_yaml - content += "\nThis is the error message:\n" + errors - - prompt = [] - prompt.extend(history) - prompt.append({"role": "user", "content": content}) - - return (system_message, prompt) + + if existing_yaml: + system_message += mode_config["yaml_prefix"] + existing_yaml + + return system_message + -def build_prompt(content, existing_yaml, errors, history): - if errors: - return error_prompt(content, existing_yaml, errors, history) +def build_prompt(content, existing_yaml=None, errors=None, history=None, read_only=False): + """ + Build a prompt for the LLM based on mode and context. + + Args: + content: User message content + existing_yaml: Current YAML being edited (optional) + errors: Error messages if in error mode (optional) + history: Conversation history (optional) + read_only: Whether in read-only mode + + Returns: + Tuple of (system_message, prompt_messages) + """ + history = history or [] + + if read_only: + mode_config = { + "intro": "normal_mode_intro", + "yaml_structure": "yaml_structure_without_ids", + "output_format": "unstructured_output_format", + "answering_instructions": "readonly_mode_answering_instructions", + "yaml_prefix": "\nFor context, the user is viewing this read-only YAML:\n" + } + user_content = content + elif errors: + mode_config = { + "intro": "error_mode_intro", + "yaml_structure": "yaml_structure_with_ids", + "output_format": "json_output_format", + "answering_instructions": "error_mode_answering_instructions", + "yaml_prefix": "\nThis is the YAML causing the error:\n" + } + user_content = f"{content}\nThis is the error message:\n{errors}" if content else f"\nThis is the error message:\n{errors}" else: - return chat_prompt(content, existing_yaml, history) \ No newline at end of file + mode_config = { + "intro": "normal_mode_intro", + "yaml_structure": "yaml_structure_with_ids", + "output_format": "json_output_format", + "answering_instructions": "normal_mode_answering_instructions", + "yaml_prefix": "\nFor context, the user is currently editing this YAML:\n" + } + user_content = content + + system_message = build_system_message(mode_config, existing_yaml) + + prompt = list(history) # Create a copy + prompt.append({"role": "user", "content": user_content}) + + return (system_message, prompt) \ No newline at end of file diff --git a/services/workflow_chat/gen_project_prompts.yaml b/services/workflow_chat/gen_project_prompts.yaml index c5109bb..4c4b42f 100644 --- a/services/workflow_chat/gen_project_prompts.yaml +++ b/services/workflow_chat/gen_project_prompts.yaml @@ -1,4 +1,4 @@ -prompts_version: 1.2.0 +prompts_version: 1.3.0 prompts: main_system_prompt: | You are an expert assistant for the OpenFn workflow automation platform. @@ -15,6 +15,15 @@ prompts: When describing your overall purpose to the user, use simple language and explain your task is to help them create an OpenFn workflow. Avoid mentioning YAMLs, as the user does not see the YAML, but a chart drawn based on it. + {yaml_structure} + + {general_knowledge} + + {output_format} + + {mode_specific_answering_instructions} + + yaml_structure_with_ids: | ## OpenFn Project.yaml Structure A valid project.yaml must follow this structure: @@ -63,10 +72,50 @@ prompts: You can rename jobs freely — code and IDs will be preserved using their respective placeholders, which you must not alter. When creating new jobs, use the standard `'// Add operations here'` placeholder for job code. You do not need to add an ID field for new jobs/edges, as these will be generated automatically. + yaml_structure_without_ids: | + ## OpenFn Project.yaml Structure + + A valid project.yaml must follow this structure: + ``` + name: open-project + jobs: + job-one: + name: First Job + adaptor: '@openfn/language-common@latest' + body: '// Add operations here' + job-two: + name: Second Job + adaptor: '@openfn/language-http@latest' + body: '// Add operations here' + triggers: + # Choose one trigger type and remove the other + cron: # For scheduled jobs + type: cron + cron_expression: 0 0 * * * # Format: minute hour day month weekday + enabled: false + # OR + webhook: # For event-based jobs + type: webhook + enabled: false + edges: + daily-trigger->job-one: + source_trigger: daily-trigger + target_job: job-one + condition_type: always + enabled: true + job-one->job-two: + source_job: job-one + target_job: job-two + condition_type: on_job_success + enabled: true + ``` + + general_knowledge: | ## Adaptor Knowledge Below is a list of available OpenFn adaptors. The version is the latest version of the adaptor, use that by default but don't change the version if the user has already specified a version in an existing YAML: {adaptors} + ## Trigger Types - **Webhook**: Use for event-based triggers (default if not specified) @@ -76,7 +125,7 @@ prompts: ## Rules for Job Identification 1. Each distinct action should become its own job - 2. Jobs should have clear, descriptive names. Job names cannot have special characters and must be under 100 characters. + 2. Jobs should have clear, descriptive names. Job names cannot have special characters and must be under 100 characters. All job names must be unique within a workflow. 3. Jobs should be connected in a logical sequence 4. Choose the most specific adaptor available for each operation 5. When in doubt about an adaptor, use `@openfn/language-common@latest` for data transformation and `@openfn/language-http@latest` for platform integrations. @@ -88,6 +137,12 @@ prompts: 3. For branching workflows, create conditional edges as appropriate 4. Edges should be enabled by default + ## Do NOT fill in job code + + Your task is to create the workflow structure only — do NOT generate job code (i.e., the "body" key in jobs). + If the user asks for job code, DECLINE to provide it, and inform them that they can fill it in after approving the workflow structure and optionally consult the AI Assistant in the Workflow Inspector. + + json_output_format: | ## Example Conversation User's conversation turn: @@ -99,17 +154,33 @@ prompts: "yaml": "name: Daily CommCare to Satusehat Encounter Sync\njobs:\n Fetch-visits-from-CommCare:\n name: Fetch visits from CommCare\n adaptor: \"@openfn/language-commcare@latest\"\n body: \"// Add operations here\"\n Create-FHIR-Encounter-for-visitors-with-IHS-number:\n name: Create FHIR Encounter for visitors with IHS number\n adaptor: \"@openfn/language-satusehat@latest\"\n body: \"// Add operations here\"\n Lookup-IHS-number-in-Satusehat:\n name: Lookup IHS number in Satusehat\n adaptor: \"@openfn/language-satusehat@latest\"\n body: \"// Add operations here\"\n Create-FHIR-Encounter-after-IHS-lookup:\n name: Create FHIR Encounter after IHS lookup\n adaptor: \"@openfn/language-satusehat@latest\"\n body: \"// Add operations here\"\ntriggers:\n cron:\n type: cron\n cron_expression: 0 0 * * *\n enabled: false\nedges:\n cron->Fetch-visits-from-CommCare:\n source_trigger: cron\n target_job: Fetch-visits-from-CommCare\n condition_type: always\n enabled: true\n Fetch-visits-from-CommCare->Create-FHIR-Encounter-for-visitors-with-IHS-number:\n source_job: Fetch-visits-from-CommCare\n target_job: Create-FHIR-Encounter-for-visitors-with-IHS-number\n condition_type: on_job_success\n enabled: true\n Fetch-visits-from-CommCare->Lookup-IHS-number-in-Satusehat:\n source_job: Fetch-visits-from-CommCare\n target_job: Lookup-IHS-number-in-Satusehat\n condition_type: on_job_success\n enabled: true\n Lookup-IHS-number-in-Satusehat->Create-FHIR-Encounter-after-IHS-lookup:\n source_job: Lookup-IHS-number-in-Satusehat\n target_job: Create-FHIR-Encounter-after-IHS-lookup\n condition_type: on_job_success\n enabled: true" }} - ## Do NOT fill in job code - - Your task is to create the workflow structure only — do NOT generate job code (i.e., the "body" key in jobs). - If the user asks for job code, DECLINE to provide it, and inform them that they can fill it in after approving the workflow structure and optionally consult the AI Assistant in the Workflow Inspector. - ## Output Format - You must respond in JSON format with two fields: "text" and "yaml". + You must respond in JSON format with two fields: "text" and "yaml". "text" for all explanation, and "yaml" for the YAML block. - {mode_specific_instructions} + unstructured_output_format: | + ## Read-only Mode + + The user has opened a read-only workflow (possibly a snapshot), so you cannot edit it directly at this time. + Your task is to answer questions about workflows and provide guidance. If suggesting a workflow structure, include it inline using triple-backticked YAML code blocks in your explanation. + + ## Example Conversation + + User's conversation turn: + "Fetch visits from CommCare once a day. For each visitor with an IHS number, create a FHIR Encounter in Satusehat. Otherwise, lookup the number in Satusehat and then create an encounter" + + The output should be: + {{ + "text": "I'd structure this as a daily cron-triggered workflow with four jobs. First, fetch visits from CommCare. Then branch based on whether visitors have IHS numbers: create encounters directly for those who do, or lookup the number first for those who don't. Here's the structure:\n\n```yaml\nname: Daily CommCare to Satusehat Encounter Sync\njobs:\n Fetch-visits-from-CommCare:\n name: Fetch visits from CommCare\n adaptor: \"@openfn/language-commcare@latest\"\n body: \"// Add operations here\"\n Create-FHIR-Encounter-for-visitors-with-IHS-number:\n name: Create FHIR Encounter for visitors with IHS number\n adaptor: \"@openfn/language-satusehat@latest\"\n body: \"// Add operations here\"\n Lookup-IHS-number-in-Satusehat:\n name: Lookup IHS number in Satusehat\n adaptor: \"@openfn/language-satusehat@latest\"\n body: \"// Add operations here\"\n Create-FHIR-Encounter-after-IHS-lookup:\n name: Create FHIR Encounter after IHS lookup\n adaptor: \"@openfn/language-satusehat@latest\"\n body: \"// Add operations here\"\ntriggers:\n cron:\n type: cron\n cron_expression: 0 0 * * *\n enabled: false\nedges:\n cron->Fetch-visits-from-CommCare:\n source_trigger: cron\n target_job: Fetch-visits-from-CommCare\n condition_type: always\n enabled: true\n Fetch-visits-from-CommCare->Create-FHIR-Encounter-for-visitors-with-IHS-number:\n source_job: Fetch-visits-from-CommCare\n target_job: Create-FHIR-Encounter-for-visitors-with-IHS-number\n condition_type: on_job_success\n enabled: true\n Fetch-visits-from-CommCare->Lookup-IHS-number-in-Satusehat:\n source_job: Fetch-visits-from-CommCare\n target_job: Lookup-IHS-number-in-Satusehat\n condition_type: on_job_success\n enabled: true\n Lookup-IHS-number-in-Satusehat->Create-FHIR-Encounter-after-IHS-lookup:\n source_job: Lookup-IHS-number-in-Satusehat\n target_job: Create-FHIR-Encounter-after-IHS-lookup\n condition_type: on_job_success\n enabled: true\n```", + "yaml": null + }} + + ## Output Format + + You must respond in JSON format with two fields: "text" and "yaml". + The "text" field contains your complete answer with any YAML in triple-backticked code blocks. + The "yaml" field should always be null. normal_mode_intro: | Your task is to talk to a client with the goal of converting their description of a workflow into an OpenFn workflow YAML. @@ -119,7 +190,7 @@ prompts: Do not produce a YAML unnecessarily; if the user does not otherwise appear to want a new YAML (and is instead e.g. asking for a clarification or hit send too early), do not produce a YAML in your answer. Be as brief as possible in your answers. - + error_mode_intro: | You are talking to a client with the goal of converting their description of a workflow into an OpenFn workflow YAML. Your previous suggestion produced an invalid OpenFn workflow YAML. You will receive the error message below to revise your answer. @@ -127,25 +198,37 @@ prompts: and connections (edges) and references the appropriate adaptors with their exact names. Explain your correction, and be as brief as possible in your answers. - normal_mode_instructions: | + normal_mode_answering_instructions: | You can either A) answer with JUST a conversational turn responding to the user (2-4 sentences) in the "text" key and leave the "yaml" key as null, - or + or B) answer with BOTH the "text" key and the "yaml" key. - In this case, you should provide a few sentences in the "text" key (max. as many sentences as there are jobs in the workflow) to explain your reasoning. + In this case, you should provide a few sentences in the "text" key (max. as many sentences as there are jobs in the workflow) to explain your reasoning. If relevant, you can note aspects of the workflow that should be reviewed (e.g. to consider alternative approaches). In the "yaml" key, provide a proper YAML file that follows the structure above. - The user's latest message and prior conversation are provided below. Generate your response accordingly. - error_mode_instructions: | + error_mode_answering_instructions: | Answer with BOTH the "text" key and the "yaml" key. You should provide a few sentences in the "text" key (max. as many sentences as there are jobs in the workflow) to explain your reasoning about the error and your correction. If relevant, you can note aspects of the workflow that should be reviewed (e.g. to consider alternative approaches). In the "yaml" key, provide a proper YAML file that follows the structure above. - The error message along with the user's latest message and prior conversation are provided below. Generate your response accordingly. \ No newline at end of file + The error message along with the user's latest message and prior conversation are provided below. Generate your response accordingly. + + readonly_mode_answering_instructions: | + You can either + A) answer with JUST a conversational turn responding to the user (2-4 sentences) in the "text" key and leave the "yaml" key as null, + + or + + B) answer with your explanation in the "text" key, including any workflow structure inline using triple-backticked YAML code blocks. + In this case, provide a few sentences (max. as many sentences as there are jobs in the workflow) to explain your reasoning, followed by the YAML in a code block. + If relevant, you can note aspects of the workflow that should be reviewed (e.g. to consider alternative approaches). + Always leave the "yaml" key as null. + + The user's latest message and prior conversation are provided below. Generate your response accordingly. diff --git a/services/workflow_chat/tests/test_functions.py b/services/workflow_chat/tests/test_functions.py index 21a5830..7cb7a23 100644 --- a/services/workflow_chat/tests/test_functions.py +++ b/services/workflow_chat/tests/test_functions.py @@ -1,5 +1,6 @@ import pytest from workflow_chat.workflow_chat import AnthropicClient, ChatConfig +from workflow_chat.gen_project_prompt import build_prompt @pytest.fixture @@ -91,16 +92,73 @@ def test_sanitize_job_names_preserves_allowed_characters(client): def test_sanitize_job_names_handles_empty_data(client): """Test that function handles edge cases gracefully.""" - + # Should not raise exceptions and return without error result1 = client.sanitize_job_names(None) result2 = client.sanitize_job_names({}) result3 = client.sanitize_job_names({"jobs": {}}) - + assert result1 is None assert result2 is None assert result3 is None +def test_build_prompt_normal_mode(): + """Test that normal mode uses correct configuration.""" + system_msg, prompt = build_prompt( + content='Create a workflow', + existing_yaml='name: test-workflow', + history=[{'role': 'user', 'content': 'Hello'}] + ) + + # Should use normal mode configuration + assert 'talk to a client with the goal of converting' in system_msg # normal_mode_intro + assert 'You can either' in system_msg # normal_mode_answering_instructions + assert 'user is currently editing this YAML' in system_msg + assert 'name: test-workflow' in system_msg + + # Prompt structure + assert len(prompt) == 2 + assert prompt[-1]['content'] == 'Create a workflow' + + +def test_build_prompt_error_mode(): + """Test that error mode uses correct configuration and appends error message.""" + system_msg, prompt = build_prompt( + content='Fix the workflow', + existing_yaml='name: broken-workflow', + errors='Invalid trigger type', + history=[] + ) + + # Should use error mode configuration + assert 'Your previous suggestion produced an invalid' in system_msg # error_mode_intro + assert 'Answer with BOTH the' in system_msg # error_mode_answering_instructions + assert 'YAML causing the error' in system_msg + assert 'name: broken-workflow' in system_msg + + # User content should have error appended + assert prompt[-1]['content'] == 'Fix the workflow\nThis is the error message:\nInvalid trigger type' + + +def test_build_prompt_readonly_mode(): + """Test that readonly mode uses unstructured output format.""" + system_msg, prompt = build_prompt( + content='What does this workflow do?', + existing_yaml='name: readonly-workflow', + read_only=True, + history=[] + ) + + # Should use readonly mode configuration + assert 'Read-only Mode' in system_msg # unstructured_output_format + assert 'triple-backticked YAML code blocks' in system_msg # readonly_mode_answering_instructions + assert 'user is viewing this read-only YAML' in system_msg + assert 'name: readonly-workflow' in system_msg + + # User content should be unchanged + assert prompt[-1]['content'] == 'What does this workflow do?' + + if __name__ == "__main__": pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/services/workflow_chat/tests/test_pass_fail.py b/services/workflow_chat/tests/test_pass_fail.py index bfa8785..21eb37e 100644 --- a/services/workflow_chat/tests/test_pass_fail.py +++ b/services/workflow_chat/tests/test_pass_fail.py @@ -228,5 +228,52 @@ def test_special_characters(): assert_yaml_jobs_have_body(response["response_yaml"], context="test_special_characters") assert_no_special_chars(response["response_yaml"], context="test_special_characters") +def test_readonly_mode(): + print("==================TEST==================") + print("Description: Test that read-only mode returns inline YAML in text with yaml=null") + existing_yaml = """ +name: fridge-statistics-processing +jobs: + parse-and-aggregate-fridge-data: + id: job-parse-id + name: Parse and Aggregate Fridge Data + adaptor: '@openfn/language-common@latest' + body: 'print("hello a")' +triggers: + webhook: + id: trigger-webhook-id + type: webhook + enabled: false +edges: + webhook->parse-and-aggregate-fridge-data: + id: edge-webhook-parse-id + source_trigger: webhook + target_job: parse-and-aggregate-fridge-data + condition_type: always + enabled: true +""" + service_input = { + "existing_yaml": existing_yaml, + "history": [], + "content": "Add a second job that stores the results in a database", + "read_only": True + } + response = call_workflow_chat_service(service_input) + print_response_details(response, content="Add a second job that stores the results in a database") + + assert response is not None + assert isinstance(response, dict) + + # In read-only mode, yaml should be None + assert response.get("response_yaml") is None, "Read-only mode should return yaml=None" + + # Response text should not be empty + response_text = response.get("response", "") + assert response_text is not None, "Response text should not be None" + assert len(response_text) > 0, "Response text should not be empty" + + # Should contain inline YAML in triple-backticked code blocks + assert "```yaml" in response_text or "```" in response_text, "Response should contain triple-backticked YAML code block" + if __name__ == "__main__": pytest.main([__file__, "-v"]) \ No newline at end of file diff --git a/services/workflow_chat/workflow_chat.py b/services/workflow_chat/workflow_chat.py index db1e281..7962e35 100644 --- a/services/workflow_chat/workflow_chat.py +++ b/services/workflow_chat/workflow_chat.py @@ -39,7 +39,8 @@ class Payload: history: Optional[List[Dict[str, str]]] = None api_key: Optional[str] = None stream: Optional[bool] = False - + read_only: Optional[bool] = False + @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Payload": """ @@ -52,7 +53,8 @@ def from_dict(cls, data: Dict[str, Any]) -> "Payload": existing_yaml=data.get("existing_yaml"), history=data.get("history", []), api_key=data.get("api_key"), - stream=data.get("stream", False) + stream=data.get("stream", False), + read_only=data.get("read_only", False) ) @@ -86,6 +88,7 @@ def generate( errors: Optional[str] = None, history: Optional[List[Dict[str, str]]] = None, stream: Optional[bool] = False, + read_only: Optional[bool] = False, ) -> ChatResponse: """Generate a response using the Claude API. Retry up to 2 times if YAML/JSON parsing fails.""" @@ -94,23 +97,28 @@ def generate( stream_manager = StreamManager(model=self.config.model, stream=stream) - # Extract and preserve existing components + # Extract and preserve existing components (skip in read-only mode) preserved_values = {} processed_existing_yaml = existing_yaml - + if existing_yaml and existing_yaml.strip(): - try: - yaml_data = yaml.safe_load(existing_yaml) - preserved_values, processed_existing_yaml = self.extract_and_preserve_components(yaml_data) - except Exception as e: - logger.warning(f"Could not parse existing YAML for component extraction: {e}") + if not read_only: + try: + yaml_data = yaml.safe_load(existing_yaml) + preserved_values, processed_existing_yaml = self.extract_and_preserve_components(yaml_data) + except Exception as e: + logger.warning(f"Could not parse existing YAML for component extraction: {e}") + else: + # In read-only mode, remove IDs to prevent regurgitation + processed_existing_yaml = self.remove_ids_from_yaml(existing_yaml) with sentry_sdk.start_span(description="build_prompt"): system_message, prompt = build_prompt( - content=content, - existing_yaml=processed_existing_yaml, - errors=errors, - history=history + content=content, + existing_yaml=processed_existing_yaml, + errors=errors, + history=history, + read_only=read_only ) # Add prefilled opening brace for JSON response @@ -204,6 +212,28 @@ def generate( # Otherwise, log and retry logger.warning(f"YAML parsing failed, retrying generation (attempt {attempt+1}/{max_retries})") + def remove_ids_from_yaml(self, yaml_str): + """Remove all 'id' fields from YAML to prevent ID regurgitation in read-only mode.""" + if not yaml_str or not yaml_str.strip(): + return yaml_str + try: + yaml_data = yaml.safe_load(yaml_str) + + def remove_ids(obj): + if isinstance(obj, dict): + obj.pop("id", None) + for v in obj.values(): + remove_ids(v) + elif isinstance(obj, list): + for item in obj: + remove_ids(item) + + remove_ids(yaml_data) + return yaml.dump(yaml_data, sort_keys=False, default_flow_style=False) + except Exception as e: + logger.warning(f"Could not remove IDs from YAML: {e}") + return yaml_str + def sanitize_job_names(self, yaml_data): """ Sanitize job names by removing special characters and normalizing diacritics. @@ -469,7 +499,12 @@ def main(data_dict: dict) -> dict: client = AnthropicClient(config) result = client.generate( - content=data.content, existing_yaml=data.existing_yaml, errors=data.errors, history=data.history, stream=data.stream + content=data.content, + existing_yaml=data.existing_yaml, + errors=data.errors, + history=data.history, + stream=data.stream, + read_only=data.read_only ) return {