@ema.co/mcp-toolkit 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of @ema.co/mcp-toolkit might be problematic. Click here for more details.

@@ -14,21 +14,42 @@
14
14
  // ─────────────────────────────────────────────────────────────────────────────
15
15
  const ACTION_NAMESPACES = {
16
16
  chat_trigger: ["triggers", "emainternal"],
17
+ voice_trigger: ["triggers", "emainternal"],
17
18
  document_trigger: ["triggers", "emainternal"],
18
19
  chat_categorizer: ["routing", "emainternal"],
19
20
  text_categorizer: ["routing", "emainternal"],
20
- search: ["search", "emainternal"],
21
- live_web_search: ["search", "emainternal"],
22
- respond_with_sources: ["generation", "emainternal"],
23
- call_llm: ["generation", "emainternal"],
24
- fixed_response: ["generation", "emainternal"],
25
- external_action_caller: ["external", "emainternal"],
26
- general_hitl: ["collaboration", "emainternal"],
27
- send_email_agent: ["external", "emainternal"],
28
- conversation_to_search_query: ["search", "emainternal"],
29
- entity_extraction: ["entity", "emainternal"],
30
- combine_search_results: ["search", "emainternal"],
31
- response_validator: ["validation", "emainternal"],
21
+ search: ["actions", "emainternal"], // Must be "actions" not "search"
22
+ live_web_search: ["actions", "emainternal"],
23
+ respond_with_sources: ["actions", "emainternal"], // Must be "actions" not "generation"
24
+ call_llm: ["actions", "emainternal"],
25
+ fixed_response: ["actions", "emainternal"],
26
+ external_action_caller: ["actions", "emainternal"],
27
+ general_hitl: ["actions", "emainternal"],
28
+ send_email_agent: ["actions", "emainternal"],
29
+ conversation_to_search_query: ["actions", "emainternal"],
30
+ entity_extraction: ["actions", "emainternal"],
31
+ combine_search_results: ["actions", "emainternal"],
32
+ response_validator: ["actions", "emainternal"],
33
+ };
34
+ // Version mapping for actions
35
+ const ACTION_VERSIONS = {
36
+ chat_trigger: "v1",
37
+ voice_trigger: "v1",
38
+ document_trigger: "v1",
39
+ chat_categorizer: "v1",
40
+ text_categorizer: "v1",
41
+ search: "v2",
42
+ live_web_search: "v1",
43
+ respond_with_sources: "v1",
44
+ call_llm: "v1",
45
+ fixed_response: "v1",
46
+ external_action_caller: "v1",
47
+ general_hitl: "v1",
48
+ send_email_agent: "v1",
49
+ conversation_to_search_query: "v1",
50
+ entity_extraction: "v1",
51
+ combine_search_results: "v1",
52
+ response_validator: "v1",
32
53
  };
33
54
  const OPERATOR_MAP = {
34
55
  eq: 1,
@@ -92,31 +113,42 @@ export function compileWorkflow(spec) {
92
113
  workflowName: {
93
114
  name: {
94
115
  namespaces: ["ema", "workflows"],
95
- name: spec.name.toLowerCase().replace(/\s+/g, "_"),
116
+ name: spec.name.toLowerCase().replace(/\s+/g, "_").slice(0, 50), // Limit name length
96
117
  },
97
118
  },
98
119
  actions,
120
+ enumTypes: validEnumTypes.length > 0 ? validEnumTypes : [], // Always include, even if empty
121
+ workflowInputs: {},
99
122
  results,
123
+ namedResults: {},
124
+ displayName: spec.name,
125
+ description: spec.description || "",
126
+ namedResultsEditable: false,
127
+ namedResultsEnabled: false,
100
128
  };
101
- // Only add enumTypes if there are valid ones
102
- if (validEnumTypes.length > 0) {
103
- workflowDef.enumTypes = validEnumTypes;
104
- }
105
129
  // Build proto_config
106
130
  const protoConfig = buildProtoConfig(spec);
107
131
  return { workflow_def: workflowDef, proto_config: protoConfig };
108
132
  }
