durable_workflow 0.1.0
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.
- checksums.yaml +7 -0
- data/.claude/todo/01.amend.md +133 -0
- data/.claude/todo/02.amend.md +444 -0
- data/.claude/todo/phase-1-core/01-GEMSPEC.md +193 -0
- data/.claude/todo/phase-1-core/02-TYPES.md +462 -0
- data/.claude/todo/phase-1-core/03-EXECUTION.md +551 -0
- data/.claude/todo/phase-1-core/04-STEPS.md +603 -0
- data/.claude/todo/phase-1-core/05-PARSER.md +719 -0
- data/.claude/todo/phase-1-core/todo.md +574 -0
- data/.claude/todo/phase-2-runtime/01-STORAGE.md +641 -0
- data/.claude/todo/phase-2-runtime/02-RUNNERS.md +511 -0
- data/.claude/todo/phase-3-extensions/01-EXTENSION-SYSTEM.md +298 -0
- data/.claude/todo/phase-3-extensions/02-AI-PLUGIN.md +936 -0
- data/.claude/todo/phase-3-extensions/todo.md +262 -0
- data/.claude/todo/phase-4-ai-rework/01-DEPENDENCIES.md +107 -0
- data/.claude/todo/phase-4-ai-rework/02-CONFIGURATION.md +123 -0
- data/.claude/todo/phase-4-ai-rework/03-TOOL-REGISTRY.md +237 -0
- data/.claude/todo/phase-4-ai-rework/04-MCP-SERVER.md +432 -0
- data/.claude/todo/phase-4-ai-rework/05-MCP-CLIENT.md +333 -0
- data/.claude/todo/phase-4-ai-rework/06-EXECUTORS.md +397 -0
- data/.claude/todo/phase-4-ai-rework/todo.md +265 -0
- data/.claude/todo/phase-5-validation/.DS_Store +0 -0
- data/.claude/todo/phase-5-validation/01-TEST-GAPS.md +615 -0
- data/.claude/todo/phase-5-validation/01-TESTS.md +2378 -0
- data/.claude/todo/phase-5-validation/02-EXAMPLES-SIMPLE.md +744 -0
- data/.claude/todo/phase-5-validation/02-EXAMPLES.md +1857 -0
- data/.claude/todo/phase-5-validation/03-EXAMPLE-SUPPORT-AGENT.md +95 -0
- data/.claude/todo/phase-5-validation/04-EXAMPLE-ORDER-FULFILLMENT.md +94 -0
- data/.claude/todo/phase-5-validation/05-EXAMPLE-DATA-PIPELINE.md +145 -0
- data/.env.example +3 -0
- data/.rubocop.yml +64 -0
- data/0.3.amend.md +89 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +22 -0
- data/Gemfile.lock +192 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +16 -0
- data/durable_workflow.gemspec +43 -0
- data/examples/approval_request.rb +106 -0
- data/examples/calculator.rb +154 -0
- data/examples/file_search_demo.rb +77 -0
- data/examples/hello_workflow.rb +57 -0
- data/examples/item_processor.rb +96 -0
- data/examples/order_fulfillment/Gemfile +6 -0
- data/examples/order_fulfillment/README.md +84 -0
- data/examples/order_fulfillment/run.rb +85 -0
- data/examples/order_fulfillment/services.rb +146 -0
- data/examples/order_fulfillment/workflow.yml +188 -0
- data/examples/parallel_fetch.rb +102 -0
- data/examples/service_integration.rb +137 -0
- data/examples/support_agent/Gemfile +6 -0
- data/examples/support_agent/README.md +91 -0
- data/examples/support_agent/config/claude_desktop.json +12 -0
- data/examples/support_agent/mcp_server.rb +49 -0
- data/examples/support_agent/run.rb +67 -0
- data/examples/support_agent/services.rb +113 -0
- data/examples/support_agent/workflow.yml +286 -0
- data/lib/durable_workflow/core/condition.rb +45 -0
- data/lib/durable_workflow/core/engine.rb +145 -0
- data/lib/durable_workflow/core/executors/approval.rb +51 -0
- data/lib/durable_workflow/core/executors/assign.rb +18 -0
- data/lib/durable_workflow/core/executors/base.rb +90 -0
- data/lib/durable_workflow/core/executors/call.rb +76 -0
- data/lib/durable_workflow/core/executors/end.rb +19 -0
- data/lib/durable_workflow/core/executors/halt.rb +24 -0
- data/lib/durable_workflow/core/executors/loop.rb +118 -0
- data/lib/durable_workflow/core/executors/parallel.rb +77 -0
- data/lib/durable_workflow/core/executors/registry.rb +34 -0
- data/lib/durable_workflow/core/executors/router.rb +26 -0
- data/lib/durable_workflow/core/executors/start.rb +61 -0
- data/lib/durable_workflow/core/executors/transform.rb +71 -0
- data/lib/durable_workflow/core/executors/workflow.rb +32 -0
- data/lib/durable_workflow/core/parser.rb +189 -0
- data/lib/durable_workflow/core/resolver.rb +61 -0
- data/lib/durable_workflow/core/schema_validator.rb +47 -0
- data/lib/durable_workflow/core/types/base.rb +41 -0
- data/lib/durable_workflow/core/types/condition.rb +25 -0
- data/lib/durable_workflow/core/types/configs.rb +103 -0
- data/lib/durable_workflow/core/types/entry.rb +26 -0
- data/lib/durable_workflow/core/types/results.rb +41 -0
- data/lib/durable_workflow/core/types/state.rb +95 -0
- data/lib/durable_workflow/core/types/step_def.rb +15 -0
- data/lib/durable_workflow/core/types/workflow_def.rb +43 -0
- data/lib/durable_workflow/core/types.rb +29 -0
- data/lib/durable_workflow/core/validator.rb +318 -0
- data/lib/durable_workflow/extensions/ai/ai.rb +149 -0
- data/lib/durable_workflow/extensions/ai/configuration.rb +41 -0
- data/lib/durable_workflow/extensions/ai/executors/agent.rb +150 -0
- data/lib/durable_workflow/extensions/ai/executors/file_search.rb +52 -0
- data/lib/durable_workflow/extensions/ai/executors/guardrail.rb +152 -0
- data/lib/durable_workflow/extensions/ai/executors/handoff.rb +33 -0
- data/lib/durable_workflow/extensions/ai/executors/mcp.rb +47 -0
- data/lib/durable_workflow/extensions/ai/mcp/adapter.rb +73 -0
- data/lib/durable_workflow/extensions/ai/mcp/client.rb +77 -0
- data/lib/durable_workflow/extensions/ai/mcp/rack_app.rb +66 -0
- data/lib/durable_workflow/extensions/ai/mcp/server.rb +122 -0
- data/lib/durable_workflow/extensions/ai/tool_registry.rb +63 -0
- data/lib/durable_workflow/extensions/ai/types.rb +213 -0
- data/lib/durable_workflow/extensions/ai.rb +6 -0
- data/lib/durable_workflow/extensions/base.rb +77 -0
- data/lib/durable_workflow/runners/adapters/inline.rb +42 -0
- data/lib/durable_workflow/runners/adapters/sidekiq.rb +69 -0
- data/lib/durable_workflow/runners/async.rb +100 -0
- data/lib/durable_workflow/runners/stream.rb +126 -0
- data/lib/durable_workflow/runners/sync.rb +40 -0
- data/lib/durable_workflow/storage/active_record.rb +148 -0
- data/lib/durable_workflow/storage/redis.rb +133 -0
- data/lib/durable_workflow/storage/sequel.rb +144 -0
- data/lib/durable_workflow/storage/store.rb +43 -0
- data/lib/durable_workflow/utils.rb +25 -0
- data/lib/durable_workflow/version.rb +5 -0
- data/lib/durable_workflow.rb +70 -0
- data/sig/durable_workflow.rbs +4 -0
- metadata +275 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Service Integration - Calling external services from workflow
|
|
5
|
+
#
|
|
6
|
+
# Demonstrates: Call step, service resolution, error handling
|
|
7
|
+
#
|
|
8
|
+
# Run: ruby examples/service_integration.rb
|
|
9
|
+
# Requires: Redis running on localhost:6379
|
|
10
|
+
|
|
11
|
+
require "bundler/setup"
|
|
12
|
+
require "securerandom"
|
|
13
|
+
require "durable_workflow"
|
|
14
|
+
require "durable_workflow/storage/redis"
|
|
15
|
+
|
|
16
|
+
DurableWorkflow.configure do |c|
|
|
17
|
+
c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Inventory service (must be globally accessible constant)
|
|
21
|
+
module InventoryService
|
|
22
|
+
STOCK = {
|
|
23
|
+
"PROD-001" => 50,
|
|
24
|
+
"PROD-002" => 0,
|
|
25
|
+
"PROD-003" => 10
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def self.check_availability(product_id:, quantity:)
|
|
29
|
+
available = STOCK.fetch(product_id, 0)
|
|
30
|
+
{
|
|
31
|
+
product_id: product_id,
|
|
32
|
+
requested: quantity,
|
|
33
|
+
available: available,
|
|
34
|
+
in_stock: available >= quantity
|
|
35
|
+
}
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.reserve(product_id:, quantity:)
|
|
39
|
+
current = STOCK.fetch(product_id, 0)
|
|
40
|
+
raise "Insufficient stock" if current < quantity
|
|
41
|
+
|
|
42
|
+
STOCK[product_id] = current - quantity
|
|
43
|
+
{
|
|
44
|
+
reservation_id: "RES-#{SecureRandom.hex(4)}",
|
|
45
|
+
product_id: product_id,
|
|
46
|
+
quantity: quantity,
|
|
47
|
+
remaining: STOCK[product_id]
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
workflow = DurableWorkflow::Core::Parser.parse(<<~YAML)
|
|
53
|
+
id: inventory_check
|
|
54
|
+
name: Inventory Check and Reserve
|
|
55
|
+
version: "1.0"
|
|
56
|
+
|
|
57
|
+
inputs:
|
|
58
|
+
product_id:
|
|
59
|
+
type: string
|
|
60
|
+
required: true
|
|
61
|
+
quantity:
|
|
62
|
+
type: integer
|
|
63
|
+
required: true
|
|
64
|
+
|
|
65
|
+
steps:
|
|
66
|
+
- id: start
|
|
67
|
+
type: start
|
|
68
|
+
next: check
|
|
69
|
+
|
|
70
|
+
- id: check
|
|
71
|
+
type: call
|
|
72
|
+
service: InventoryService
|
|
73
|
+
method: check_availability
|
|
74
|
+
input:
|
|
75
|
+
product_id: "$input.product_id"
|
|
76
|
+
quantity: "$input.quantity"
|
|
77
|
+
output: availability
|
|
78
|
+
next: decide
|
|
79
|
+
|
|
80
|
+
- id: decide
|
|
81
|
+
type: router
|
|
82
|
+
routes:
|
|
83
|
+
- when:
|
|
84
|
+
field: availability.in_stock
|
|
85
|
+
op: eq
|
|
86
|
+
value: true
|
|
87
|
+
then: reserve
|
|
88
|
+
default: out_of_stock
|
|
89
|
+
|
|
90
|
+
- id: reserve
|
|
91
|
+
type: call
|
|
92
|
+
service: InventoryService
|
|
93
|
+
method: reserve
|
|
94
|
+
input:
|
|
95
|
+
product_id: "$input.product_id"
|
|
96
|
+
quantity: "$input.quantity"
|
|
97
|
+
output: reservation
|
|
98
|
+
next: success
|
|
99
|
+
|
|
100
|
+
- id: success
|
|
101
|
+
type: assign
|
|
102
|
+
set:
|
|
103
|
+
status: reserved
|
|
104
|
+
next: end
|
|
105
|
+
|
|
106
|
+
- id: out_of_stock
|
|
107
|
+
type: assign
|
|
108
|
+
set:
|
|
109
|
+
status: out_of_stock
|
|
110
|
+
error: "Insufficient stock available"
|
|
111
|
+
next: end
|
|
112
|
+
|
|
113
|
+
- id: end
|
|
114
|
+
type: end
|
|
115
|
+
result:
|
|
116
|
+
status: "$status"
|
|
117
|
+
availability: "$availability"
|
|
118
|
+
reservation: "$reservation"
|
|
119
|
+
error: "$error"
|
|
120
|
+
YAML
|
|
121
|
+
|
|
122
|
+
runner = DurableWorkflow::Runners::Sync.new(workflow)
|
|
123
|
+
|
|
124
|
+
# Available product
|
|
125
|
+
result = runner.run(input: { product_id: "PROD-001", quantity: 5 })
|
|
126
|
+
puts "PROD-001 (qty 5): #{result.output[:status]}"
|
|
127
|
+
puts " Reservation: #{result.output[:reservation][:reservation_id]}" if result.output[:reservation]
|
|
128
|
+
|
|
129
|
+
# Out of stock
|
|
130
|
+
result = runner.run(input: { product_id: "PROD-002", quantity: 1 })
|
|
131
|
+
puts "\nPROD-002 (qty 1): #{result.output[:status]}"
|
|
132
|
+
puts " Error: #{result.output[:error]}"
|
|
133
|
+
|
|
134
|
+
# Partial availability
|
|
135
|
+
result = runner.run(input: { product_id: "PROD-003", quantity: 20 })
|
|
136
|
+
puts "\nPROD-003 (qty 20): #{result.output[:status]}"
|
|
137
|
+
puts " Error: #{result.output[:error]}"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Support Agent
|
|
2
|
+
|
|
3
|
+
AI-powered customer support workflow with tool calling and MCP integration.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Multi-agent system (triage, billing, technical)
|
|
8
|
+
- Tool integration (lookup orders, create tickets, check status)
|
|
9
|
+
- Handoffs between specialized agents
|
|
10
|
+
- Content moderation guardrails
|
|
11
|
+
- MCP server for Claude Desktop integration
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd examples/support_agent
|
|
17
|
+
bundle install
|
|
18
|
+
|
|
19
|
+
# Set API key
|
|
20
|
+
export OPENAI_API_KEY=your-key
|
|
21
|
+
# or
|
|
22
|
+
export ANTHROPIC_API_KEY=your-key
|
|
23
|
+
|
|
24
|
+
# Start Redis
|
|
25
|
+
redis-server
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Interactive CLI
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
ruby run.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### As MCP Server (Claude Desktop)
|
|
37
|
+
|
|
38
|
+
1. Copy config to Claude Desktop:
|
|
39
|
+
```bash
|
|
40
|
+
cp config/claude_desktop.json ~/.config/claude/claude_desktop_config.json
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
2. Restart Claude Desktop
|
|
44
|
+
|
|
45
|
+
3. The support tools will be available in Claude
|
|
46
|
+
|
|
47
|
+
## Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
User Input
|
|
51
|
+
↓
|
|
52
|
+
┌─────────────┐
|
|
53
|
+
│ Triage │ ← Classifies request
|
|
54
|
+
│ Agent │
|
|
55
|
+
└─────────────┘
|
|
56
|
+
↓
|
|
57
|
+
┌─────────────────────────────────┐
|
|
58
|
+
│ Router │
|
|
59
|
+
├─────────────┬─────────┬─────────┤
|
|
60
|
+
│ Billing │ Tech │ Other │
|
|
61
|
+
│ Agent │ Agent │ │
|
|
62
|
+
└─────────────┴─────────┴─────────┘
|
|
63
|
+
↓
|
|
64
|
+
Tools: lookup_order, create_ticket, check_status, escalate
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Tools Available
|
|
68
|
+
|
|
69
|
+
| Tool | Description |
|
|
70
|
+
|------|-------------|
|
|
71
|
+
| `lookup_order` | Find order by ID or customer email |
|
|
72
|
+
| `create_ticket` | Create support ticket |
|
|
73
|
+
| `check_status` | Check ticket status |
|
|
74
|
+
| `escalate` | Escalate to human agent |
|
|
75
|
+
| `refund_order` | Process refund (billing only) |
|
|
76
|
+
| `reset_password` | Reset user password (tech only) |
|
|
77
|
+
|
|
78
|
+
## MCP Integration
|
|
79
|
+
|
|
80
|
+
When running as MCP server, external AI agents can:
|
|
81
|
+
|
|
82
|
+
1. Discover available tools via `tools/list`
|
|
83
|
+
2. Call tools via `tools/call`
|
|
84
|
+
3. Run the full workflow as a tool
|
|
85
|
+
|
|
86
|
+
Example Claude Desktop interaction:
|
|
87
|
+
```
|
|
88
|
+
User: "I need help with order ORD-12345"
|
|
89
|
+
Claude: [calls lookup_order with order_id: "ORD-12345"]
|
|
90
|
+
Claude: "I found your order. It was placed on..."
|
|
91
|
+
```
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# MCP Server for Claude Desktop integration
|
|
5
|
+
#
|
|
6
|
+
# Add to ~/.config/claude/claude_desktop_config.json:
|
|
7
|
+
# {
|
|
8
|
+
# "mcpServers": {
|
|
9
|
+
# "support_agent": {
|
|
10
|
+
# "command": "ruby",
|
|
11
|
+
# "args": ["examples/support_agent/mcp_server.rb"],
|
|
12
|
+
# "cwd": "/path/to/durable_workflow"
|
|
13
|
+
# }
|
|
14
|
+
# }
|
|
15
|
+
# }
|
|
16
|
+
|
|
17
|
+
require "bundler/setup"
|
|
18
|
+
require "dotenv/load" if File.exist?(File.expand_path("../../.env", __dir__))
|
|
19
|
+
require "durable_workflow"
|
|
20
|
+
require "durable_workflow/extensions/ai"
|
|
21
|
+
require "durable_workflow/storage/redis"
|
|
22
|
+
require_relative "services"
|
|
23
|
+
|
|
24
|
+
# Suppress stdout logging (corrupts MCP protocol)
|
|
25
|
+
$stderr = File.open("/tmp/support_agent_mcp.log", "a")
|
|
26
|
+
|
|
27
|
+
# Configure
|
|
28
|
+
DurableWorkflow.configure do |c|
|
|
29
|
+
c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
|
|
30
|
+
c.logger = Logger.new("/dev/null")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
DurableWorkflow::Extensions::AI.configure do |c|
|
|
34
|
+
c.api_keys[:openai] = ENV["OPENAI_API_KEY"]
|
|
35
|
+
c.api_keys[:anthropic] = ENV["ANTHROPIC_API_KEY"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Services auto-resolved via Object.const_get
|
|
39
|
+
|
|
40
|
+
# Load and register workflow
|
|
41
|
+
workflow = DurableWorkflow.load(File.join(__dir__, "workflow.yml"))
|
|
42
|
+
DurableWorkflow.register(workflow)
|
|
43
|
+
|
|
44
|
+
# Run MCP server with workflow tools + workflow itself exposed
|
|
45
|
+
DurableWorkflow::Extensions::AI::MCP::Server.stdio(
|
|
46
|
+
workflow,
|
|
47
|
+
name: "support_agent",
|
|
48
|
+
expose_workflow: true # Makes "run_support_agent" available as a tool
|
|
49
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "bundler/setup"
|
|
5
|
+
require "dotenv/load" if File.exist?(File.expand_path("../../.env", __dir__))
|
|
6
|
+
require "durable_workflow"
|
|
7
|
+
require "durable_workflow/extensions/ai"
|
|
8
|
+
require "durable_workflow/storage/redis"
|
|
9
|
+
require_relative "services"
|
|
10
|
+
|
|
11
|
+
# Configure
|
|
12
|
+
DurableWorkflow.configure do |c|
|
|
13
|
+
c.store = DurableWorkflow::Storage::Redis.new(url: "redis://localhost:6379")
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
DurableWorkflow::Extensions::AI.configure do |c|
|
|
17
|
+
c.api_keys[:openai] = ENV["OPENAI_API_KEY"]
|
|
18
|
+
c.api_keys[:anthropic] = ENV["ANTHROPIC_API_KEY"]
|
|
19
|
+
c.default_model = "gpt-4o-mini"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Services are auto-resolved via Object.const_get (SupportServices is a global module)
|
|
23
|
+
|
|
24
|
+
# Load and register workflow
|
|
25
|
+
workflow = DurableWorkflow.load(File.join(__dir__, "workflow.yml"))
|
|
26
|
+
DurableWorkflow.register(workflow)
|
|
27
|
+
runner = DurableWorkflow::Runners::Stream.new(workflow)
|
|
28
|
+
|
|
29
|
+
# Subscribe to events for visibility
|
|
30
|
+
runner.subscribe do |event|
|
|
31
|
+
case event.type
|
|
32
|
+
when "step.started"
|
|
33
|
+
puts " [#{event.data[:step_id]}]" if ENV["DEBUG"]
|
|
34
|
+
when "agent.tool_use"
|
|
35
|
+
puts " -> Tool: #{event.data[:tool]} #{event.data[:arguments]}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts "=" * 60
|
|
40
|
+
puts "Customer Support Agent"
|
|
41
|
+
puts "=" * 60
|
|
42
|
+
puts "Type 'quit' to exit"
|
|
43
|
+
puts
|
|
44
|
+
|
|
45
|
+
loop do
|
|
46
|
+
print "\nYou: "
|
|
47
|
+
input = gets&.chomp
|
|
48
|
+
break if input.nil? || input.downcase == "quit"
|
|
49
|
+
next if input.empty?
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
result = runner.run(input: {
|
|
53
|
+
message: input,
|
|
54
|
+
customer_id: "CUST-001"
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
puts "\nAgent: #{result.output[:response]}"
|
|
58
|
+
if result.output[:triage].is_a?(Hash)
|
|
59
|
+
puts " [Category: #{result.output[:triage][:category]}, Urgency: #{result.output[:triage][:urgency]}]"
|
|
60
|
+
end
|
|
61
|
+
rescue => e
|
|
62
|
+
puts "\nError: #{e.message}"
|
|
63
|
+
puts e.backtrace.first(3).join("\n") if ENV["DEBUG"]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
puts "\nGoodbye!"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
# Mock services for support agent demo
|
|
6
|
+
module SupportServices
|
|
7
|
+
ORDERS = {
|
|
8
|
+
"ORD-12345" => {
|
|
9
|
+
id: "ORD-12345",
|
|
10
|
+
customer_email: "alice@example.com",
|
|
11
|
+
status: "shipped",
|
|
12
|
+
total: 149.99,
|
|
13
|
+
items: [
|
|
14
|
+
{ name: "Wireless Headphones", quantity: 1, price: 149.99 }
|
|
15
|
+
],
|
|
16
|
+
created_at: "2024-01-10",
|
|
17
|
+
tracking: "1Z999AA10123456784"
|
|
18
|
+
},
|
|
19
|
+
"ORD-67890" => {
|
|
20
|
+
id: "ORD-67890",
|
|
21
|
+
customer_email: "bob@example.com",
|
|
22
|
+
status: "delivered",
|
|
23
|
+
total: 299.99,
|
|
24
|
+
items: [
|
|
25
|
+
{ name: "Smart Watch", quantity: 1, price: 299.99 }
|
|
26
|
+
],
|
|
27
|
+
created_at: "2024-01-05",
|
|
28
|
+
delivered_at: "2024-01-08"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
TICKETS = {}
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
def classify_request(category:, urgency:, summary:)
|
|
36
|
+
{
|
|
37
|
+
category: category,
|
|
38
|
+
urgency: urgency,
|
|
39
|
+
summary: summary,
|
|
40
|
+
classified_at: Time.now.iso8601
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def lookup_order(order_id: nil, email: nil)
|
|
45
|
+
if order_id
|
|
46
|
+
order = ORDERS[order_id]
|
|
47
|
+
return { error: "Order not found: #{order_id}" } unless order
|
|
48
|
+
order
|
|
49
|
+
elsif email
|
|
50
|
+
orders = ORDERS.values.select { |o| o[:customer_email] == email }
|
|
51
|
+
return { error: "No orders found for #{email}" } if orders.empty?
|
|
52
|
+
{ orders: orders, count: orders.size }
|
|
53
|
+
else
|
|
54
|
+
{ error: "Please provide order_id or email" }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def refund_order(order_id:, reason:, amount: nil)
|
|
59
|
+
order = ORDERS[order_id]
|
|
60
|
+
return { error: "Order not found: #{order_id}" } unless order
|
|
61
|
+
|
|
62
|
+
refund_amount = amount || order[:total]
|
|
63
|
+
{
|
|
64
|
+
refund_id: "REF-#{SecureRandom.hex(4).upcase}",
|
|
65
|
+
order_id: order_id,
|
|
66
|
+
amount: refund_amount,
|
|
67
|
+
reason: reason,
|
|
68
|
+
status: "processed",
|
|
69
|
+
processed_at: Time.now.iso8601
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def create_ticket(subject:, description:, priority: "medium")
|
|
74
|
+
ticket_id = "TKT-#{SecureRandom.hex(4).upcase}"
|
|
75
|
+
ticket = {
|
|
76
|
+
id: ticket_id,
|
|
77
|
+
subject: subject,
|
|
78
|
+
description: description,
|
|
79
|
+
priority: priority,
|
|
80
|
+
status: "open",
|
|
81
|
+
created_at: Time.now.iso8601
|
|
82
|
+
}
|
|
83
|
+
TICKETS[ticket_id] = ticket
|
|
84
|
+
ticket
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def check_status(ticket_id:)
|
|
88
|
+
ticket = TICKETS[ticket_id]
|
|
89
|
+
return { error: "Ticket not found: #{ticket_id}" } unless ticket
|
|
90
|
+
ticket
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def reset_password(email:)
|
|
94
|
+
{
|
|
95
|
+
email: email,
|
|
96
|
+
reset_sent: true,
|
|
97
|
+
message: "Password reset email sent to #{email}",
|
|
98
|
+
expires_in: "24 hours"
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def escalate(reason:, urgency:)
|
|
103
|
+
{
|
|
104
|
+
escalation_id: "ESC-#{SecureRandom.hex(4).upcase}",
|
|
105
|
+
reason: reason,
|
|
106
|
+
urgency: urgency,
|
|
107
|
+
status: "pending_human_review",
|
|
108
|
+
estimated_response: urgency == "high" ? "1 hour" : "4 hours",
|
|
109
|
+
created_at: Time.now.iso8601
|
|
110
|
+
}
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|