Skip to content
Merged
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
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This plugin exposes safe outbound phone-call tools for CALL-E:
- `get_call` fetches one CALL-E call result by call ID.
- `create_and_wait` previews or creates one outbound CALL-E call, then waits for a final result or timeout.
- `list_goals` and `get_goal` read published CALL-E Goals without starting a call.
- `prepare_goal_run` selects a published Goal, describes its fields, and validates a reviewed business mapping without starting a call.
- `create_goal_run`, `get_goal_run`, and `create_goal_run_and_wait` execute and monitor one phone-specific published Goal.

Phone calls are real-world side effects. Live calls require both `dry_run=false` and `confirm_live_call=true`.
Expand Down Expand Up @@ -106,6 +107,29 @@ The example starts in dry run mode. Supply `phone_number`, `appointment_time`, a

Goal Run tools follow the same live-call boundary as Call tools: live execution requires both `dry_run=false` and `confirm_live_call=true`.

#### Configure and review a Goal Run

Add **Prepare CALL-E Goal Run** before a creation tool. Its **Published Goal** selector lists the authenticated owner's executable Goals by title, published version, and stable Goal ID. It reads the existing Goal APIs, including every catalog page.

1. Choose the intended Goal and run this read-only node once. The output describes input and result fields, required flags, types, enum options, defaults, and the immutable published RunSpec ID and version.
2. Review those fields, then copy the node's `goal_json` output into its **Reviewed Goal interface** configuration field. Save this value with the workflow. Keep it fixed across business events until you review another publication.
3. Map upstream business data into **Mapped business variables JSON**. Bind a Code node's output instead of assembling a JSON string in a prompt. Preparation merges the published defaults and checks missing, unknown, wrongly typed, and invalid enum values. Boolean `false`, integer `0`, and other business values keep their types.
4. Gate the creation node on `ready=true`. Bind its Goal ID and Variables JSON to the preparation node's `goal_id` and `variables_json` outputs. These routing outputs are empty when preparation fails. The `errors` and `changes` outputs identify fields needing attention.

Preparation checks the current interface against the saved one, including result mappings. A new publication or changed field requirement stops this path until the operator reviews and updates the saved interface. Unsupported API/schema shapes also stop preparation with a field-level error. Keep the reviewed interface in configuration; binding it to the current `get_goal` response on every run would silently approve changes.

The API chooses the RunSpec at acceptance. A read-only preflight is not an atomic version selector: publishing between preparation and creation can still change the accepted version. Compare the returned Run's `run_spec` with the prepared ID and version before acting on its result. Retain the accepted Run ID when they differ; do not redial.

For recovery, retain the original business event, prepared variables, phone, and idempotency key before submission. If creation was uncertain, replay that exact saved request through the existing **Create CALL-E Goal Run** tool, which does not depend on the current Goal interface. If the Run ID is known, resume with **Get CALL-E Goal Run**. A failed configuration check for a new event is not a reason to change a saved recovery key.

#### Business-field mapping example

Import [goal-run-configuration.yml](examples/goal-run-configuration.yml) after installing this plugin and configuring its credentials. The example maps a saved record's `reference` to the published Goal's `test_reference` input, and its saved `event_id` to the idempotency key. Use a published integration-test Goal whose required input is a string `test_reference` and whose nullable `confirmed` result is `yes`, `no`, or `unknown`. For another Goal, edit the **Map business fields** and **Check accepted version and result** Code nodes to match the fields shown by preparation.

Select that Goal in **Review and validate Goal inputs**, run the workflow, review the returned field report, and save its `current_goal_json` in **Reviewed Goal interface**. Supply an owned or explicitly authorized E.164 phone number in `business_record.phone_number`. The placeholder number is blocked for live calls. Keep the record and event ID unchanged for replay. Only after inspecting the preview, change **Execute reviewed Goal** to `dry_run=false` and `confirm_live_call=true` for one authorized test.

The example gates creation on valid configuration, checks the accepted RunSpec again, and returns `confirmed` only for explicit `result.confirmed=yes` with an available result, completed telephone outcome, no error, and no timeout. Empty, unavailable, unknown, and changed-interface results remain for manual review. Preparation and its field selector never place a call. Mapped variables and Run outputs may contain personal data; restrict access to workflow runs and downstream consumers.

