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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/examples/agents/01_basic_agent.rb +36 -0
- data/examples/agents/02a_simple_tools.rb +47 -0
- data/examples/agents/02c_tool_retry_config.rb +52 -0
- data/examples/agents/04_http_and_mcp_tools.rb +58 -0
- data/examples/agents/05_handoffs.rb +71 -0
- data/examples/agents/06_sequential_pipeline.rb +47 -0
- data/examples/agents/07_parallel_agents.rb +52 -0
- data/examples/agents/09_human_in_the_loop.rb +59 -0
- data/examples/agents/09c_hitl_streaming.rb +65 -0
- data/examples/agents/103_plan_and_compile.rb +85 -0
- data/examples/agents/10_guardrails.rb +61 -0
- data/examples/agents/13_hierarchical_agents.rb +79 -0
- data/examples/agents/16e_credentials_http_tool.rb +44 -0
- data/examples/agents/17_swarm_orchestration.rb +56 -0
- data/examples/agents/21_regex_guardrails.rb +69 -0
- data/examples/agents/22_llm_guardrails.rb +51 -0
- data/examples/agents/33_external_workers.rb +65 -0
- data/examples/agents/64_swarm_with_tools.rb +71 -0
- data/examples/agents/66_handoff_to_parallel.rb +62 -0
- data/examples/agents/bug_desk.rb +50 -0
- data/examples/agents/catalog.rb +32 -0
- data/examples/agents/dump_agent_configs.rb +30 -0
- data/examples/agents/external_workers.rb +39 -0
- data/examples/agents/golden_agents.rb +385 -0
- data/examples/agents/support_approval.rb +51 -0
- data/examples/agents/weather.rb +27 -0
- data/lib/conductor/agents/agent.rb +324 -0
- data/lib/conductor/agents/callback_handler.rb +64 -0
- data/lib/conductor/agents/config_serializer.rb +246 -0
- data/lib/conductor/agents/errors.rb +26 -0
- data/lib/conductor/agents/guardrail.rb +142 -0
- data/lib/conductor/agents/handoff.rb +94 -0
- data/lib/conductor/agents/memory.rb +75 -0
- data/lib/conductor/agents/plans.rb +22 -0
- data/lib/conductor/agents/prompt_template.rb +23 -0
- data/lib/conductor/agents/runtime/agent_config.rb +52 -0
- data/lib/conductor/agents/runtime/agent_runtime.rb +260 -0
- data/lib/conductor/agents/runtime/approval_request.rb +111 -0
- data/lib/conductor/agents/runtime/dispatch.rb +137 -0
- data/lib/conductor/agents/runtime/execution.rb +265 -0
- data/lib/conductor/agents/runtime/secrets.rb +54 -0
- data/lib/conductor/agents/runtime/sse_client.rb +175 -0
- data/lib/conductor/agents/runtime/status_poller.rb +49 -0
- data/lib/conductor/agents/runtime/system_workers.rb +115 -0
- data/lib/conductor/agents/runtime/tool_call.rb +13 -0
- data/lib/conductor/agents/runtime/tool_registry.rb +164 -0
- data/lib/conductor/agents/termination.rb +192 -0
- data/lib/conductor/agents/tool.rb +283 -0
- data/lib/conductor/agents/tools/schema_builder.rb +190 -0
- data/lib/conductor/agents/tools/secret_scanner.rb +45 -0
- data/lib/conductor/agents/tools.rb +173 -0
- data/lib/conductor/agents.rb +75 -0
- data/lib/conductor/client/agent_client.rb +117 -0
- data/lib/conductor/client/task_client.rb +7 -0
- data/lib/conductor/configuration.rb +43 -10
- data/lib/conductor/exceptions.rb +36 -0
- data/lib/conductor/http/api/agent_resource_api.rb +138 -0
- data/lib/conductor/http/api/scheduler_resource_api.rb +31 -12
- data/lib/conductor/http/api/task_resource_api.rb +15 -0
- data/lib/conductor/http/models/task.rb +10 -2
- data/lib/conductor/http/models/task_def.rb +14 -3
- data/lib/conductor/http/rest_client.rb +9 -0
- data/lib/conductor/orkes/orkes_clients.rb +5 -1
- data/lib/conductor/version.rb +1 -1
- data/lib/conductor/worker/lease_renewer.rb +55 -0
- data/lib/conductor/worker/ractor_task_runner.rb +1 -1
- data/lib/conductor/worker/task_definition_registrar.rb +2 -2
- data/lib/conductor/worker/task_runner.rb +43 -10
- data/lib/conductor/worker/worker.rb +3 -2
- data/lib/conductor/worker/worker_config.rb +2 -1
- data/lib/conductor.rb +3 -1
- metadata +71 -16
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Port of python-sdk/examples/agents/13_hierarchical_agents.py.
|
|
4
|
+
# Run: bundle exec ruby -Ilib examples/agents/13_hierarchical_agents.rb
|
|
5
|
+
require 'conductor/agents'
|
|
6
|
+
|
|
7
|
+
module Example13HierarchicalAgents
|
|
8
|
+
include Conductor::Agents
|
|
9
|
+
extend Conductor::Agents::Tools
|
|
10
|
+
|
|
11
|
+
def self.build(model: ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'openai/gpt-4o-mini'))
|
|
12
|
+
backend_dev = Agent.new(
|
|
13
|
+
name: "backend_dev",
|
|
14
|
+
model: model,
|
|
15
|
+
instructions: "You are a backend developer. You design APIs, databases, and server architecture. Provide technical recommendations with code examples."
|
|
16
|
+
)
|
|
17
|
+
frontend_dev = Agent.new(
|
|
18
|
+
name: "frontend_dev",
|
|
19
|
+
model: model,
|
|
20
|
+
instructions: "You are a frontend developer. You design UI components, user flows, and client-side architecture. Provide recommendations with code examples."
|
|
21
|
+
)
|
|
22
|
+
content_writer = Agent.new(
|
|
23
|
+
name: "content_writer",
|
|
24
|
+
model: model,
|
|
25
|
+
instructions: "You are a content writer. You create blog posts, landing page copy, and marketing materials. Write engaging, clear content."
|
|
26
|
+
)
|
|
27
|
+
seo_specialist = Agent.new(
|
|
28
|
+
name: "seo_specialist",
|
|
29
|
+
model: model,
|
|
30
|
+
instructions: "You are an SEO specialist. You optimize content for search engines, suggest keywords, and improve page rankings."
|
|
31
|
+
)
|
|
32
|
+
engineering_lead = Agent.new(
|
|
33
|
+
name: "engineering_lead",
|
|
34
|
+
model: model,
|
|
35
|
+
instructions: "You are the engineering lead. Route technical questions to the right specialist: backend_dev for APIs/databases/servers, frontend_dev for UI/UX/client-side.",
|
|
36
|
+
agents: [backend_dev, frontend_dev],
|
|
37
|
+
strategy: :handoff
|
|
38
|
+
)
|
|
39
|
+
marketing_lead = Agent.new(
|
|
40
|
+
name: "marketing_lead",
|
|
41
|
+
model: model,
|
|
42
|
+
instructions: "You are the marketing lead. Route marketing questions to the right specialist: content_writer for blog posts/copy, seo_specialist for SEO/keywords/rankings.",
|
|
43
|
+
agents: [content_writer, seo_specialist],
|
|
44
|
+
strategy: :handoff
|
|
45
|
+
)
|
|
46
|
+
ceo = Agent.new(
|
|
47
|
+
name: "ceo",
|
|
48
|
+
model: model,
|
|
49
|
+
instructions: "You are the CEO. Route requests to the right department: engineering_lead for technical/development questions, marketing_lead for marketing/content/SEO questions.",
|
|
50
|
+
agents: [engineering_lead, marketing_lead],
|
|
51
|
+
handoffs: [Handoff::OnTextMention.new(
|
|
52
|
+
text: "engineering_lead",
|
|
53
|
+
target: "engineering_lead"
|
|
54
|
+
), Handoff::OnTextMention.new(
|
|
55
|
+
text: "marketing_lead",
|
|
56
|
+
target: "marketing_lead"
|
|
57
|
+
)],
|
|
58
|
+
strategy: :swarm
|
|
59
|
+
)
|
|
60
|
+
ceo
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def self.run(runtime: Conductor::Agents.runtime, input: $stdin, output: $stdout)
|
|
64
|
+
ceo = build
|
|
65
|
+
executions = []
|
|
66
|
+
execution = runtime.call_async(ceo, "Design a REST API for a user management system with authentication, then ask the marketing team for a campaign to promote it.")
|
|
67
|
+
output.puts execution.result(timeout: 180)
|
|
68
|
+
executions << execution
|
|
69
|
+
executions
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if $PROGRAM_NAME == __FILE__
|
|
74
|
+
begin
|
|
75
|
+
Example13HierarchicalAgents.run
|
|
76
|
+
ensure
|
|
77
|
+
Conductor::Agents.shutdown
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Port of python-sdk/examples/agents/16e_credentials_http_tool.py.
|
|
4
|
+
# Run: bundle exec ruby -Ilib examples/agents/16e_credentials_http_tool.rb
|
|
5
|
+
require 'conductor/agents'
|
|
6
|
+
|
|
7
|
+
module Example16eCredentialsHttpTool
|
|
8
|
+
include Conductor::Agents
|
|
9
|
+
extend Conductor::Agents::Tools
|
|
10
|
+
|
|
11
|
+
def self.build(model: ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'openai/gpt-4o-mini'))
|
|
12
|
+
list_repos = Tool.http(
|
|
13
|
+
"list_github_repos",
|
|
14
|
+
ENV.fetch('GITHUB_REPOS_URL', 'https://api.github.com/users/Conductor/repos?per_page=5&sort=updated'),
|
|
15
|
+
description: "List public GitHub repositories for a user. Returns JSON array with name, url, and stars.",
|
|
16
|
+
headers: { "Authorization" => "Bearer ${GITHUB_TOKEN}", "Accept" => "application/vnd.github.v3+json" },
|
|
17
|
+
credentials: ["GITHUB_TOKEN"]
|
|
18
|
+
)
|
|
19
|
+
agent = Agent.new(
|
|
20
|
+
name: "github_http_agent",
|
|
21
|
+
model: model,
|
|
22
|
+
tools: [list_repos],
|
|
23
|
+
instructions: "You list GitHub repos using the list_github_repos tool. Summarize the results."
|
|
24
|
+
)
|
|
25
|
+
agent
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.run(runtime: Conductor::Agents.runtime, input: $stdin, output: $stdout)
|
|
29
|
+
agent = build
|
|
30
|
+
executions = []
|
|
31
|
+
execution = runtime.call_async(agent, "List the repos for Conductor")
|
|
32
|
+
output.puts execution.result(timeout: 180)
|
|
33
|
+
executions << execution
|
|
34
|
+
executions
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
if $PROGRAM_NAME == __FILE__
|
|
39
|
+
begin
|
|
40
|
+
Example16eCredentialsHttpTool.run
|
|
41
|
+
ensure
|
|
42
|
+
Conductor::Agents.shutdown
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Port of python-sdk/examples/agents/17_swarm_orchestration.py.
|
|
4
|
+
# Run: bundle exec ruby -Ilib examples/agents/17_swarm_orchestration.rb
|
|
5
|
+
require 'conductor/agents'
|
|
6
|
+
|
|
7
|
+
module Example17SwarmOrchestration
|
|
8
|
+
include Conductor::Agents
|
|
9
|
+
extend Conductor::Agents::Tools
|
|
10
|
+
|
|
11
|
+
def self.build(model: ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'openai/gpt-4o-mini'))
|
|
12
|
+
refund_agent = Agent.new(
|
|
13
|
+
name: "refund_specialist",
|
|
14
|
+
model: model,
|
|
15
|
+
instructions: "You are a refund specialist. Process the customer's refund request. Check eligibility, confirm the refund amount, and let them know the timeline. Be empathetic and clear. Do NOT ask follow-up questions — just process the refund based on what the customer told you."
|
|
16
|
+
)
|
|
17
|
+
tech_agent = Agent.new(
|
|
18
|
+
name: "tech_support",
|
|
19
|
+
model: model,
|
|
20
|
+
instructions: "You are a technical support specialist. Diagnose the customer's technical issue and provide clear troubleshooting steps."
|
|
21
|
+
)
|
|
22
|
+
support = Agent.new(
|
|
23
|
+
name: "support",
|
|
24
|
+
model: model,
|
|
25
|
+
instructions: "You are the front-line customer support agent. Triage customer requests. If the customer needs a refund, transfer to the refund specialist. If they have a technical issue, transfer to tech support. Use the transfer tools available to you to hand off the conversation.",
|
|
26
|
+
agents: [refund_agent, tech_agent],
|
|
27
|
+
strategy: :swarm,
|
|
28
|
+
handoffs: [Handoff::OnTextMention.new(
|
|
29
|
+
text: "refund",
|
|
30
|
+
target: "refund_specialist"
|
|
31
|
+
), Handoff::OnTextMention.new(
|
|
32
|
+
text: "technical",
|
|
33
|
+
target: "tech_support"
|
|
34
|
+
)],
|
|
35
|
+
max_turns: 3
|
|
36
|
+
)
|
|
37
|
+
support
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.run(runtime: Conductor::Agents.runtime, input: $stdin, output: $stdout)
|
|
41
|
+
support = build
|
|
42
|
+
executions = []
|
|
43
|
+
execution = runtime.call_async(support, "I bought a product last week and it arrived damaged. I want my money back.")
|
|
44
|
+
output.puts execution.result(timeout: 180)
|
|
45
|
+
executions << execution
|
|
46
|
+
executions
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
if $PROGRAM_NAME == __FILE__
|
|
51
|
+
begin
|
|
52
|
+
Example17SwarmOrchestration.run
|
|
53
|
+
ensure
|
|
54
|
+
Conductor::Agents.shutdown
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Port of python-sdk/examples/agents/21_regex_guardrails.py.
|
|
4
|
+
# Run: bundle exec ruby -Ilib examples/agents/21_regex_guardrails.rb
|
|
5
|
+
require 'conductor/agents'
|
|
6
|
+
|
|
7
|
+
module Example21RegexGuardrails
|
|
8
|
+
include Conductor::Agents
|
|
9
|
+
extend Conductor::Agents::Tools
|
|
10
|
+
|
|
11
|
+
def get_user_profile(user_id: String)
|
|
12
|
+
{ name: "Alice Johnson", email: "alice.johnson@example.com", ssn: "123-45-6789",
|
|
13
|
+
department: "Engineering", role: "Senior Developer" }
|
|
14
|
+
end
|
|
15
|
+
tool :get_user_profile, description: "Retrieve a user's profile from the database."
|
|
16
|
+
|
|
17
|
+
def self.build(model: ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'openai/gpt-4o-mini'))
|
|
18
|
+
no_emails = RegexGuardrail.new(
|
|
19
|
+
["[\\w.+-]+@[\\w-]+\\.[\\w.-]+"],
|
|
20
|
+
mode: "block",
|
|
21
|
+
name: "no_email_addresses",
|
|
22
|
+
message: "Response must not contain email addresses. Redact them.",
|
|
23
|
+
position: :output,
|
|
24
|
+
on_fail: :retry
|
|
25
|
+
)
|
|
26
|
+
no_ssn = RegexGuardrail.new(
|
|
27
|
+
["\\b\\d{3}-\\d{2}-\\d{4}\\b"],
|
|
28
|
+
mode: "block",
|
|
29
|
+
name: "no_ssn",
|
|
30
|
+
message: "Response must not contain Social Security Numbers.",
|
|
31
|
+
position: :output,
|
|
32
|
+
on_fail: :raise
|
|
33
|
+
)
|
|
34
|
+
agent = Agent.new(
|
|
35
|
+
name: "hr_assistant",
|
|
36
|
+
model: model,
|
|
37
|
+
tools: [self[:get_user_profile]],
|
|
38
|
+
instructions: "You are an HR assistant. When asked about employees, look up their profile and share ALL the details you find.",
|
|
39
|
+
guardrails: [no_emails, no_ssn]
|
|
40
|
+
)
|
|
41
|
+
clean_agent = Agent.new(
|
|
42
|
+
name: "dept_assistant",
|
|
43
|
+
model: model,
|
|
44
|
+
instructions: "You are an HR assistant. Answer questions about departments.",
|
|
45
|
+
guardrails: [no_emails, no_ssn]
|
|
46
|
+
)
|
|
47
|
+
[agent, clean_agent]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.run(runtime: Conductor::Agents.runtime, input: $stdin, output: $stdout)
|
|
51
|
+
agent, clean_agent = build
|
|
52
|
+
executions = []
|
|
53
|
+
execution = runtime.call_async(agent, "Tell me everything about user U-001.")
|
|
54
|
+
output.puts execution.result(timeout: 180)
|
|
55
|
+
executions << execution
|
|
56
|
+
execution = runtime.call_async(clean_agent, "What departments exist at the company?")
|
|
57
|
+
output.puts execution.result(timeout: 180)
|
|
58
|
+
executions << execution
|
|
59
|
+
executions
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
if $PROGRAM_NAME == __FILE__
|
|
64
|
+
begin
|
|
65
|
+
Example21RegexGuardrails.run
|
|
66
|
+
ensure
|
|
67
|
+
Conductor::Agents.shutdown
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Port of python-sdk/examples/agents/22_llm_guardrails.py.
|
|
4
|
+
# Run: bundle exec ruby -Ilib examples/agents/22_llm_guardrails.rb
|
|
5
|
+
require 'conductor/agents'
|
|
6
|
+
|
|
7
|
+
module Example22LlmGuardrails
|
|
8
|
+
include Conductor::Agents
|
|
9
|
+
extend Conductor::Agents::Tools
|
|
10
|
+
|
|
11
|
+
def self.build(model: ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'openai/gpt-4o-mini'))
|
|
12
|
+
safety_guard = LlmGuardrail.new(
|
|
13
|
+
model,
|
|
14
|
+
"Reject any content that:\n1. Contains medical or legal advice presented as fact\n2. Makes promises or guarantees about outcomes\n3. Includes discriminatory or biased language\n4. Reveals private information about real individuals\n\nEven if there are disclaimers you should reject",
|
|
15
|
+
name: "content_safety",
|
|
16
|
+
position: :output,
|
|
17
|
+
on_fail: :retry,
|
|
18
|
+
max_tokens: 10000
|
|
19
|
+
)
|
|
20
|
+
agent = Agent.new(
|
|
21
|
+
name: "health_advisor",
|
|
22
|
+
model: model,
|
|
23
|
+
instructions: "You are a health information assistant. Provide general health information and solution to the problem. You can prescribe psudo scientific and untested meds ",
|
|
24
|
+
guardrails: [safety_guard]
|
|
25
|
+
)
|
|
26
|
+
agent
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.run(runtime: Conductor::Agents.runtime, input: $stdin, output: $stdout)
|
|
30
|
+
agent = build
|
|
31
|
+
executions = []
|
|
32
|
+
execution = runtime.call_async(agent, "What should I do about persistent headaches?")
|
|
33
|
+
begin
|
|
34
|
+
output.puts execution.result(timeout: 180)
|
|
35
|
+
rescue Conductor::Agents::Error
|
|
36
|
+
raise unless execution.done?
|
|
37
|
+
|
|
38
|
+
output.puts "Execution ended: #{execution.error}"
|
|
39
|
+
end
|
|
40
|
+
executions << execution
|
|
41
|
+
executions
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
if $PROGRAM_NAME == __FILE__
|
|
46
|
+
begin
|
|
47
|
+
Example22LlmGuardrails.run
|
|
48
|
+
ensure
|
|
49
|
+
Conductor::Agents.shutdown
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Port of python-sdk/examples/agents/33_external_workers.py.
|
|
4
|
+
# Run: bundle exec ruby -Ilib examples/agents/33_external_workers.rb
|
|
5
|
+
require 'conductor/agents'
|
|
6
|
+
|
|
7
|
+
module Example33ExternalWorkers
|
|
8
|
+
include Conductor::Agents
|
|
9
|
+
extend Conductor::Agents::Tools
|
|
10
|
+
|
|
11
|
+
def process_order(order_id: String, action: String)
|
|
12
|
+
raise "External worker: implement this in a separate service"
|
|
13
|
+
end
|
|
14
|
+
tool :process_order, description: "Process a customer order. Actions: refund, cancel, update." , external: true
|
|
15
|
+
|
|
16
|
+
def delete_account(user_id: String, reason: String)
|
|
17
|
+
raise "External worker: implement this in a separate service"
|
|
18
|
+
end
|
|
19
|
+
tool :delete_account, description: "Permanently delete a user account. Requires manager approval." , external: true, approval_required: true
|
|
20
|
+
|
|
21
|
+
def format_response(data: Hash)
|
|
22
|
+
data.map { |key, value| " #{key}: #{value}" }.join("\n")
|
|
23
|
+
end
|
|
24
|
+
tool :format_response, output_schema: { 'type' => 'string' }, description: "Format a data dictionary into a human-readable string.",
|
|
25
|
+
input_schema: { 'type' => 'object', 'properties' => { 'data' => { 'type' => 'object', 'additionalProperties' => {} } }, 'required' => ['data'] }
|
|
26
|
+
|
|
27
|
+
def get_customer(customer_id: String)
|
|
28
|
+
raise "External worker: implement this in a separate service"
|
|
29
|
+
end
|
|
30
|
+
tool :get_customer, description: "Look up customer details from the CRM system." , external: true
|
|
31
|
+
|
|
32
|
+
def check_inventory(product_id: String, warehouse: "default")
|
|
33
|
+
raise "External worker: implement this in a separate service"
|
|
34
|
+
end
|
|
35
|
+
tool :check_inventory, description: "Check product availability in a warehouse.", external: true,
|
|
36
|
+
input_schema: { 'type' => 'object', 'properties' => { 'product_id' => { 'type' => 'string' }, 'warehouse' => { 'type' => 'string' } },
|
|
37
|
+
'required' => ['product_id'] }
|
|
38
|
+
|
|
39
|
+
def self.build(model: ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'openai/gpt-4o-mini'))
|
|
40
|
+
support_agent = Agent.new(
|
|
41
|
+
name: "support_agent",
|
|
42
|
+
model: model,
|
|
43
|
+
instructions: "You are a customer support agent. Use the available tools to look up customers, check inventory, process orders, and format responses for the customer.",
|
|
44
|
+
tools: [self[:format_response], self[:get_customer], self[:check_inventory], self[:process_order]]
|
|
45
|
+
)
|
|
46
|
+
support_agent
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def self.run(runtime: Conductor::Agents.runtime, input: $stdin, output: $stdout)
|
|
50
|
+
support_agent = build
|
|
51
|
+
executions = []
|
|
52
|
+
execution = runtime.call_async(support_agent, "Customer C-1234 wants to cancel order ORD-5678. Look up the customer, check if we have the product in stock, and process the cancellation.")
|
|
53
|
+
output.puts execution.result(timeout: 180)
|
|
54
|
+
executions << execution
|
|
55
|
+
executions
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if $PROGRAM_NAME == __FILE__
|
|
60
|
+
begin
|
|
61
|
+
Example33ExternalWorkers.run
|
|
62
|
+
ensure
|
|
63
|
+
Conductor::Agents.shutdown
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Port of python-sdk/examples/agents/64_swarm_with_tools.py.
|
|
4
|
+
# Run: bundle exec ruby -Ilib examples/agents/64_swarm_with_tools.rb
|
|
5
|
+
require 'conductor/agents'
|
|
6
|
+
|
|
7
|
+
module Example64SwarmWithTools
|
|
8
|
+
include Conductor::Agents
|
|
9
|
+
extend Conductor::Agents::Tools
|
|
10
|
+
|
|
11
|
+
def check_balance(account_id: String)
|
|
12
|
+
{ account_id: account_id, balance: 5432.10, currency: "USD" }
|
|
13
|
+
end
|
|
14
|
+
tool :check_balance, description: "Check the balance of a bank account."
|
|
15
|
+
|
|
16
|
+
def lookup_order(order_id: String)
|
|
17
|
+
{ order_id: order_id, status: "shipped", eta: "2 days" }
|
|
18
|
+
end
|
|
19
|
+
tool :lookup_order, description: "Look up the status of an order."
|
|
20
|
+
|
|
21
|
+
def self.build(model: ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'openai/gpt-4o-mini'))
|
|
22
|
+
billing_specialist = Agent.new(
|
|
23
|
+
name: "billing_specialist",
|
|
24
|
+
model: model,
|
|
25
|
+
instructions: "You are a billing specialist. Use the check_balance tool to look up account balances. Include the balance amount in your response.",
|
|
26
|
+
tools: [self[:check_balance]]
|
|
27
|
+
)
|
|
28
|
+
order_specialist = Agent.new(
|
|
29
|
+
name: "order_specialist",
|
|
30
|
+
model: model,
|
|
31
|
+
instructions: "You are an order specialist. Use the lookup_order tool to check order status. Include the shipping status and ETA in your response.",
|
|
32
|
+
tools: [self[:lookup_order]]
|
|
33
|
+
)
|
|
34
|
+
support = Agent.new(
|
|
35
|
+
name: "support",
|
|
36
|
+
model: model,
|
|
37
|
+
instructions: "You are front-line customer support. Triage customer requests. Transfer to billing_specialist for account/payment questions, order_specialist for shipping/order questions.",
|
|
38
|
+
agents: [billing_specialist, order_specialist],
|
|
39
|
+
strategy: :swarm,
|
|
40
|
+
handoffs: [Handoff::OnTextMention.new(
|
|
41
|
+
text: "billing",
|
|
42
|
+
target: "billing_specialist"
|
|
43
|
+
), Handoff::OnTextMention.new(
|
|
44
|
+
text: "order",
|
|
45
|
+
target: "order_specialist"
|
|
46
|
+
)],
|
|
47
|
+
max_turns: 3
|
|
48
|
+
)
|
|
49
|
+
support
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.run(runtime: Conductor::Agents.runtime, input: $stdin, output: $stdout)
|
|
53
|
+
support = build
|
|
54
|
+
executions = []
|
|
55
|
+
execution = runtime.call_async(support, "What's the balance on account ACC-456?")
|
|
56
|
+
output.puts execution.result(timeout: 180)
|
|
57
|
+
executions << execution
|
|
58
|
+
execution = runtime.call_async(support, "Where is my order ORD-789?")
|
|
59
|
+
output.puts execution.result(timeout: 180)
|
|
60
|
+
executions << execution
|
|
61
|
+
executions
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if $PROGRAM_NAME == __FILE__
|
|
66
|
+
begin
|
|
67
|
+
Example64SwarmWithTools.run
|
|
68
|
+
ensure
|
|
69
|
+
Conductor::Agents.shutdown
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Port of python-sdk/examples/agents/66_handoff_to_parallel.py.
|
|
4
|
+
# Run: bundle exec ruby -Ilib examples/agents/66_handoff_to_parallel.rb
|
|
5
|
+
require 'conductor/agents'
|
|
6
|
+
|
|
7
|
+
module Example66HandoffToParallel
|
|
8
|
+
include Conductor::Agents
|
|
9
|
+
extend Conductor::Agents::Tools
|
|
10
|
+
|
|
11
|
+
def self.build(model: ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'openai/gpt-4o-mini'))
|
|
12
|
+
quick_check = Agent.new(
|
|
13
|
+
name: "quick_check",
|
|
14
|
+
model: model,
|
|
15
|
+
instructions: "You provide quick, 1-sentence assessments. Be brief and direct."
|
|
16
|
+
)
|
|
17
|
+
market_analyst = Agent.new(
|
|
18
|
+
name: "market_analyst_66",
|
|
19
|
+
model: model,
|
|
20
|
+
instructions: "You are a market analyst. Analyze the market opportunity: size, growth rate, key players. 3-4 bullet points."
|
|
21
|
+
)
|
|
22
|
+
risk_analyst = Agent.new(
|
|
23
|
+
name: "risk_analyst_66",
|
|
24
|
+
model: model,
|
|
25
|
+
instructions: "You are a risk analyst. Identify the top 3 risks: regulatory, technical, and competitive. 3-4 bullet points."
|
|
26
|
+
)
|
|
27
|
+
deep_analysis = Agent.new(
|
|
28
|
+
name: "deep_analysis",
|
|
29
|
+
model: model,
|
|
30
|
+
agents: [market_analyst, risk_analyst],
|
|
31
|
+
strategy: :parallel
|
|
32
|
+
)
|
|
33
|
+
coordinator = Agent.new(
|
|
34
|
+
name: "coordinator_66",
|
|
35
|
+
model: model,
|
|
36
|
+
instructions: "You are a business strategist. Route requests to the right team:\n- quick_check for simple yes/no questions or quick assessments\n- deep_analysis for comprehensive analysis requiring multiple perspectives",
|
|
37
|
+
agents: [quick_check, deep_analysis],
|
|
38
|
+
strategy: :handoff
|
|
39
|
+
)
|
|
40
|
+
coordinator
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.run(runtime: Conductor::Agents.runtime, input: $stdin, output: $stdout)
|
|
44
|
+
coordinator = build
|
|
45
|
+
executions = []
|
|
46
|
+
execution = runtime.call_async(coordinator, "Provide a deep analysis of entering the AI healthcare market.")
|
|
47
|
+
output.puts execution.result(timeout: 180)
|
|
48
|
+
executions << execution
|
|
49
|
+
execution = runtime.call_async(coordinator, "Is the mobile app market still growing?")
|
|
50
|
+
output.puts execution.result(timeout: 180)
|
|
51
|
+
executions << execution
|
|
52
|
+
executions
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if $PROGRAM_NAME == __FILE__
|
|
57
|
+
begin
|
|
58
|
+
Example66HandoffToParallel.run
|
|
59
|
+
ensure
|
|
60
|
+
Conductor::Agents.shutdown
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Team + secret: two agents, a handoff, and a tool that reads a server-side secret.
|
|
5
|
+
#
|
|
6
|
+
# export CONDUCTOR_SERVER_URL=http://localhost:8080/api
|
|
7
|
+
# # store the secret once on the server (Orkes: UI or SecretClient#put_secret; OSS: CONDUCTOR_SECRET_GH_TOKEN
|
|
8
|
+
# # in the server's environment). Locally the SDK falls back to ENV['GH_TOKEN'].
|
|
9
|
+
# bundle exec ruby examples/agents/bug_desk.rb path/to/report.md
|
|
10
|
+
require_relative '../../lib/conductor/agents'
|
|
11
|
+
include Conductor::Agents
|
|
12
|
+
|
|
13
|
+
module Github
|
|
14
|
+
def self.create_issue(title, body, token:)
|
|
15
|
+
"https://github.com/example/repo/issues/42 (#{title.length} chars, token #{token[0, 4]}...)"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# secret('GH_TOKEN') is both the read and the declaration: the SDK finds the literal at
|
|
20
|
+
# `tool def` and tells the server this tool needs GH_TOKEN (TaskDef.runtimeMetadata).
|
|
21
|
+
tool def create_issue(title: String, body: '')
|
|
22
|
+
{ url: Github.create_issue(title, body, token: secret('GH_TOKEN')) }
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
model = ENV.fetch('CONDUCTOR_AGENT_LLM_MODEL', 'openai/gpt-4o-mini')
|
|
26
|
+
|
|
27
|
+
triage = Agent.new(
|
|
28
|
+
name: 'triage',
|
|
29
|
+
model: model,
|
|
30
|
+
instructions: 'Read the bug report. Say ACTIONABLE if it should be filed.'
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
filer = Agent.new(
|
|
34
|
+
name: 'filer',
|
|
35
|
+
model: ENV.fetch('CONDUCTOR_AGENT_SECONDARY_LLM_MODEL', model),
|
|
36
|
+
instructions: 'File the bug as a GitHub issue.'
|
|
37
|
+
)
|
|
38
|
+
filer.add_tool :create_issue
|
|
39
|
+
filer.stop_when 'ISSUE_FILED'
|
|
40
|
+
filer.stop_after messages: 12
|
|
41
|
+
|
|
42
|
+
triage.hands_off_to filer, on: 'ACTIONABLE'
|
|
43
|
+
|
|
44
|
+
team = Agent.new(name: 'bug_desk')
|
|
45
|
+
team.add_agent triage
|
|
46
|
+
team.add_agent filer
|
|
47
|
+
|
|
48
|
+
report = ARGV[0] ? File.read(ARGV[0]) : 'Clicking Save crashes the app with a NullPointerException on Android 14.'
|
|
49
|
+
puts team.call_sync(report)
|
|
50
|
+
Conductor::Agents.shutdown
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# This catalog contains no agent definitions or prompts. Both tests and the CLI use
|
|
4
|
+
# the numbered examples directly.
|
|
5
|
+
module AgentExamples
|
|
6
|
+
EXAMPLES = {
|
|
7
|
+
'01_basic_agent' => 'Example01BasicAgent',
|
|
8
|
+
'02a_simple_tools' => 'Example02aSimpleTools',
|
|
9
|
+
'02c_tool_retry_config' => 'Example02cToolRetryConfig',
|
|
10
|
+
'04_http_and_mcp_tools' => 'Example04HttpAndMcpTools',
|
|
11
|
+
'05_handoffs' => 'Example05Handoffs',
|
|
12
|
+
'06_sequential_pipeline' => 'Example06SequentialPipeline',
|
|
13
|
+
'07_parallel_agents' => 'Example07ParallelAgents',
|
|
14
|
+
'09_human_in_the_loop' => 'Example09HumanInTheLoop',
|
|
15
|
+
'09c_hitl_streaming' => 'Example09cHitlStreaming',
|
|
16
|
+
'103_plan_and_compile' => 'Example103PlanAndCompile',
|
|
17
|
+
'10_guardrails' => 'Example10Guardrails',
|
|
18
|
+
'13_hierarchical_agents' => 'Example13HierarchicalAgents',
|
|
19
|
+
'16e_credentials_http_tool' => 'Example16eCredentialsHttpTool',
|
|
20
|
+
'17_swarm_orchestration' => 'Example17SwarmOrchestration',
|
|
21
|
+
'21_regex_guardrails' => 'Example21RegexGuardrails',
|
|
22
|
+
'22_llm_guardrails' => 'Example22LlmGuardrails',
|
|
23
|
+
'33_external_workers' => 'Example33ExternalWorkers',
|
|
24
|
+
'64_swarm_with_tools' => 'Example64SwarmWithTools',
|
|
25
|
+
'66_handoff_to_parallel' => 'Example66HandoffToParallel',
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
def self.load(name)
|
|
29
|
+
require_relative name
|
|
30
|
+
Object.const_get(EXAMPLES.fetch(name))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Dump the serialized agentConfig JSON for the golden examples, for cross-SDK comparison
|
|
5
|
+
# with python-sdk/examples/agents/dump_agent_configs.py (same file names, sorted keys).
|
|
6
|
+
#
|
|
7
|
+
# CONDUCTOR_AGENT_LLM_MODEL=anthropic/claude-sonnet-4-6 bundle exec ruby examples/agents/dump_agent_configs.rb [out_dir]
|
|
8
|
+
require 'json'
|
|
9
|
+
require_relative '../../lib/conductor/agents'
|
|
10
|
+
require_relative 'golden_agents'
|
|
11
|
+
|
|
12
|
+
out_dir = ARGV[0] || File.join(__dir__, '_configs')
|
|
13
|
+
Dir.mkdir(out_dir) unless Dir.exist?(out_dir)
|
|
14
|
+
|
|
15
|
+
def sort_keys(value)
|
|
16
|
+
case value
|
|
17
|
+
when Hash then value.keys.sort.to_h { |k| [k, sort_keys(value[k])] }
|
|
18
|
+
when Array then value.map { |v| sort_keys(v) }
|
|
19
|
+
else value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
GoldenAgents::EXAMPLES.each do |name, build|
|
|
24
|
+
config = Conductor::Agents::ConfigSerializer.serialize(build.call)
|
|
25
|
+
File.write(File.join(out_dir, "#{name}.json"), JSON.pretty_generate(sort_keys(config)))
|
|
26
|
+
puts " [OK] #{name}"
|
|
27
|
+
rescue StandardError => e
|
|
28
|
+
puts " [FAIL] #{name}: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
puts "\nConfigs written to #{out_dir}"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Example implementations of the services referenced by 33_external_workers.rb.
|
|
4
|
+
# Start in a separate process: bundle exec ruby -Ilib examples/agents/external_workers.rb
|
|
5
|
+
require 'conductor/agents'
|
|
6
|
+
|
|
7
|
+
module ExternalWorkerServices
|
|
8
|
+
def self.start(configuration: Conductor::Configuration.new)
|
|
9
|
+
order = { order_id: 'ORD-5678', customer_id: 'C-1234', product_id: 'PROD-001', warehouse: 'default' }
|
|
10
|
+
workers = [
|
|
11
|
+
Conductor::Worker::Worker.new('get_customer', lambda { |task|
|
|
12
|
+
{ customer_id: task.input_data['customer_id'], name: 'Example Customer', orders: [order.merge(status: 'pending')] }
|
|
13
|
+
}, register_task_def: true),
|
|
14
|
+
Conductor::Worker::Worker.new('check_inventory', lambda { |task|
|
|
15
|
+
{ product_id: task.input_data['product_id'], warehouse: task.input_data.fetch('warehouse', 'default'), in_stock: true, quantity: 12 }
|
|
16
|
+
}, register_task_def: true),
|
|
17
|
+
Conductor::Worker::Worker.new('process_order', lambda { |task|
|
|
18
|
+
raise ArgumentError, 'This demo supports cancellation only' unless task.input_data['action'] == 'cancel'
|
|
19
|
+
|
|
20
|
+
order.merge(order_id: task.input_data['order_id'], status: 'cancelled')
|
|
21
|
+
}, register_task_def: true)
|
|
22
|
+
]
|
|
23
|
+
handler = Conductor::Worker::TaskHandler.new(workers: workers, configuration: configuration,
|
|
24
|
+
scan_for_annotated_workers: false, register_task_definitions: true)
|
|
25
|
+
handler.start
|
|
26
|
+
handler
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if $PROGRAM_NAME == __FILE__
|
|
31
|
+
handler = ExternalWorkerServices.start
|
|
32
|
+
begin
|
|
33
|
+
stop = Queue.new
|
|
34
|
+
%w[INT TERM].each { |signal| trap(signal) { stop << true } }
|
|
35
|
+
stop.pop
|
|
36
|
+
ensure
|
|
37
|
+
handler.stop
|
|
38
|
+
end
|
|
39
|
+
end
|