conductor_ruby 0.1.0 → 0.1.1

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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/examples/agents/01_basic_agent.rb +36 -0
  4. data/examples/agents/02a_simple_tools.rb +47 -0
  5. data/examples/agents/02c_tool_retry_config.rb +52 -0
  6. data/examples/agents/04_http_and_mcp_tools.rb +58 -0
  7. data/examples/agents/05_handoffs.rb +71 -0
  8. data/examples/agents/06_sequential_pipeline.rb +47 -0
  9. data/examples/agents/07_parallel_agents.rb +52 -0
  10. data/examples/agents/09_human_in_the_loop.rb +59 -0
  11. data/examples/agents/09c_hitl_streaming.rb +65 -0
  12. data/examples/agents/103_plan_and_compile.rb +85 -0
  13. data/examples/agents/10_guardrails.rb +61 -0
  14. data/examples/agents/13_hierarchical_agents.rb +79 -0
  15. data/examples/agents/16e_credentials_http_tool.rb +44 -0
  16. data/examples/agents/17_swarm_orchestration.rb +56 -0
  17. data/examples/agents/21_regex_guardrails.rb +69 -0
  18. data/examples/agents/22_llm_guardrails.rb +51 -0
  19. data/examples/agents/33_external_workers.rb +65 -0
  20. data/examples/agents/64_swarm_with_tools.rb +71 -0
  21. data/examples/agents/66_handoff_to_parallel.rb +62 -0
  22. data/examples/agents/bug_desk.rb +50 -0
  23. data/examples/agents/catalog.rb +32 -0
  24. data/examples/agents/dump_agent_configs.rb +30 -0
  25. data/examples/agents/external_workers.rb +39 -0
  26. data/examples/agents/golden_agents.rb +385 -0
  27. data/examples/agents/support_approval.rb +51 -0
  28. data/examples/agents/weather.rb +27 -0
  29. data/lib/conductor/agents/agent.rb +324 -0
  30. data/lib/conductor/agents/callback_handler.rb +64 -0
  31. data/lib/conductor/agents/config_serializer.rb +246 -0
  32. data/lib/conductor/agents/errors.rb +26 -0
  33. data/lib/conductor/agents/guardrail.rb +142 -0
  34. data/lib/conductor/agents/handoff.rb +94 -0
  35. data/lib/conductor/agents/memory.rb +75 -0
  36. data/lib/conductor/agents/plans.rb +22 -0
  37. data/lib/conductor/agents/prompt_template.rb +23 -0
  38. data/lib/conductor/agents/runtime/agent_config.rb +52 -0
  39. data/lib/conductor/agents/runtime/agent_runtime.rb +260 -0
  40. data/lib/conductor/agents/runtime/approval_request.rb +111 -0
  41. data/lib/conductor/agents/runtime/dispatch.rb +137 -0
  42. data/lib/conductor/agents/runtime/execution.rb +265 -0
  43. data/lib/conductor/agents/runtime/secrets.rb +54 -0
  44. data/lib/conductor/agents/runtime/sse_client.rb +175 -0
  45. data/lib/conductor/agents/runtime/status_poller.rb +49 -0
  46. data/lib/conductor/agents/runtime/system_workers.rb +115 -0
  47. data/lib/conductor/agents/runtime/tool_call.rb +13 -0
  48. data/lib/conductor/agents/runtime/tool_registry.rb +164 -0
  49. data/lib/conductor/agents/termination.rb +192 -0
  50. data/lib/conductor/agents/tool.rb +283 -0
  51. data/lib/conductor/agents/tools/schema_builder.rb +190 -0
  52. data/lib/conductor/agents/tools/secret_scanner.rb +45 -0
  53. data/lib/conductor/agents/tools.rb +173 -0
  54. data/lib/conductor/agents.rb +75 -0
  55. data/lib/conductor/client/agent_client.rb +117 -0
  56. data/lib/conductor/client/task_client.rb +7 -0
  57. data/lib/conductor/configuration.rb +43 -10
  58. data/lib/conductor/exceptions.rb +36 -0
  59. data/lib/conductor/http/api/agent_resource_api.rb +138 -0
  60. data/lib/conductor/http/api/scheduler_resource_api.rb +31 -12
  61. data/lib/conductor/http/api/task_resource_api.rb +15 -0
  62. data/lib/conductor/http/models/task.rb +10 -2
  63. data/lib/conductor/http/models/task_def.rb +14 -3
  64. data/lib/conductor/http/rest_client.rb +9 -0
  65. data/lib/conductor/orkes/orkes_clients.rb +5 -1
  66. data/lib/conductor/version.rb +1 -1
  67. data/lib/conductor/worker/lease_renewer.rb +55 -0
  68. data/lib/conductor/worker/ractor_task_runner.rb +1 -1
  69. data/lib/conductor/worker/task_definition_registrar.rb +2 -2
  70. data/lib/conductor/worker/task_runner.rb +43 -10
  71. data/lib/conductor/worker/worker.rb +3 -2
  72. data/lib/conductor/worker/worker_config.rb +2 -1
  73. data/lib/conductor.rb +3 -1
  74. metadata +71 -16
