Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions services/workflow_chat/gen_project_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
temperature: 0
115 changes: 67 additions & 48 deletions services/workflow_chat/gen_project_prompt.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@

import os
import json
from .config_loader import ConfigLoader
from .available_adaptors import get_adaptors_string

Expand All @@ -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)
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)
115 changes: 99 additions & 16 deletions services/workflow_chat/gen_project_prompts.yaml
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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.
Expand All @@ -119,33 +190,45 @@ 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.
Your task is to produce a corrected, properly structured OpenFn workflow YAML that defines workflow jobs (steps), triggers (webhook or cron),
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.
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.
Loading