language-operator 0.0.1 → 0.1.31
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/.rubocop.yml +125 -0
- data/CHANGELOG.md +88 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +284 -0
- data/LICENSE +229 -21
- data/Makefile +82 -0
- data/README.md +3 -11
- data/Rakefile +63 -0
- data/bin/aictl +7 -0
- data/completions/_aictl +232 -0
- data/completions/aictl.bash +121 -0
- data/completions/aictl.fish +114 -0
- data/docs/architecture/agent-runtime.md +585 -0
- data/docs/dsl/SCHEMA_VERSION.md +250 -0
- data/docs/dsl/agent-reference.md +604 -0
- data/docs/dsl/best-practices.md +1078 -0
- data/docs/dsl/chat-endpoints.md +895 -0
- data/docs/dsl/constraints.md +671 -0
- data/docs/dsl/mcp-integration.md +1177 -0
- data/docs/dsl/webhooks.md +932 -0
- data/docs/dsl/workflows.md +744 -0
- data/lib/language_operator/agent/base.rb +110 -0
- data/lib/language_operator/agent/executor.rb +440 -0
- data/lib/language_operator/agent/instrumentation.rb +54 -0
- data/lib/language_operator/agent/metrics_tracker.rb +183 -0
- data/lib/language_operator/agent/safety/ast_validator.rb +272 -0
- data/lib/language_operator/agent/safety/audit_logger.rb +104 -0
- data/lib/language_operator/agent/safety/budget_tracker.rb +175 -0
- data/lib/language_operator/agent/safety/content_filter.rb +93 -0
- data/lib/language_operator/agent/safety/manager.rb +207 -0
- data/lib/language_operator/agent/safety/rate_limiter.rb +150 -0
- data/lib/language_operator/agent/safety/safe_executor.rb +127 -0
- data/lib/language_operator/agent/scheduler.rb +183 -0
- data/lib/language_operator/agent/telemetry.rb +116 -0
- data/lib/language_operator/agent/web_server.rb +610 -0
- data/lib/language_operator/agent/webhook_authenticator.rb +226 -0
- data/lib/language_operator/agent.rb +149 -0
- data/lib/language_operator/cli/commands/agent.rb +1205 -0
- data/lib/language_operator/cli/commands/cluster.rb +371 -0
- data/lib/language_operator/cli/commands/install.rb +404 -0
- data/lib/language_operator/cli/commands/model.rb +266 -0
- data/lib/language_operator/cli/commands/persona.rb +393 -0
- data/lib/language_operator/cli/commands/quickstart.rb +22 -0
- data/lib/language_operator/cli/commands/status.rb +143 -0
- data/lib/language_operator/cli/commands/system.rb +772 -0
- data/lib/language_operator/cli/commands/tool.rb +537 -0
- data/lib/language_operator/cli/commands/use.rb +47 -0
- data/lib/language_operator/cli/errors/handler.rb +180 -0
- data/lib/language_operator/cli/errors/suggestions.rb +176 -0
- data/lib/language_operator/cli/formatters/code_formatter.rb +77 -0
- data/lib/language_operator/cli/formatters/log_formatter.rb +288 -0
- data/lib/language_operator/cli/formatters/progress_formatter.rb +49 -0
- data/lib/language_operator/cli/formatters/status_formatter.rb +37 -0
- data/lib/language_operator/cli/formatters/table_formatter.rb +163 -0
- data/lib/language_operator/cli/formatters/value_formatter.rb +113 -0
- data/lib/language_operator/cli/helpers/cluster_context.rb +62 -0
- data/lib/language_operator/cli/helpers/cluster_validator.rb +101 -0
- data/lib/language_operator/cli/helpers/editor_helper.rb +58 -0
- data/lib/language_operator/cli/helpers/kubeconfig_validator.rb +167 -0
- data/lib/language_operator/cli/helpers/pastel_helper.rb +24 -0
- data/lib/language_operator/cli/helpers/resource_dependency_checker.rb +74 -0
- data/lib/language_operator/cli/helpers/schedule_builder.rb +108 -0
- data/lib/language_operator/cli/helpers/user_prompts.rb +69 -0
- data/lib/language_operator/cli/main.rb +236 -0
- data/lib/language_operator/cli/templates/tools/generic.yaml +66 -0
- data/lib/language_operator/cli/wizards/agent_wizard.rb +246 -0
- data/lib/language_operator/cli/wizards/quickstart_wizard.rb +588 -0
- data/lib/language_operator/client/base.rb +214 -0
- data/lib/language_operator/client/config.rb +136 -0
- data/lib/language_operator/client/cost_calculator.rb +37 -0
- data/lib/language_operator/client/mcp_connector.rb +123 -0
- data/lib/language_operator/client.rb +19 -0
- data/lib/language_operator/config/cluster_config.rb +101 -0
- data/lib/language_operator/config/tool_patterns.yaml +57 -0
- data/lib/language_operator/config/tool_registry.rb +96 -0
- data/lib/language_operator/config.rb +138 -0
- data/lib/language_operator/dsl/adapter.rb +124 -0
- data/lib/language_operator/dsl/agent_context.rb +90 -0
- data/lib/language_operator/dsl/agent_definition.rb +427 -0
- data/lib/language_operator/dsl/chat_endpoint_definition.rb +115 -0
- data/lib/language_operator/dsl/config.rb +119 -0
- data/lib/language_operator/dsl/context.rb +50 -0
- data/lib/language_operator/dsl/execution_context.rb +47 -0
- data/lib/language_operator/dsl/helpers.rb +109 -0
- data/lib/language_operator/dsl/http.rb +184 -0
- data/lib/language_operator/dsl/mcp_server_definition.rb +73 -0
- data/lib/language_operator/dsl/parameter_definition.rb +124 -0
- data/lib/language_operator/dsl/registry.rb +36 -0
- data/lib/language_operator/dsl/schema.rb +1102 -0
- data/lib/language_operator/dsl/shell.rb +125 -0
- data/lib/language_operator/dsl/tool_definition.rb +112 -0
- data/lib/language_operator/dsl/webhook_authentication.rb +114 -0
- data/lib/language_operator/dsl/webhook_definition.rb +106 -0
- data/lib/language_operator/dsl/workflow_definition.rb +259 -0
- data/lib/language_operator/dsl.rb +161 -0
- data/lib/language_operator/errors.rb +60 -0
- data/lib/language_operator/kubernetes/client.rb +279 -0
- data/lib/language_operator/kubernetes/resource_builder.rb +194 -0
- data/lib/language_operator/loggable.rb +47 -0
- data/lib/language_operator/logger.rb +141 -0
- data/lib/language_operator/retry.rb +123 -0
- data/lib/language_operator/retryable.rb +132 -0
- data/lib/language_operator/templates/README.md +23 -0
- data/lib/language_operator/templates/examples/agent_synthesis.tmpl +115 -0
- data/lib/language_operator/templates/examples/persona_distillation.tmpl +19 -0
- data/lib/language_operator/templates/schema/.gitkeep +0 -0
- data/lib/language_operator/templates/schema/CHANGELOG.md +93 -0
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +306 -0
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +452 -0
- data/lib/language_operator/tool_loader.rb +242 -0
- data/lib/language_operator/validators.rb +170 -0
- data/lib/language_operator/version.rb +1 -1
- data/lib/language_operator.rb +65 -3
- data/requirements/tasks/challenge.md +9 -0
- data/requirements/tasks/iterate.md +36 -0
- data/requirements/tasks/optimize.md +21 -0
- data/requirements/tasks/tag.md +5 -0
- data/test_agent_dsl.rb +108 -0
- metadata +507 -20
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../client'
|
|
4
|
+
require_relative 'telemetry'
|
|
5
|
+
require_relative 'instrumentation'
|
|
6
|
+
|
|
7
|
+
module LanguageOperator
|
|
8
|
+
module Agent
|
|
9
|
+
# Base Agent Class
|
|
10
|
+
#
|
|
11
|
+
# Extends LanguageOperator::Client::Base with agent-specific functionality including:
|
|
12
|
+
# - Workspace integration
|
|
13
|
+
# - Goal-directed execution
|
|
14
|
+
# - Autonomous operation modes
|
|
15
|
+
#
|
|
16
|
+
# @example Basic agent
|
|
17
|
+
# agent = LanguageOperator::Agent::Base.new(config)
|
|
18
|
+
# agent.connect!
|
|
19
|
+
# agent.execute_goal("Complete the task")
|
|
20
|
+
class Base < LanguageOperator::Client::Base
|
|
21
|
+
include Instrumentation
|
|
22
|
+
|
|
23
|
+
attr_reader :workspace_path, :mode
|
|
24
|
+
|
|
25
|
+
# Initialize the agent
|
|
26
|
+
#
|
|
27
|
+
# @param config [Hash] Configuration hash
|
|
28
|
+
def initialize(config)
|
|
29
|
+
super
|
|
30
|
+
|
|
31
|
+
# Initialize OpenTelemetry
|
|
32
|
+
LanguageOperator::Agent::Telemetry.configure
|
|
33
|
+
otel_enabled = !ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil).nil?
|
|
34
|
+
logger.info "OpenTelemetry #{otel_enabled ? 'enabled' : 'disabled'}"
|
|
35
|
+
|
|
36
|
+
@workspace_path = ENV.fetch('WORKSPACE_PATH', '/workspace')
|
|
37
|
+
@mode = ENV.fetch('AGENT_MODE', 'autonomous')
|
|
38
|
+
@executor = nil
|
|
39
|
+
@scheduler = nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Run the agent in its configured mode
|
|
43
|
+
#
|
|
44
|
+
# @return [void]
|
|
45
|
+
def run
|
|
46
|
+
with_span('agent.run', attributes: {
|
|
47
|
+
'agent.name' => ENV.fetch('AGENT_NAME', nil),
|
|
48
|
+
'agent.mode' => @mode,
|
|
49
|
+
'agent.workspace_available' => workspace_available?
|
|
50
|
+
}) do
|
|
51
|
+
connect!
|
|
52
|
+
|
|
53
|
+
case @mode
|
|
54
|
+
when 'autonomous', 'interactive'
|
|
55
|
+
run_autonomous
|
|
56
|
+
when 'scheduled', 'event-driven'
|
|
57
|
+
run_scheduled
|
|
58
|
+
when 'reactive', 'http', 'webhook'
|
|
59
|
+
run_reactive
|
|
60
|
+
else
|
|
61
|
+
raise "Unknown agent mode: #{@mode}"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Execute a single goal
|
|
67
|
+
#
|
|
68
|
+
# @param goal [String] The goal to achieve
|
|
69
|
+
# @return [String] The result
|
|
70
|
+
def execute_goal(goal)
|
|
71
|
+
@executor ||= Executor.new(self)
|
|
72
|
+
@executor.execute(goal)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Check if workspace is available
|
|
76
|
+
#
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def workspace_available?
|
|
79
|
+
File.directory?(@workspace_path) && File.writable?(@workspace_path)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
# Run in autonomous mode
|
|
85
|
+
#
|
|
86
|
+
# @return [void]
|
|
87
|
+
def run_autonomous
|
|
88
|
+
@executor = Executor.new(self)
|
|
89
|
+
@executor.run_loop
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Run in scheduled mode
|
|
93
|
+
#
|
|
94
|
+
# @return [void]
|
|
95
|
+
def run_scheduled
|
|
96
|
+
@scheduler = Scheduler.new(self)
|
|
97
|
+
@scheduler.start
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Run in reactive mode (HTTP server)
|
|
101
|
+
#
|
|
102
|
+
# @return [void]
|
|
103
|
+
def run_reactive
|
|
104
|
+
require_relative 'web_server'
|
|
105
|
+
@web_server = WebServer.new(self)
|
|
106
|
+
@web_server.start
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../logger'
|
|
4
|
+
require_relative '../loggable'
|
|
5
|
+
require_relative 'metrics_tracker'
|
|
6
|
+
require_relative 'safety/manager'
|
|
7
|
+
require_relative 'instrumentation'
|
|
8
|
+
|
|
9
|
+
module LanguageOperator
|
|
10
|
+
module Agent
|
|
11
|
+
# Task Executor
|
|
12
|
+
#
|
|
13
|
+
# Handles autonomous task execution with retry logic and error handling.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# executor = Executor.new(agent)
|
|
17
|
+
# executor.execute("Complete the task")
|
|
18
|
+
class Executor
|
|
19
|
+
include LanguageOperator::Loggable
|
|
20
|
+
include Instrumentation
|
|
21
|
+
|
|
22
|
+
attr_reader :agent, :iteration_count, :metrics_tracker
|
|
23
|
+
|
|
24
|
+
# Initialize the executor
|
|
25
|
+
#
|
|
26
|
+
# @param agent [LanguageOperator::Agent::Base] The agent instance
|
|
27
|
+
# @param agent_definition [LanguageOperator::Dsl::AgentDefinition, nil] Optional agent definition
|
|
28
|
+
def initialize(agent, agent_definition: nil)
|
|
29
|
+
@agent = agent
|
|
30
|
+
@agent_definition = agent_definition
|
|
31
|
+
@iteration_count = 0
|
|
32
|
+
@max_iterations = 100
|
|
33
|
+
@show_full_responses = ENV.fetch('SHOW_FULL_RESPONSES', 'false') == 'true'
|
|
34
|
+
@metrics_tracker = MetricsTracker.new
|
|
35
|
+
|
|
36
|
+
# Initialize safety manager from agent definition or environment
|
|
37
|
+
@safety_manager = initialize_safety_manager(agent_definition)
|
|
38
|
+
|
|
39
|
+
logger.debug('Executor initialized',
|
|
40
|
+
max_iterations: @max_iterations,
|
|
41
|
+
show_full_responses: @show_full_responses,
|
|
42
|
+
workspace: @agent.workspace_path,
|
|
43
|
+
safety_enabled: @safety_manager&.enabled?)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Execute a task with additional context (for webhooks/HTTP requests)
|
|
47
|
+
#
|
|
48
|
+
# @param instruction [String] The instruction to execute
|
|
49
|
+
# @param context [Hash] Additional context (webhook payload, request data, etc.)
|
|
50
|
+
# @return [String] The result
|
|
51
|
+
def execute_with_context(instruction:, context: {})
|
|
52
|
+
# Build enriched instruction with context
|
|
53
|
+
enriched_instruction = build_instruction_with_context(instruction, context)
|
|
54
|
+
|
|
55
|
+
# Execute with standard logic
|
|
56
|
+
execute(enriched_instruction)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Execute a single task or workflow
|
|
60
|
+
#
|
|
61
|
+
# @param task [String] The task to execute
|
|
62
|
+
# @param agent_definition [LanguageOperator::Dsl::AgentDefinition, nil] Optional agent definition with workflow
|
|
63
|
+
# @return [String] The result
|
|
64
|
+
# rubocop:disable Metrics/BlockLength
|
|
65
|
+
def execute(task, agent_definition: nil)
|
|
66
|
+
with_span('agent.execute_goal', attributes: {
|
|
67
|
+
'agent.goal_description' => task[0...500]
|
|
68
|
+
}) do
|
|
69
|
+
@iteration_count += 1
|
|
70
|
+
|
|
71
|
+
# Route to workflow execution if agent has a workflow defined
|
|
72
|
+
return execute_workflow(agent_definition) if agent_definition&.workflow
|
|
73
|
+
|
|
74
|
+
# Standard instruction-based execution
|
|
75
|
+
logger.info('Starting iteration',
|
|
76
|
+
iteration: @iteration_count,
|
|
77
|
+
max_iterations: @max_iterations)
|
|
78
|
+
logger.debug('Prompt', prompt: task[0..200])
|
|
79
|
+
|
|
80
|
+
# Safety check before request
|
|
81
|
+
if @safety_manager&.enabled?
|
|
82
|
+
# Estimate cost and tokens (rough estimate)
|
|
83
|
+
estimated_tokens = estimate_tokens(task)
|
|
84
|
+
estimated_cost = estimate_cost(estimated_tokens)
|
|
85
|
+
|
|
86
|
+
@safety_manager.check_request!(
|
|
87
|
+
message: task,
|
|
88
|
+
estimated_cost: estimated_cost,
|
|
89
|
+
estimated_tokens: estimated_tokens
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
logger.info('🤖 LLM request')
|
|
94
|
+
result = logger.timed('LLM response received') do
|
|
95
|
+
@agent.send_message(task)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Record metrics
|
|
99
|
+
model_id = @agent.config.dig('llm', 'model')
|
|
100
|
+
@metrics_tracker.record_request(result, model_id) if model_id
|
|
101
|
+
|
|
102
|
+
# Safety check after response and record spending
|
|
103
|
+
result_text = result.is_a?(String) ? result : result.content
|
|
104
|
+
metrics = @metrics_tracker.cumulative_stats
|
|
105
|
+
|
|
106
|
+
if @safety_manager&.enabled?
|
|
107
|
+
@safety_manager.check_response!(result_text)
|
|
108
|
+
@safety_manager.record_request(
|
|
109
|
+
cost: metrics[:estimatedCost],
|
|
110
|
+
tokens: metrics[:totalTokens]
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
logger.info('✓ Iteration completed',
|
|
114
|
+
iteration: @iteration_count,
|
|
115
|
+
response_length: result_text.length,
|
|
116
|
+
total_tokens: metrics[:totalTokens],
|
|
117
|
+
estimated_cost: "$#{metrics[:estimatedCost]}")
|
|
118
|
+
logger.debug('Response preview', response: result_text[0..200])
|
|
119
|
+
|
|
120
|
+
result
|
|
121
|
+
rescue StandardError => e
|
|
122
|
+
handle_error(e)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
# rubocop:enable Metrics/BlockLength
|
|
126
|
+
|
|
127
|
+
# Run continuous execution loop
|
|
128
|
+
#
|
|
129
|
+
# @return [void]
|
|
130
|
+
def run_loop
|
|
131
|
+
start_time = Time.now
|
|
132
|
+
|
|
133
|
+
logger.info('▶ Starting execution')
|
|
134
|
+
logger.info('Configuration',
|
|
135
|
+
workspace: @agent.workspace_path,
|
|
136
|
+
mcp_servers: @agent.servers_info.length,
|
|
137
|
+
max_iterations: @max_iterations)
|
|
138
|
+
|
|
139
|
+
# Log persona loading
|
|
140
|
+
persona = @agent.config.dig('agent', 'persona') || 'default'
|
|
141
|
+
logger.info("👤 Loading persona: #{persona}")
|
|
142
|
+
|
|
143
|
+
# Log MCP server details
|
|
144
|
+
if @agent.servers_info.any?
|
|
145
|
+
@agent.servers_info.each do |server|
|
|
146
|
+
logger.info('◆ MCP server connected', name: server[:name], tool_count: server[:tool_count])
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Get initial instructions from config or environment
|
|
151
|
+
instructions = @agent.config.dig('agent', 'instructions') ||
|
|
152
|
+
ENV['AGENT_INSTRUCTIONS'] ||
|
|
153
|
+
'Monitor workspace and respond to changes'
|
|
154
|
+
|
|
155
|
+
logger.info('Instructions', instructions: instructions[0..200])
|
|
156
|
+
logger.info('Starting autonomous execution loop')
|
|
157
|
+
|
|
158
|
+
loop do
|
|
159
|
+
break if @iteration_count >= @max_iterations
|
|
160
|
+
|
|
161
|
+
progress_pct = ((@iteration_count.to_f / @max_iterations) * 100).round(1)
|
|
162
|
+
logger.debug('Loop progress',
|
|
163
|
+
iteration: @iteration_count,
|
|
164
|
+
max: @max_iterations,
|
|
165
|
+
progress: "#{progress_pct}%")
|
|
166
|
+
|
|
167
|
+
result = execute(instructions)
|
|
168
|
+
result_text = result.is_a?(String) ? result : result.content
|
|
169
|
+
|
|
170
|
+
# Log result based on verbosity settings
|
|
171
|
+
if @show_full_responses
|
|
172
|
+
logger.info('Full iteration result',
|
|
173
|
+
iteration: @iteration_count,
|
|
174
|
+
result: result_text)
|
|
175
|
+
else
|
|
176
|
+
preview = result_text[0..200]
|
|
177
|
+
preview += '...' if result_text.length > 200
|
|
178
|
+
logger.info('Iteration result',
|
|
179
|
+
iteration: @iteration_count,
|
|
180
|
+
preview: preview)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Rate limiting
|
|
184
|
+
logger.debug('Rate limit pause', duration: 5)
|
|
185
|
+
sleep 5
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Log execution summary
|
|
189
|
+
total_duration = Time.now - start_time
|
|
190
|
+
metrics = @metrics_tracker.cumulative_stats
|
|
191
|
+
logger.info('✅ Execution complete',
|
|
192
|
+
iterations: @iteration_count,
|
|
193
|
+
duration_s: total_duration.round(2),
|
|
194
|
+
total_requests: metrics[:requestCount],
|
|
195
|
+
total_tokens: metrics[:totalTokens],
|
|
196
|
+
estimated_cost: "$#{metrics[:estimatedCost]}",
|
|
197
|
+
reason: @iteration_count >= @max_iterations ? 'max_iterations' : 'completed')
|
|
198
|
+
|
|
199
|
+
return unless @iteration_count >= @max_iterations
|
|
200
|
+
|
|
201
|
+
logger.warn('Maximum iterations reached',
|
|
202
|
+
iterations: @max_iterations,
|
|
203
|
+
reason: 'Hit max_iterations limit')
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Execute a workflow-based agent
|
|
207
|
+
#
|
|
208
|
+
# @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
|
|
209
|
+
# @return [RubyLLM::Message] The final response
|
|
210
|
+
def execute_workflow(agent_def)
|
|
211
|
+
start_time = Time.now
|
|
212
|
+
|
|
213
|
+
logger.info("▶ Starting workflow execution: #{agent_def.name}")
|
|
214
|
+
|
|
215
|
+
# Log persona if defined
|
|
216
|
+
logger.info("👤 Loading persona: #{agent_def.persona}") if agent_def.persona
|
|
217
|
+
|
|
218
|
+
# Build orchestration prompt from agent definition
|
|
219
|
+
prompt = build_workflow_prompt(agent_def)
|
|
220
|
+
logger.debug('Workflow prompt', prompt: prompt[0..300])
|
|
221
|
+
|
|
222
|
+
# Register workflow steps as tools (placeholder - will implement after tool converter)
|
|
223
|
+
# For now, just execute with instructions
|
|
224
|
+
result = logger.timed('🤖 LLM request') do
|
|
225
|
+
@agent.send_message(prompt)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Record metrics
|
|
229
|
+
model_id = @agent.config.dig('llm', 'model')
|
|
230
|
+
@metrics_tracker.record_request(result, model_id) if model_id
|
|
231
|
+
|
|
232
|
+
# Write output if configured
|
|
233
|
+
write_output(agent_def, result) if agent_def.output_config && result
|
|
234
|
+
|
|
235
|
+
# Log execution summary
|
|
236
|
+
total_duration = Time.now - start_time
|
|
237
|
+
metrics = @metrics_tracker.cumulative_stats
|
|
238
|
+
logger.info('✅ Workflow execution completed',
|
|
239
|
+
duration_s: total_duration.round(2),
|
|
240
|
+
total_tokens: metrics[:totalTokens],
|
|
241
|
+
estimated_cost: "$#{metrics[:estimatedCost]}")
|
|
242
|
+
result
|
|
243
|
+
rescue StandardError => e
|
|
244
|
+
logger.error('❌ Workflow execution failed', error: e.message)
|
|
245
|
+
handle_error(e)
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Build orchestration prompt from agent definition
|
|
249
|
+
#
|
|
250
|
+
# @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
|
|
251
|
+
# @return [String] The prompt
|
|
252
|
+
def build_workflow_prompt(agent_def)
|
|
253
|
+
prompt = "# Task: #{agent_def.description}\n\n"
|
|
254
|
+
|
|
255
|
+
if agent_def.objectives&.any?
|
|
256
|
+
prompt += "## Objectives:\n"
|
|
257
|
+
agent_def.objectives.each { |obj| prompt += "- #{obj}\n" }
|
|
258
|
+
prompt += "\n"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
if agent_def.workflow&.steps&.any?
|
|
262
|
+
prompt += "## Workflow Steps:\n"
|
|
263
|
+
agent_def.workflow.step_order.each do |step_name|
|
|
264
|
+
step = agent_def.workflow.steps[step_name]
|
|
265
|
+
prompt += step_name.to_s.tr('_', ' ').capitalize.to_s
|
|
266
|
+
prompt += " (using tool: #{step.tool_name})" if step.tool_name
|
|
267
|
+
prompt += " - depends on: #{step.dependencies.join(', ')}" if step.dependencies&.any?
|
|
268
|
+
prompt += "\n"
|
|
269
|
+
end
|
|
270
|
+
prompt += "\n"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
if agent_def.constraints
|
|
274
|
+
prompt += "## Constraints:\n"
|
|
275
|
+
prompt += "- Maximum iterations: #{agent_def.constraints[:max_iterations]}\n" if agent_def.constraints[:max_iterations]
|
|
276
|
+
prompt += "- Timeout: #{agent_def.constraints[:timeout]}\n" if agent_def.constraints[:timeout]
|
|
277
|
+
prompt += "\n"
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
prompt += 'Please complete this task following the workflow steps.'
|
|
281
|
+
prompt
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Write output to configured destinations
|
|
285
|
+
#
|
|
286
|
+
# @param agent_def [LanguageOperator::Dsl::AgentDefinition] The agent definition
|
|
287
|
+
# @param result [RubyLLM::Message] The result to write
|
|
288
|
+
def write_output(agent_def, result)
|
|
289
|
+
return unless agent_def.output_config
|
|
290
|
+
|
|
291
|
+
content = result.is_a?(String) ? result : result.content
|
|
292
|
+
|
|
293
|
+
if (workspace_path = agent_def.output_config[:workspace])
|
|
294
|
+
full_path = File.join(@agent.workspace_path, workspace_path)
|
|
295
|
+
|
|
296
|
+
begin
|
|
297
|
+
FileUtils.mkdir_p(File.dirname(full_path))
|
|
298
|
+
File.write(full_path, content)
|
|
299
|
+
logger.info("📝 Wrote output to #{workspace_path}")
|
|
300
|
+
rescue Errno::EACCES, Errno::EPERM
|
|
301
|
+
# Permission denied - try writing to workspace root
|
|
302
|
+
fallback_path = File.join(@agent.workspace_path, 'output.txt')
|
|
303
|
+
begin
|
|
304
|
+
File.write(fallback_path, content)
|
|
305
|
+
logger.warn("⚠️ Could not write to #{workspace_path}, wrote to output.txt instead")
|
|
306
|
+
rescue StandardError => e2
|
|
307
|
+
logger.warn("⚠️ Could not write output to workspace: #{e2.message}")
|
|
308
|
+
logger.info("📄 Output (first 500 chars): #{content[0..500]}")
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Future: Handle Slack, email outputs
|
|
314
|
+
rescue StandardError => e
|
|
315
|
+
logger.warn('Output writing failed', error: e.message)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
private
|
|
319
|
+
|
|
320
|
+
def logger_component
|
|
321
|
+
'Agent::Executor'
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Build instruction enriched with request context
|
|
325
|
+
#
|
|
326
|
+
# @param instruction [String] Base instruction
|
|
327
|
+
# @param context [Hash] Request context
|
|
328
|
+
# @return [String] Enriched instruction
|
|
329
|
+
def build_instruction_with_context(instruction, context)
|
|
330
|
+
enriched = instruction.dup
|
|
331
|
+
enriched += "\n\n## Request Context\n"
|
|
332
|
+
enriched += "- Method: #{context[:method]}\n" if context[:method]
|
|
333
|
+
enriched += "- Path: #{context[:path]}\n" if context[:path]
|
|
334
|
+
|
|
335
|
+
if context[:params] && !context[:params].empty?
|
|
336
|
+
enriched += "\n### Parameters:\n"
|
|
337
|
+
enriched += "```json\n#{JSON.pretty_generate(context[:params])}\n```\n"
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
if context[:body] && !context[:body].empty?
|
|
341
|
+
enriched += "\n### Request Body:\n"
|
|
342
|
+
enriched += "```\n#{context[:body][0..1000]}\n```\n"
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
if context[:headers] && !context[:headers].empty?
|
|
346
|
+
enriched += "\n### Headers:\n"
|
|
347
|
+
context[:headers].each do |key, value|
|
|
348
|
+
enriched += "- #{key}: #{value}\n"
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
enriched
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def initialize_safety_manager(agent_definition)
|
|
356
|
+
# Get safety config from agent definition constraints
|
|
357
|
+
config = agent_definition&.constraints || {}
|
|
358
|
+
|
|
359
|
+
# Merge with environment variables
|
|
360
|
+
config = {
|
|
361
|
+
enabled: ENV.fetch('SAFETY_ENABLED', 'true') != 'false',
|
|
362
|
+
daily_budget: config[:daily_budget] || parse_float_env('DAILY_BUDGET'),
|
|
363
|
+
hourly_budget: config[:hourly_budget] || parse_float_env('HOURLY_BUDGET'),
|
|
364
|
+
token_budget: config[:token_budget] || parse_int_env('TOKEN_BUDGET'),
|
|
365
|
+
requests_per_minute: config[:requests_per_minute] || parse_int_env('REQUESTS_PER_MINUTE'),
|
|
366
|
+
requests_per_hour: config[:requests_per_hour] || parse_int_env('REQUESTS_PER_HOUR'),
|
|
367
|
+
requests_per_day: config[:requests_per_day] || parse_int_env('REQUESTS_PER_DAY'),
|
|
368
|
+
blocked_patterns: config[:blocked_patterns] || parse_array_env('BLOCKED_PATTERNS'),
|
|
369
|
+
blocked_topics: config[:blocked_topics] || parse_array_env('BLOCKED_TOPICS'),
|
|
370
|
+
case_sensitive: config[:case_sensitive] || ENV.fetch('CASE_SENSITIVE', 'false') == 'true',
|
|
371
|
+
audit_logging: config[:audit_logging] != false
|
|
372
|
+
}.compact
|
|
373
|
+
|
|
374
|
+
return nil if config[:enabled] == false
|
|
375
|
+
|
|
376
|
+
Safety::Manager.new(config)
|
|
377
|
+
rescue StandardError => e
|
|
378
|
+
logger.warn('Failed to initialize safety manager',
|
|
379
|
+
error: e.message,
|
|
380
|
+
fallback: 'Safety features disabled')
|
|
381
|
+
nil
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def parse_float_env(key)
|
|
385
|
+
val = ENV.fetch(key, nil)
|
|
386
|
+
return nil unless val
|
|
387
|
+
|
|
388
|
+
val.to_f
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def parse_int_env(key)
|
|
392
|
+
val = ENV.fetch(key, nil)
|
|
393
|
+
return nil unless val
|
|
394
|
+
|
|
395
|
+
val.to_i
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def parse_array_env(key)
|
|
399
|
+
val = ENV.fetch(key, nil)
|
|
400
|
+
return nil unless val
|
|
401
|
+
|
|
402
|
+
val.split(',').map(&:strip)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def estimate_tokens(text)
|
|
406
|
+
# Rough estimate: ~1.3 tokens per word
|
|
407
|
+
(text.split.length * 1.3).to_i
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def estimate_cost(tokens)
|
|
411
|
+
# Estimate based on common model pricing
|
|
412
|
+
# Average of ~$3-15 per 1M tokens (using $10 as middle ground)
|
|
413
|
+
# This is a rough estimate; actual cost varies by model
|
|
414
|
+
(tokens / 1_000_000.0) * 10.0
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
def handle_error(error)
|
|
418
|
+
case error
|
|
419
|
+
when Timeout::Error, /timeout/i.match?(error.message)
|
|
420
|
+
logger.error('Request timeout',
|
|
421
|
+
error: error.class.name,
|
|
422
|
+
message: error.message,
|
|
423
|
+
iteration: @iteration_count)
|
|
424
|
+
when /connection refused|operation not permitted/i.match?(error.message)
|
|
425
|
+
logger.error('Connection failed',
|
|
426
|
+
error: error.class.name,
|
|
427
|
+
message: error.message,
|
|
428
|
+
hint: 'Check if model service is healthy and accessible')
|
|
429
|
+
else
|
|
430
|
+
logger.error('Task execution failed',
|
|
431
|
+
error: error.class.name,
|
|
432
|
+
message: error.message)
|
|
433
|
+
logger.debug('Backtrace', trace: error.backtrace[0..5].join("\n")) if error.backtrace
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
"Error executing task: #{error.message}"
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'opentelemetry/sdk'
|
|
4
|
+
|
|
5
|
+
module LanguageOperator
|
|
6
|
+
module Agent
|
|
7
|
+
# OpenTelemetry instrumentation helpers for agent methods
|
|
8
|
+
#
|
|
9
|
+
# Provides reusable patterns for tracing agent operations with
|
|
10
|
+
# automatic error handling and span status management.
|
|
11
|
+
#
|
|
12
|
+
# @example Instrument a method
|
|
13
|
+
# include LanguageOperator::Agent::Instrumentation
|
|
14
|
+
#
|
|
15
|
+
# def my_method
|
|
16
|
+
# with_span('my_method', attributes: { 'key' => 'value' }) do
|
|
17
|
+
# # Method implementation
|
|
18
|
+
# end
|
|
19
|
+
# end
|
|
20
|
+
module Instrumentation
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# Get the configured OpenTelemetry tracer
|
|
24
|
+
#
|
|
25
|
+
# @return [OpenTelemetry::Trace::Tracer]
|
|
26
|
+
def tracer
|
|
27
|
+
@tracer ||= OpenTelemetry.tracer_provider.tracer(
|
|
28
|
+
'language-operator-agent',
|
|
29
|
+
LanguageOperator::VERSION
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Execute block within a traced span with automatic error handling
|
|
34
|
+
#
|
|
35
|
+
# Creates a span with the given name and attributes, executes the block,
|
|
36
|
+
# and automatically records exceptions and sets error status if raised.
|
|
37
|
+
#
|
|
38
|
+
# @param name [String] Span name
|
|
39
|
+
# @param attributes [Hash] Span attributes
|
|
40
|
+
# @yield [OpenTelemetry::Trace::Span] The created span
|
|
41
|
+
# @return [Object] Result of the block
|
|
42
|
+
# @raise Re-raises any exception after recording it on the span
|
|
43
|
+
def with_span(name, attributes: {})
|
|
44
|
+
tracer.in_span(name, attributes: attributes) do |span|
|
|
45
|
+
yield span
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
span.record_exception(e)
|
|
48
|
+
span.status = OpenTelemetry::Trace::Status.error(e.message)
|
|
49
|
+
raise
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|