## Upgrade from 0.1.x

Version 0.2.0 moves new Calls to v2. Before upgrading a workflow:
Expand Down
203 changes: 203 additions & 0 deletions examples/goal-run-configuration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
app:
name: CALL-E reviewed Goal Run
description: Map a saved business record, validate a reviewed Goal interface, and run only when ready. Starts in dry run mode.
mode: workflow
icon: telephone_receiver
icon_type: emoji
icon_background: '#E8F0FE'
dependencies: []
kind: app
version: 0.7.0
workflow:
conversation_variables: []
environment_variables: []
features: {}
graph:
edges:
- {id: start-map, source: start, sourceHandle: source, target: map, targetHandle: target, type: custom, data: {sourceType: start, targetType: code}}
- {id: map-prepare, source: map, sourceHandle: source, target: prepare, targetHandle: target, type: custom, data: {sourceType: code, targetType: tool}}
- {id: prepare-gate, source: prepare, sourceHandle: source, target: gate, targetHandle: target, type: custom, data: {sourceType: tool, targetType: if-else}}
- {id: gate-run, source: gate, sourceHandle: ready, target: run, targetHandle: target, type: custom, data: {sourceType: if-else, targetType: tool}}
- {id: gate-attention, source: gate, sourceHandle: 'false', target: attention, targetHandle: target, type: custom, data: {sourceType: if-else, targetType: end}}
- {id: run-inspect, source: run, sourceHandle: source, target: inspect, targetHandle: target, type: custom, data: {sourceType: tool, targetType: code}}
- {id: inspect-end, source: inspect, sourceHandle: source, target: end, targetHandle: target, type: custom, data: {sourceType: code, targetType: end}}
nodes:
- id: start
type: custom
position: {x: 0, y: 160}
data:
type: start
title: Saved business event
variables:
- variable: business_record
label: Saved business record JSON
type: paragraph
required: true
max_length: 10000
default: '{"reference":"ORDER-8472","phone_number":"+14155550123","event_id":"order-8472:confirm:1"}'
- id: map
type: custom
position: {x: 300, y: 160}
data:
type: code
title: Map business fields
code_language: python3
variables:
- {variable: business_record, value_selector: [start, business_record]}
outputs:
variables_json: {type: string, children: null}
phone_number: {type: string, children: null}
idempotency_key: {type: string, children: null}
code: |
import json

def main(business_record: str) -> dict:
record = json.loads(business_record)
if not isinstance(record, dict):
raise ValueError("Business record must be a JSON object.")
for field in ("reference", "phone_number", "event_id"):
if not isinstance(record.get(field), str) or not record[field].strip():
raise ValueError("Business record requires a non-empty " + field + ".")
return {
"variables_json": json.dumps({"test_reference": record["reference"]}, sort_keys=True),
"phone_number": record["phone_number"],
"idempotency_key": record["event_id"],
}
- id: prepare
type: custom
position: {x: 600, y: 160}
data:
type: tool
title: Review and validate Goal inputs
provider_type: builtin
provider_id: derekwang2002/call_e/call_e
provider_name: derekwang2002/call_e/call_e
provider_show_name: CALL-E
plugin_id: derekwang2002/call_e
tool_name: prepare_goal_run
tool_label: Prepare CALL-E Goal Run
tool_node_version: '2'
tool_configurations:
goal_id: {type: constant, value: ''}
reviewed_goal_json: {type: constant, value: ''}
tool_parameters:
variables_json: {type: variable, value: [map, variables_json]}
- id: gate
type: custom
position: {x: 900, y: 160}
data:
type: if-else
title: Configuration ready?
cases:
- case_id: ready
logical_operator: and
conditions:
- {id: configuration-ready, variable_selector: [prepare, ready], varType: boolean, comparison_operator: is, value: true}
- id: run
type: custom
position: {x: 1200, y: 0}
data:
type: tool
title: Execute reviewed Goal
provider_type: builtin
provider_id: derekwang2002/call_e/call_e
provider_name: derekwang2002/call_e/call_e
provider_show_name: CALL-E
plugin_id: derekwang2002/call_e
tool_name: create_goal_run_and_wait
tool_label: Create And Wait For CALL-E Goal Run
tool_node_version: '2'
tool_configurations: {}
tool_parameters:
goal_id: {type: variable, value: [prepare, goal_id]}
variables_json: {type: variable, value: [prepare, variables_json]}
phone_number: {type: variable, value: [map, phone_number]}
idempotency_key: {type: variable, value: [map, idempotency_key]}
dry_run: {type: constant, value: true}
confirm_live_call: {type: constant, value: false}
poll_interval_seconds: {type: constant, value: 5}
wait_timeout_minutes: {type: constant, value: 4}
- id: inspect
type: custom
position: {x: 1500, y: 0}
data:
type: code
title: Check accepted version and result
code_language: python3
variables:
- {variable: tool_output, value_selector: [run, json]}
- {variable: expected_id, value_selector: [prepare, run_spec_id]}
- {variable: expected_version, value_selector: [prepare, run_spec_version]}
outputs:
route: {type: string, children: null}
goal_run_id: {type: string, children: null}
business_result_json: {type: string, children: null}
error_json: {type: string, children: null}
code: |
import json