@@ -0,0 +1,385 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Agents whose serialized agentConfig must match the Python SDK byte for byte
4
+ # (spec/fixtures/agents/configs/*.json, vendored from python-sdk examples/agents/_configs).
5
+ #
6
+ # Used by spec/conductor/agents/contract_spec.rb and by dump_agent_configs.rb.
7
+ # Each example keeps its tools in its own module so that tools with the same name but
8
+ # different descriptions (get_weather in 02 vs 03) do not collide.
9
+ require 'conductor/agents'
10
+ require_relative '103_plan_and_compile'
11
+
12
+ module GoldenAgents
13
+ MODEL = ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'anthropic/claude-sonnet-4-6')
14
+ A = Conductor::Agents
15
+
16
+ module Ex02
17
+ extend A::Tools
18
+ tool def get_weather(city: String)
19
+ {}
20
+ end
21
+ describe :get_weather, 'Get current weather for a city.'
22
+ tool def calculate(expression: String)
23
+ {}
24
+ end
25
+ describe :calculate, 'Evaluate a math expression.'
26
+ tool def send_email(to: String, subject: String, body: String)
27
+ {}
28
+ end
29
+ describe :send_email, 'Send an email.'
30
+ requires_approval :send_email
31
+ self[:send_email].timeout_seconds = 60
32
+ end
33
+
34
+ module Ex03
35
+ extend A::Tools
36
+ tool def get_weather(city: String)
37
+ {}
38
+ end
39
+ describe :get_weather, 'Get current weather data for a city.'
40
+ end
41
+
42
+ module Ex05
43
+ extend A::Tools
44
+ tool def check_balance(account_id: String)
45
+ {}
46
+ end
47
+ describe :check_balance, 'Check the balance of a bank account.'
48
+ tool def lookup_order(order_id: String)
49
+ {}
50
+ end
51
+ describe :lookup_order, 'Look up the status of an order.'
52
+ tool def get_pricing(product: String)
53
+ {}
54
+ end
55
+ describe :get_pricing, 'Get pricing information for a product.'
56
+ end
57
+
58
+ module Ex10
59
+ extend A::Tools
60
+ tool def get_order_status(order_id: String)
61
+ {}
62
+ end
63
+ describe :get_order_status, 'Look up the current status of an order.'
64
+ tool def get_customer_info(customer_id: String)
65
+ {}
66
+ end
67
+ describe :get_customer_info, 'Retrieve customer details including payment info on file.'
68
+ end
69
+
70
+ module Ex19
71
+ extend A::Tools
72
+ tool def search(query: String)
73
+ ''
74
+ end
75
+ describe :search, 'Search for information.'
76
+ self[:search].output_schema = { 'type' => 'string' }
77
+ end
78
+
79
+ module Ex21
80
+ extend A::Tools
81
+ tool def get_user_profile(user_id: String)
82
+ {}
83
+ end
84
+ describe :get_user_profile, "Retrieve a user's profile from the database."
85
+ end
86
+
87
+ module Ex45
88
+ extend A::Tools
89
+ tool def search_knowledge_base(query: String)
90
+ {}
91
+ end
92
+ describe :search_knowledge_base, 'Search an internal knowledge base for information.'
93
+ tool def calculate(expression: String)
94
+ {}
95
+ end
96
+ describe :calculate, 'Evaluate a math expression safely.'
97
+ end
98
+
99
+ module Ex47
100
+ extend A::Tools
101
+ tool def get_facts(topic: String)
102
+ {}
103
+ end
104
+ describe :get_facts, 'Get interesting facts about a topic.'
105
+ end
106
+
107
+ # 47_callbacks: before_model / after_model hooks
108
+ class MonitorHandler < A::CallbackHandler
109
+ def on_model_start(**_kwargs)
110
+ {}
111
+ end
112
+
113
+ def on_model_end(**_kwargs)
114
+ {}
115
+ end
116
+ end
117
+
118
+ EXAMPLES = {
119
+ '01_basic_agent' => lambda {
120
+ A::Agent.new(name: 'greeter', model: MODEL)
121
+ },
122
+
123
+ '02_tools' => lambda {
124
+ A::Agent.new(
125
+ name: 'tool_demo_agent', model: MODEL,
126
+ tools: [Ex02[:get_weather], Ex02[:calculate], Ex02[:send_email]],
127
+ instructions: 'You are a helpful assistant with access to weather, calculator, and email tools.'
128
+ )
129
+ },
130
+
131
+ '03_structured_output' => lambda {
132
+ weather_report = {
133
+ 'title' => 'WeatherReport',
134
+ 'type' => 'object',
135
+ 'properties' => {
136
+ 'city' => { 'title' => 'City', 'type' => 'string' },
137
+ 'temperature' => { 'title' => 'Temperature', 'type' => 'number' },
138
+ 'condition' => { 'title' => 'Condition', 'type' => 'string' },
139
+ 'recommendation' => { 'title' => 'Recommendation', 'type' => 'string' }
140
+ },
141
+ 'required' => %w[city temperature condition recommendation]
142
+ }
143
+ A::Agent.new(
144
+ name: 'weather_reporter', model: MODEL, tools: [Ex03[:get_weather]], output_type: weather_report,
145
+ instructions: 'You are a weather reporter. Get the weather and provide a recommendation.'
146
+ )
147
+ },
148
+
149
+ '05_handoffs' => lambda {
150
+ billing = A::Agent.new(name: 'billing', model: MODEL, tools: [Ex05[:check_balance]],
151
+ instructions: 'You handle billing questions: balances, payments, invoices.')
152
+ technical = A::Agent.new(name: 'technical', model: MODEL, tools: [Ex05[:lookup_order]],
153
+ instructions: 'You handle technical questions: order status, shipping, returns.')
154
+ sales = A::Agent.new(name: 'sales', model: MODEL, tools: [Ex05[:get_pricing]],
155
+ instructions: 'You handle sales questions: pricing, products, promotions.')
156
+ A::Agent.new(name: 'support', model: MODEL, agents: [billing, technical, sales], strategy: :handoff,
157
+ instructions: 'Route customer requests to the right specialist: billing, technical, or sales.')
158
+ },
159
+
160
+ '06_sequential_pipeline' => lambda {
161
+ researcher = A::Agent.new(
162
+ name: 'researcher', model: MODEL,
163
+ instructions: 'You are a researcher. Given a topic, provide key facts and data points. ' \
164
+ 'Be thorough but concise. Output raw research findings.'
165
+ )
166
+ writer = A::Agent.new(
167
+ name: 'writer', model: MODEL,
168
+ instructions: 'You are a writer. Take research findings and write a clear, engaging ' \
169
+ 'article. Use headers and bullet points where appropriate.'
170
+ )
171
+ editor = A::Agent.new(
172
+ name: 'editor', model: MODEL,
173
+ instructions: 'You are an editor. Review the article for clarity, grammar, and tone. ' \
174
+ 'Make improvements and output the final polished version.'
175
+ )
176
+ researcher >> writer >> editor
177
+ },
178
+
179
+ '07_parallel_agents' => lambda {
180
+ market = A::Agent.new(
181
+ name: 'market_analyst', model: MODEL,
182
+ instructions: 'You are a market analyst. Analyze the given topic from a market perspective: ' \
183
+ 'market size, growth trends, key players, and opportunities.'
184
+ )
185
+ risk = A::Agent.new(
186
+ name: 'risk_analyst', model: MODEL,
187
+ instructions: 'You are a risk analyst. Analyze the given topic for risks: ' \
188
+ 'regulatory risks, technical risks, competitive threats, and mitigation strategies.'
189
+ )
190
+ compliance = A::Agent.new(
191
+ name: 'compliance', model: MODEL,
192
+ instructions: 'You are a compliance specialist. Check the given topic for compliance considerations: ' \
193
+ 'data privacy, regulatory requirements, and industry standards.'
194
+ )
195
+ A::Agent.new(name: 'analysis', model: MODEL, agents: [market, risk, compliance], strategy: :parallel)
196
+ },
197
+
198
+ '08_router_agent' => lambda {
199
+ planner = A::Agent.new(name: 'planner', model: MODEL,
200
+ instructions: 'You create implementation plans. Break down tasks into clear numbered steps.')
201
+ coder = A::Agent.new(name: 'coder', model: MODEL,
202
+ instructions: 'You write code. Output clean, well-documented Python code.')
203
+ reviewer = A::Agent.new(name: 'reviewer', model: MODEL,
204
+ instructions: 'You review code. Check for bugs, style issues, and suggest improvements.')
205
+ A::Agent.new(
206
+ name: 'dev_team', model: MODEL, agents: [planner, coder, reviewer], strategy: :router, router: planner,
207
+ instructions: 'You are the tech lead. Route requests to the right team member: ' \
208
+ 'planner for design/architecture, coder for implementation, reviewer for code review.'
209
+ )
210
+ },
211
+
212
+ '10_guardrails' => lambda {
213
+ no_pii = A::Guardrail.new(name: 'no_pii', position: :output, on_fail: :retry) do |content|
214
+ if content =~ /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/ || content =~ /\b\d{3}-\d{2}-\d{4}\b/
215
+ A::GuardrailResult.new(passed: false, message: 'Your response contains PII. Redact it.')
216
+ else
217
+ A::GuardrailResult.new(passed: true)
218
+ end
219
+ end
220
+ A::Agent.new(
221
+ name: 'support_agent', model: MODEL, tools: [Ex10[:get_order_status], Ex10[:get_customer_info]],
222
+ guardrails: [no_pii],
223
+ instructions: 'You are a customer support assistant. Use the available tools to ' \
224
+ 'answer questions about orders and customers. Always include all ' \
225
+ 'details from the tool results in your response.'
226
+ )
227
+ },
228
+
229
+ '13_hierarchical_agents' => lambda {
230
+ backend = A::Agent.new(
231
+ name: 'backend_dev', model: MODEL,
232
+ instructions: 'You are a backend developer. You design APIs, databases, and server ' \
233
+ 'architecture. Provide technical recommendations with code examples.'
234
+ )
235
+ frontend = A::Agent.new(
236
+ name: 'frontend_dev', model: MODEL,
237
+ instructions: 'You are a frontend developer. You design UI components, user flows, ' \
238
+ 'and client-side architecture. Provide recommendations with code examples.'
239
+ )
240
+ content = A::Agent.new(
241
+ name: 'content_writer', model: MODEL,
242
+ instructions: 'You are a content writer. You create blog posts, landing page copy, ' \
243
+ 'and marketing materials. Write engaging, clear content.'
244
+ )
245
+ seo = A::Agent.new(
246
+ name: 'seo_specialist', model: MODEL,
247
+ instructions: 'You are an SEO specialist. You optimize content for search engines, ' \
248
+ 'suggest keywords, and improve page rankings.'
249
+ )
250
+ engineering = A::Agent.new(
251
+ name: 'engineering_lead', model: MODEL, agents: [backend, frontend], strategy: :handoff,
252
+ instructions: 'You are the engineering lead. Route technical questions to the right ' \
253
+ 'specialist: backend_dev for APIs/databases/servers, frontend_dev for UI/UX/client-side.'
254
+ )
255
+ marketing = A::Agent.new(
256
+ name: 'marketing_lead', model: MODEL, agents: [content, seo], strategy: :handoff,
257
+ instructions: 'You are the marketing lead. Route marketing questions to the right ' \
258
+ 'specialist: content_writer for blog posts/copy, seo_specialist for SEO/keywords/rankings.'
259
+ )
260
+ A::Agent.new(
261
+ name: 'ceo', model: MODEL, agents: [engineering, marketing], strategy: :swarm,
262
+ handoffs: [
263
+ A::Handoff::OnTextMention.new(target: 'engineering_lead', text: 'engineering_lead'),
264
+ A::Handoff::OnTextMention.new(target: 'marketing_lead', text: 'marketing_lead')
265
+ ],
266
+ instructions: 'You are the CEO. Route requests to the right department: ' \
267
+ 'engineering_lead for technical/development questions, ' \
268
+ 'marketing_lead for marketing/content/SEO questions.'
269
+ )
270
+ },
271
+
272
+ '17_swarm_orchestration' => lambda {
273
+ refund = A::Agent.new(
274
+ name: 'refund_specialist', model: MODEL,
275
+ instructions: "You are a refund specialist. Process the customer's refund request. " \
276
+ 'Check eligibility, confirm the refund amount, and let them know the ' \
277
+ 'timeline. Be empathetic and clear. Do NOT ask follow-up questions -- ' \
278
+ 'just process the refund based on what the customer told you.'
279
+ )
280
+ tech = A::Agent.new(
281
+ name: 'tech_support', model: MODEL,
282
+ instructions: "You are a technical support specialist. Diagnose the customer's " \
283
+ 'technical issue and provide clear troubleshooting steps.'
284
+ )
285
+ A::Agent.new(
286
+ name: 'support', model: MODEL, agents: [refund, tech], strategy: :swarm, max_turns: 3,
287
+ handoffs: [
288
+ A::Handoff::OnTextMention.new(target: 'refund_specialist', text: 'refund'),
289
+ A::Handoff::OnTextMention.new(target: 'tech_support', text: 'technical')
290
+ ],
291
+ instructions: 'You are the front-line customer support agent. Triage customer requests. ' \
292
+ 'If the customer needs a refund, transfer to the refund specialist. ' \
293
+ 'If they have a technical issue, transfer to tech support. ' \
294
+ 'Use the transfer tools available to you to hand off the conversation.'
295
+ )
296
+ },
297
+
298
+ '19_composable_termination_simple' => lambda {
299
+ A::Agent.new(name: 'researcher', model: MODEL, tools: [Ex19[:search]],
300
+ instructions: 'Research the topic and say DONE when you have enough info.',
301
+ termination: A::Termination::TextMention.new('DONE'))
302
+ },
303
+
304
+ '19_composable_termination_or' => lambda {
305
+ A::Agent.new(name: 'chatbot', model: MODEL,
306
+ instructions: "Have a conversation. Say GOODBYE when you're finished.",
307
+ termination: A::Termination::TextMention.new('GOODBYE') | A::Termination::MaxMessage.new(20))
308
+ },
309
+
310
+ '19_composable_termination_and' => lambda {
311
+ A::Agent.new(name: 'deliberator', model: MODEL, tools: [Ex19[:search]],
312
+ instructions: 'Research thoroughly. Only provide your FINAL ANSWER after ' \
313
+ 'using the search tool at least twice.',
314
+ termination: A::Termination::TextMention.new('FINAL ANSWER') & A::Termination::MaxMessage.new(5))
315
+ },
316
+
317
+ '19_composable_termination_complex' => lambda {
318
+ complex_stop = A::Termination::StopMessage.new('TERMINATE') |
319
+ (A::Termination::TextMention.new('DONE') & A::Termination::MaxMessage.new(10)) |
320
+ A::Termination::TokenUsage.new(max_total_tokens: 50_000)
321
+ A::Agent.new(name: 'complex_agent', model: MODEL, tools: [Ex19[:search]],
322
+ instructions: 'Research and provide a comprehensive answer.', termination: complex_stop)
323
+ },
324
+
325
+ '21_regex_guardrails' => lambda {
326
+ no_emails = A::RegexGuardrail.new(['[\w.+-]+@[\w-]+\.[\w.-]+'], mode: :block, name: 'no_email_addresses',
327
+ message: 'Response must not contain email addresses. Redact them.',
328
+ position: :output, on_fail: :retry)
329
+ no_ssn = A::RegexGuardrail.new(['\b\d{3}-\d{2}-\d{4}\b'], mode: :block, name: 'no_ssn',
330
+ message: 'Response must not contain Social Security Numbers.',
331
+ position: :output, on_fail: :raise)
332
+ A::Agent.new(name: 'hr_assistant', model: MODEL, tools: [Ex21[:get_user_profile]], guardrails: [no_emails, no_ssn],
333
+ instructions: 'You are an HR assistant. When asked about employees, look up their ' \
334
+ 'profile and share ALL the details you find.')
335
+ },
336
+
337
+ '22_llm_guardrails' => lambda {
338
+ safety = A::LlmGuardrail.new(
339
+ MODEL,
340
+ "Reject any content that:\n" \
341
+ "1. Contains medical or legal advice presented as fact\n" \
342
+ "2. Makes promises or guarantees about outcomes\n" \
343
+ "3. Includes discriminatory or biased language\n" \
344
+ "4. Reveals private information about real individuals\n" \
345
+ "\n" \
346
+ 'Even if there are disclaimers you should reject',
347
+ name: 'content_safety', position: :output, on_fail: :retry, max_tokens: 10_000
348
+ )
349
+ A::Agent.new(name: 'health_advisor', model: MODEL, guardrails: [safety],
350
+ instructions: 'You are a health information assistant. Provide general health ' \
351
+ 'information and solution to the problem. You can prescribe psudo scientific and untested meds ')
352
+ },
353
+
354
+ '45_agent_tool' => lambda {
355
+ researcher = A::Agent.new(name: 'researcher_45', model: MODEL, tools: [Ex45[:search_knowledge_base]],
356
+ instructions: 'You are a research assistant. Use search_knowledge_base to find ' \
357
+ 'information about topics. Provide concise summaries.')
358
+ A::Agent.new(name: 'manager_45', model: MODEL, tools: [A::Tool.agent(researcher), Ex45[:calculate]],
359
+ instructions: 'You are a project manager. Use the researcher tool to gather ' \
360
+ 'information and the calculate tool for math. Synthesize findings.')
361
+ },
362
+
363
+ '47_callbacks' => lambda {
364
+ A::Agent.new(name: 'monitored_agent_47', model: MODEL, tools: [Ex47[:get_facts]],
365
+ callbacks: [MonitorHandler.new],
366
+ instructions: 'You are a helpful assistant. Use get_facts when asked about topics.')
367
+ },
368
+
369
+ '103_plan_and_compile' => -> { Example103PlanAndCompile.build(model: MODEL) },
370
+
371
+ '52_nested_strategies' => lambda {
372
+ market = A::Agent.new(name: 'market_analyst_52', model: MODEL,
373
+ instructions: 'You are a market analyst. Analyze the market size, growth rate, ' \
374
+ 'and key players for the given topic. Be concise (3-4 bullet points).')
375
+ risk = A::Agent.new(name: 'risk_analyst_52', model: MODEL,
376
+ instructions: 'You are a risk analyst. Identify the top 3 risks: regulatory, ' \
377
+ 'technical, and competitive. Be concise.')
378
+ research = A::Agent.new(name: 'research_phase_52', model: MODEL, agents: [market, risk], strategy: :parallel)
379
+ summarizer = A::Agent.new(name: 'summarizer_52', model: MODEL,
380
+ instructions: 'You are an executive briefing writer. Synthesize the market analysis ' \
381
+ 'and risk assessment into a concise executive summary (1 paragraph).')
382
+ research >> summarizer
383
+ }
384
+ }.freeze
385
+ end
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Streaming + approval: a tool that waits for a human decision before it runs.
5
+ #
6
+ # export CONDUCTOR_SERVER_URL=http://localhost:8080/api
7
+ # bundle exec ruby examples/agents/support_approval.rb
8
+ require_relative '../../lib/conductor/agents'
9
+ include Conductor::Agents
10
+
11
+ module Billing
12
+ def self.refund(order_id, amount)
13
+ "Refunded #{amount} for order #{order_id}"
14
+ end
15
+ end
16
+
17
+ tool def issue_refund(order_id: String, amount: Float)
18
+ { message: Billing.refund(order_id, amount) }
19
+ end
20
+ requires_approval :issue_refund
21
+
22
+ agent = Agent.new(
23
+ name: 'support',
24
+ model: ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'anthropic/claude-sonnet-4-5'),
25
+ instructions: 'Help with orders.'
26
+ )
27
+ agent.add_tool :issue_refund
28
+
29
+ # Decide approval-required tool calls. `request` exposes the tool's arguments as methods.
30
+ agent.on_approval do |request|
31
+ puts "approval requested: #{request.tool_name} #{request.arguments}"
32
+ request.amount < 100 ? request.approve : request.reject('Needs a manager')
33
+ end
34
+
35
+ # Blocking
36
+ answer = agent.call_sync('Refund order A-1029, it arrived broken. It cost 49 dollars.')
37
+ puts answer
38
+
39
+ # Non-blocking with a callback when finished
40
+ agent.call_async('Refund order B-2, it cost 4900 dollars.') do |answer, execution|
41
+ puts "finished (#{execution.finish_reason}): #{answer.inspect}"
42
+ end
43
+
44
+ # Non-blocking, poll it yourself
45
+ execution = agent.call_async('Refund order C-3, it cost 12 dollars.')
46
+ sleep 0.5 until execution.done?
47
+ puts execution.result
48
+ puts "finish_reason: #{execution.finish_reason}" # :stop, or :rejected if the tool was rejected
49
+ puts "tool calls: #{execution.tool_calls.inspect}"
50
+
51
+ Conductor::Agents.shutdown
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Tools: an agent with one Ruby method as a tool.
5
+ #
6
+ # export CONDUCTOR_SERVER_URL=http://localhost:8080/api
7
+ # bundle exec ruby examples/agents/weather.rb
8
+ #
9
+ # The model string names a server-side integration ("openai" here) and a model; the SDK
10
+ # never sees the provider key.
11
+ require_relative '../../lib/conductor/agents'
12
+ include Conductor::Agents
13
+
14
+ tool def get_weather(city: String, units: 'metric')
15
+ { temp_c: 21.0, summary: "Sunny in #{city}" }
16
+ end
17
+ describe :get_weather, 'Get the current weather for a city.'
18
+
19
+ agent = Agent.new(
20
+ name: 'weather',
21
+ model: ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'openai/gpt-4o-mini'),
22
+ instructions: 'Answer weather questions.'
23
+ )
24
+ agent.add_tool :get_weather
25
+
26
+ puts agent.call_sync('Weather in Lisbon?')
27
+ Conductor::Agents.shutdown