109
133
  function buildAction(node, enumTypeName) {
110
- const namespaces = ACTION_NAMESPACES[node.actionType] ?? ["custom", "emainternal"];
134
+ const namespaces = ACTION_NAMESPACES[node.actionType] ?? ["actions", "emainternal"];
135
+ const version = ACTION_VERSIONS[node.actionType] ?? "v1";
111
136
  const action = {
112
137
  // CRITICAL: Use "name" not "actionName" - this is the node identifier in the workflow
113
138
  name: node.id,
114
- actionDisplayName: node.displayName,
115
- actionDescription: node.description ?? "",
116
139
  action: {
117
140
  name: { namespaces, name: node.actionType },
141
+ version,
118
142
  },
119
143
  inputs: {},
144
+ displaySettings: {
145
+ displayName: node.displayName ?? "",
146
+ description: node.description ?? "",
147
+ coordinates: { x: 800, y: 200 }, // Default position
148
+ showConfig: 0,
149
+ },
150
+ typeArguments: {},
151
+ tools: [],
120
152
  disableHumanInteraction: node.disableHitl ?? false,
121
153
  };
122
154
  // Build inputs
@@ -218,17 +250,80 @@ function buildInputBinding(binding) {
218
250
  }
219
251
  function buildProtoConfig(spec) {
220
252
  const projectType = spec.personaType === "voice" ? 5 : spec.personaType === "chat" ? 4 : 2;
253
+ // Widget format expected by Ema API:
254
+ // { name: "widgetName", type: <type_id>, widgetName: { ...config... } }
255
+ // The config is stored under a key matching the widget's name
221
256
  const widgets = [
222
- { widget_type_id: 6, widget_name: "fusionModel", widget_config: { selectedModels: ["gpt-4.1"] } },
223
- { widget_type_id: 8, widget_name: "dataProtection", widget_config: { enabled: true } },
257
+ {
258
+ name: "fusionModel",
259
+ type: 6,
260
+ fusionModel: { selectedModels: ["gpt-4.1"] }
261
+ },
262
+ {
263
+ name: "dataProtection",
264
+ type: 8,
265
+ dataProtection: { enabled: true }
266
+ },
224
267
  ];
225
268
  if (spec.personaType === "voice") {
226
- widgets.push({ widget_type_id: 38, widget_name: "voiceSettings", widget_config: { languageHints: ["en-US"], voiceModel: "default" } }, { widget_type_id: 39, widget_name: "conversationSettings", widget_config: {} }, { widget_type_id: 43, widget_name: "vadSettings", widget_config: { turnTimeout: 5, silenceEndCallTimeout: 30, maxConversationDuration: 300 } });
269
+ // Build voice settings from spec.voiceConfig with sensible defaults
270
+ const vc = spec.voiceConfig ?? {};
271
+ const defaultIdentity = `You are ${spec.name}. ${spec.description}`;
272
+ widgets.push({
273
+ name: "voiceSettings",
274
+ type: 38,
275
+ voiceSettings: {
276
+ languageHints: vc.languageHints ?? ["en-US"],
277
+ voiceModel: "default"
278
+ }
279
+ }, {
280
+ name: "conversationSettings",
281
+ type: 39,
282
+ conversationSettings: {
283
+ welcomeMessage: vc.welcomeMessage ?? `Hello, this is ${spec.name}. How can I help you today?`,
284
+ identityAndPurpose: vc.identityAndPurpose ?? defaultIdentity,
285
+ takeActionInstructions: vc.takeActionInstructions ?? "Take appropriate actions based on the user's request. Always confirm before making changes.",
286
+ hangupInstructions: vc.hangupInstructions ?? "End the call politely when the user's request is complete or they ask to hang up.",
287
+ transferCallInstructions: vc.transferInstructions ?? "",
288
+ speechCharacteristics: vc.speechCharacteristics ?? "Professional, friendly, and helpful",
289
+ systemPrompt: vc.systemPrompt ?? "",
290
+ waitMessage: vc.waitMessage ?? "One moment please while I look that up...",
291
+ }
292
+ }, {
293
+ name: "vadSettings",
294
+ type: 43,
295
+ vadSettings: {
296
+ turnTimeout: 5,
297
+ silenceEndCallTimeout: 30,
298
+ maxConversationDuration: 300
299
+ }
300
+ });
227
301
  }
228
302
  if (spec.personaType === "chat") {
229
- widgets.push({ widget_type_id: 28, widget_name: "chatbotSdkConfig", widget_config: { name: spec.name, theme: { primaryColor: "#0066cc" }, allowedDomains: ["*"] } }, { widget_type_id: 33, widget_name: "feedbackMessage", widget_config: { message: { question: "Was this response helpful?" }, feedbackFrequency: "always" } });
303
+ // Build chat settings from spec.chatConfig with sensible defaults
304
+ const cc = spec.chatConfig ?? {};
305
+ widgets.push({
306
+ name: "chatbotSdkConfig",
307
+ type: 28,
308
+ chatbotSdkConfig: {
309
+ name: cc.name ?? spec.name,
310
+ theme: { primaryColor: cc.primaryColor ?? "#0066cc" },
311
+ allowedDomains: cc.allowedDomains ?? ["*"]
312
+ }
313
+ }, {
314
+ name: "feedbackMessage",
315
+ type: 33,
316
+ feedbackMessage: {
317
+ message: { question: cc.feedbackQuestion ?? "Was this response helpful?" },
318
+ feedbackFrequency: "always"
319
+ }
320
+ });
230
321
  }
231
- widgets.push({ widget_type_id: 3, widget_name: "fileUpload", widget_config: { useChunking: true } });
322
+ widgets.push({
323
+ name: "fileUpload",
324
+ type: 3,
325
+ fileUpload: { useChunking: true }
326
+ });
232
327
  return {
233
328
  project_type: projectType,
234
329
  name: spec.name,
@@ -1433,12 +1433,21 @@ export function intentToSpec(intent) {
1433
1433
  nodeId: respondId,
1434
1434
  output: searchId ? "response_with_sources" : "response_with_sources",
1435
1435
  });
1436
+ // Build voiceConfig from intent.voice_config
1437
+ const voiceConfig = intent.persona_type === "voice" ? {
1438
+ welcomeMessage: intent.voice_config?.welcome_message,
1439
+ identityAndPurpose: intent.voice_config?.identity ?? intent.description,
1440
+ hangupInstructions: intent.voice_config?.hangup_instructions,
1441
+ // Additional fields can be extracted from intent description in the future
1442
+ } : undefined;
1436
1443
  return {
1437
1444
  name: intent.name,
1438
1445
  description: intent.description,
1439
1446
  personaType: intent.persona_type,
1440
1447
  nodes,
1441
1448
  resultMappings,
1449
+ ...(voiceConfig && { voiceConfig }),
1450
+ ...(intent.persona_type === "chat" && { chatConfig: { name: intent.name } }),
1442
1451
  };
1443
1452
  }
1444
1453
  export function parseInput(input) {
@@ -60,7 +60,8 @@ interface Node {
60
60
  \`\`\`typescript
61
61
  type ActionType =
62
62
  // Triggers (entry points)
63
- | "chat_trigger" // Chat/voice entry point
63
+ | "chat_trigger" // Chat message entry point
64
+ | "voice_trigger" // Phone/voice call entry point
64
65
  | "document_trigger" // Document upload entry point
65
66
 
66
67
  // Routing (categorization)
@@ -350,6 +351,7 @@ function decompileAction(action, enumLookup) {
350
351
  function mapToActionType(rawName) {
351
352
  const mapping = {
352
353
  chat_trigger: "chat_trigger",
354
+ voice_trigger: "voice_trigger",
353
355
  document_trigger: "document_trigger",
354
356
  chat_categorizer: "chat_categorizer",
355
357
  text_categorizer: "text_categorizer",
@@ -30,19 +30,29 @@ Some clients show a **prefixed name** (for example Cursor: `mcp_ema_workflow`).
30
30
 
31
31
  ### Create a new AI Employee (greenfield)
32
32
 
33
+ **Architecture**: Creates persona from template, configures settings. Template provides valid workflow.
34
+
33
35
  1. **Requirements**: `template(questions=true)` (and `template(questions=true, category="Voice")` for voice)
34
36
  2. **Pick agents/pattern**: `action(suggest="<use case>")`
35
- 3. **Get the pattern**: `template(pattern="<pattern>")`
36
- 4. **Generate workflow** (preview by default):
37
+ 3. **Create and configure in one step**:
37
38
  ```typescript
38
- workflow(input="<requirements>", type="chat|voice|dashboard")
39
+ workflow(
40
+ input="<description of what it does>",
41
+ name="My AI Employee",
42
+ type="voice|chat|dashboard",
43
+ preview=false
44
+ )
39
45
  ```
40
- 5. **Create persona** (if needed): `persona(mode="create", name="...", type="chat|voice|dashboard")`
41
- 6. **Deploy** (preview=false):
46
+ This creates the persona from template and configures proto_config (voice settings, welcome message, etc.)
47
+
48
+ 4. **Customize workflow** (if needed - AFTER creation):
42
49
  ```typescript
43
- workflow(input="<requirements>", persona_id="<id>", preview=false)
50
+ workflow(persona_id="<id>", input="add search node before responding")
44
51
  ```
45
- 7. **Review**: `workflow(mode="analyze", persona_id="...")`
52
+
53
+ 5. **Review**: `workflow(persona_id="<id>")` to analyze
54
+
55
+ **Key insight**: Don't try to generate and deploy a custom workflow in greenfield. Create from template first, then modify.
46
56
 
47
57
  ### Extend an existing AI Employee (brownfield)
48
58
 
@@ -129,10 +139,11 @@ persona(id="My Bot", mode="version_policy", auto_on_deploy=true)
129
139
 
130
140
  All modification modes default to `preview=true` for safety. Use `preview=false` to deploy.
131
141
 
132
- - **Generate (greenfield)**: Create new workflow
142
+ - **Create (greenfield)**: Create new persona from template with configured settings
133
143
  ```typescript
134
- workflow(input="IT helpdesk with KB search") // Preview
135
- workflow(input="...", persona_id="abc", preview=false) // Generate AND deploy
144
+ workflow(input="IT helpdesk chatbot", name="IT Helper", type="chat", preview=false)
145
+ // Creates persona from template, configures proto_config
146
+ // Template workflow is preserved - customize later via modify
136
147
  ```
137
148
 
138
149
  - **Extend (brownfield)**: Modify existing workflow with natural language
@@ -299,12 +310,13 @@ The MCP automatically condenses long prompts via `condensePromptForAutobuilder()
299
310
 
300
311
  ## Workflow Generation Strategy
301
312
 
302
- | Approach | When to Use | Notes |
303
- |----------|-------------|-------|
304
- | **Local compilation** | Simple/medium workflows | `workflow(mode="compile", ...)` - reliable, fast |
305
- | **Auto Builder** | Complex/exploratory workflows | Requires concise prompts (<2500 chars) |
313
+ | Scenario | Approach | How |
314
+ |----------|----------|-----|
315
+ | **New persona** (greenfield) | Create from template | `workflow(input="...", name="...", type="...", preview=false)` |
316
+ | **Modify existing** (brownfield) | LLM-native transform | `workflow(persona_id="...", input="add X")` |
317
+ | **Complex workflows** | Auto Builder | May time out with long prompts (>2500 chars) |
306
318
 
307
- Prefer local compilation for deterministic results. Use `use_autobuilder=false` flag when generating workflows.
319
+ **DO NOT** try to compile custom workflows for new personas. The template provides a valid workflow structure - customize it after creation via brownfield/modify mode.
308
320
 
309
321
  ## Legacy tools (migration only)
310
322
 
@@ -0,0 +1,196 @@
1
+ # Persona Creation Test Guide
2
+
3
+ This guide tests the workflow tool's ability to create and configure AI Employees (personas).
4
+
5
+ ## Architecture
6
+
7
+ ### Greenfield (New Personas)
8
+ 1. **Create** persona from template (provides valid workflow structure)
9
+ 2. **Fetch** the created persona (get template's structure)
10
+ 3. **Update** proto_config only (voice settings, welcome message, etc.)
11
+ 4. **Customize workflow later** via modify mode if needed
12
+
13
+ ### Brownfield (Existing Personas)
14
+ 1. **Fetch** existing persona and workflow
15
+ 2. **Decompile** workflow_def → WorkflowSpec (typed representation)
16
+ 3. **Transform** spec based on user intent (LLM-native)
17
+ 4. **Compile** back to workflow_def
18
+ 5. **Update** persona with transformed workflow
19
+
20
+ ## Prerequisites
21
+
22
+ 1. **Rebuild the code**:
23
+ ```bash
24
+ cd /path/to/ema-mcp-toolkit
25
+ npm run build
26
+ ```
27
+
28
+ 2. **Restart the MCP server** (Cursor caches code):
29
+ - `Cmd+Shift+P` → "Developer: Reload Window"
30
+
31
+ 3. Access to Ema dev environment
32
+
33
+ ---
34
+
35
+ ## Test Cases
36
+
37
+ ### Test 1: Create Voice AI Employee (Greenfield)
38
+
39
+ **Goal:** Create a new Voice AI persona from template with configured settings.
40
+
41
+ **Command:**
42
+ ```
43
+ workflow(
44
+ input="Voice AI sales development representative for Ema. Handles discovery calls, qualifies prospects, explains value propositions.",
45
+ name="SP-TEST-VOICE-GREENFIELD",
46
+ type="voice",
47
+ preview=false,
48
+ env="dev"
49
+ )
50
+ ```
51
+
52
+ **Expected Result:**
53
+ - `status: "deployed"`
54
+ - `deployed_to.created: true`
55
+ - `deployed_to.persona_id` returned
56
+ - `next_steps` explains how to customize workflow
57
+
58
+ **Verify:**
59
+ ```
60
+ persona(id="<persona_id>", include_workflow=true, env="dev")
61
+ ```
62
+
63
+ **Check:**
64
+ - [ ] Persona exists with correct name
65
+ - [ ] `proto_config.widgets` contains `conversationSettings` with:
66
+ - [ ] `welcomeMessage` - generated greeting
67
+ - [ ] `identityAndPurpose` - from description
68
+ - [ ] `workflow_def` has template workflow (trigger node + possibly more from template)
69
+
70
+ ---
71
+
72
+ ### Test 2: Create Chat AI Employee (Greenfield)
73
+
74
+ **Command:**
75
+ ```
76
+ workflow(
77
+ input="IT helpdesk chatbot that answers employee questions about password resets, VPN setup, and software installation.",
78
+ name="SP-TEST-CHAT-GREENFIELD",
79
+ type="chat",
80
+ preview=false,
81
+ env="dev"
82
+ )
83
+ ```
84
+
85
+ **Expected Result:**
86
+ - `status: "deployed"`
87
+ - `deployed_to.created: true`
88
+
89
+ **Verify:**
90
+ ```
91
+ persona(id="<persona_id>", include_workflow=true, env="dev")
92
+ ```
93
+
94
+ ---
95
+
96
+ ### Test 3: Modify Existing Workflow (Brownfield)
97
+
98
+ **Prerequisites:** Create a persona first (Test 1 or 2).
99
+
100
+ **Goal:** Add functionality to an existing persona's workflow.
101
+
102
+ **Command:**
103
+ ```
104
+ workflow(
105
+ persona_id="<persona_id_from_test_1>",
106
+ input="add a search node that queries the knowledge base before responding",
107
+ preview=true,
108
+ env="dev"
109
+ )
110
+ ```
111
+
112
+ **Expected Result:**
113
+ - `mode: "modify"`
114
+ - Shows proposed changes
115
+ - `modified_workflow` with new nodes
116
+
117
+ **Deploy the changes:**
118
+ ```
119
+ workflow(
120
+ persona_id="<persona_id>",
121
+ input="add a search node that queries the knowledge base before responding",
122
+ preview=false,
123
+ env="dev"
124
+ )
125
+ ```
126
+
127
+ ---
128
+
129
+ ### Test 4: Analyze Workflow
130
+
131
+ **Command:**
132
+ ```
133
+ workflow(persona_id="<persona_id>", env="dev")
134
+ ```
135
+
136
+ **Expected Result:**
137
+ - `mode: "analyze"`
138
+ - `issues` array (may be empty if healthy)
139
+ - `metrics` with node_count, edge_count
140
+
141
+ ---
142
+
143
+ ### Test 5: Preview Only (No Deploy)
144
+
145
+ **Goal:** Verify preview mode returns workflow without deploying.
146
+
147
+ **Command:**
148
+ ```
149
+ workflow(
150
+ input="Customer support voice AI that handles billing inquiries",
151
+ type="voice"
152
+ )
153
+ ```
154
+
155
+ **Expected Result:**
156
+ - `status: "preview"`
157
+ - `workflow_def` returned
158
+ - `proto_config` with voice settings populated
159
+ - NO persona created
160
+
161
+ ---
162
+
163
+ ## Cleanup
164
+
165
+ After testing, delete test personas via the Ema UI or leave them for inspection.
166
+
167
+ Test persona names:
168
+ - SP-TEST-VOICE-GREENFIELD
169
+ - SP-TEST-CHAT-GREENFIELD
170
+
171
+ ---
172
+
173
+ ## Troubleshooting
174
+
175
+ ### "Internal Server Error"
176
+ - Usually means workflow format is wrong
177
+ - Check if MCP server has latest code (restart Cursor)
178
+
179
+ ### "Widget name is empty"
180
+ - Old code issue - restart MCP server
181
+
182
+ ### "Workflow name does not match"
183
+ - Old code tried to replace workflow - now we don't touch workflow in greenfield
184
+
185
+ ### proto_config not updated
186
+ - Check that greenfield flow only updates proto_config, not workflow
187
+ - Widget merging should preserve template structure
188
+
189
+ ---
190
+
191
+ ## Key Code Locations
192
+
193
+ - **Greenfield flow**: `src/mcp/handlers-consolidated.ts` (search for "GREENFIELD")
194
+ - **Brownfield/modify flow**: `src/mcp/handlers-consolidated.ts` (search for "modify")
195
+ - **Workflow transformer**: `src/sdk/workflow-transformer.ts` (decompile/compile)
196
+ - **Proto config generation**: `src/sdk/workflow-generator.ts` (buildProtoConfig)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ema.co/mcp-toolkit",
3
- "version": "1.5.0",
3
+ "version": "1.5.2",
4
4
  "description": "Ema AI Employee toolkit - MCP server, CLI, and SDK for managing AI Employees across environments",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,63 @@
1
+ # Demo Scenarios
2
+
3
+ Pre-built demo data scenarios for reliable AI Employee demonstrations.
4
+
5
+ ## Available Scenarios
6
+
7
+ | Scenario | Use Case | Persona Types |
8
+ |----------|----------|---------------|
9
+ | `sales-sdr` | Sales Development Representative | Voice, Chat |
10
+ | `support-tier1` | Customer Support Tier 1 | Voice, Chat |
11
+ | `hr-assistant` | HR Policy & Benefits Assistant | Chat, Voice |
12
+
13
+ ## Usage
14
+
15
+ Generate a demo kit:
16
+
17
+ ```
18
+ demo(mode="kit", persona_id="...", scenario="sales-sdr")
19
+ ```
20
+
21
+ Validate demo readiness:
22
+
23
+ ```
24
+ demo(mode="validate_kit", persona_id="...")
25
+ ```
26
+
27
+ ## What's Generated
28
+
29
+ Each demo kit includes:
30
+
31
+ 1. **KB Documents** - Markdown files for the knowledge base with demo-ready data
32
+ 2. **Demo Script** - Questions to ask with expected answers
33
+ 3. **Fixed Response Fallbacks** - Workflow nodes for guaranteed responses
34
+ 4. **Validation Queries** - Test queries to verify before the demo
35
+
36
+ ## Customization
37
+
38
+ Add custom Q&A pairs:
39
+
40
+ ```
41
+ demo(mode="kit", persona_id="...", scenario="sales-sdr", custom_qa=[
42
+ {"question": "Tell me about feature X", "answer": "Feature X does..."}
43
+ ])
44
+ ```
45
+
46
+ ## Creating Custom Scenarios
47
+
48
+ Create a JSON file in this directory with the following structure:
49
+
50
+ ```json
51
+ {
52
+ "id": "my-scenario",
53
+ "name": "My Custom Scenario",
54
+ "description": "Description",
55
+ "persona_types": ["voice", "chat"],
56
+ "tags": ["tag1", "tag2"],
57
+ "intents": [...],
58
+ "entities": [...],
59
+ "qa_pairs": [...]
60
+ }
61
+ ```
62
+
63
+ See existing scenarios for complete examples.