def main(tool_output: list, expected_id: str, expected_version: int) -> dict:
envelope = tool_output[0] if tool_output and isinstance(tool_output[0], dict) else {}
resource = envelope.get("goal_run")
if not isinstance(resource, dict):
resource = envelope.get("accepted_resource")
resource = resource if isinstance(resource, dict) else {}
goal_run_id = envelope.get("goal_run_id") or resource.get("id") or ""
error = envelope.get("error")
if error is None:
error = resource.get("error")
result = resource.get("result")
route = "manual_review"
if not goal_run_id and envelope.get("dry_run") is True and error is None:
route = "preview"
elif goal_run_id:
spec = resource.get("run_spec")
same_version = (
isinstance(spec, dict)
and spec.get("id") == expected_id
and type(spec.get("version")) is int
and spec["version"] == expected_version
)
if not same_version:
route = "interface_changed_after_acceptance"
elif (
resource.get("object") == "goal_run"
and resource.get("result_status") == "available"
and resource.get("call_outcome") == "completed"
and error is None
and not envelope.get("timed_out")
and isinstance(result, dict)
and result.get("confirmed") == "yes"
):
route = "confirmed"
return {
"route": route,
"goal_run_id": goal_run_id,
"business_result_json": json.dumps(result, ensure_ascii=False),
"error_json": json.dumps(error, ensure_ascii=False),
}
- id: attention
type: custom
position: {x: 1200, y: 320}
data:
type: end
title: Review configuration
outputs:
- {variable: ready, value_selector: [prepare, ready], value_type: boolean}
- {variable: configuration_report, value_selector: [prepare, report], value_type: string}
- {variable: validation_errors, value_selector: [prepare, errors], value_type: 'array[object]'}
- {variable: interface_changes, value_selector: [prepare, changes], value_type: 'array[object]'}
- {variable: current_goal_json, value_selector: [prepare, goal_json], value_type: string}
- id: end
type: custom
position: {x: 1800, y: 0}
data:
type: end
title: Goal Run outcome
outputs:
- {variable: route, value_selector: [inspect, route], value_type: string}
- {variable: goal_run_id, value_selector: [inspect, goal_run_id], value_type: string}
- {variable: business_result_json, value_selector: [inspect, business_result_json], value_type: string}
- {variable: error_json, value_selector: [inspect, error_json], value_type: string}
- {variable: idempotency_key, value_selector: [map, idempotency_key], value_type: string}
- {variable: response, value_selector: [run, json], value_type: 'array[object]'}
2 changes: 1 addition & 1 deletion manifest.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version: 0.3.0
version: 0.4.0
type: plugin
author: derekwang2002
name: call_e
Expand Down
1 change: 1 addition & 0 deletions provider/call_e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ tools:
- tools/create_and_wait.yaml
- tools/list_goals.yaml
- tools/get_goal.yaml
- tools/prepare_goal_run.yaml
- tools/create_goal_run.yaml
- tools/get_goal_run.yaml
- tools/create_goal_run_and_wait.yaml
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "call-e-dify-plugin"
version = "0.3.0"
version = "0.4.0"
description = "CALL-E tools for Dify workflows and agents"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
Loading
Loading