language-operator 0.1.70 → 0.1.80

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.
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LanguageOperator
4
+ module Agent
5
+ # Execution State Manager
6
+ #
7
+ # Manages the execution state of agents to prevent concurrent executions.
8
+ # Thread-safe implementation ensures only one execution runs at a time.
9
+ #
10
+ # @example Check if execution is running
11
+ # state = ExecutionState.new
12
+ # state.running? # => false
13
+ #
14
+ # @example Start an execution
15
+ # state.start_execution('exec-123')
16
+ # state.running? # => true
17
+ class ExecutionState
18
+ attr_reader :current_execution_id, :started_at, :status
19
+
20
+ # Initialize a new execution state
21
+ def initialize
22
+ @mutex = Mutex.new
23
+ @current_execution_id = nil
24
+ @started_at = nil
25
+ @status = :idle # :idle, :running, :completed, :failed
26
+ end
27
+
28
+ # Start a new execution
29
+ #
30
+ # @param execution_id [String] Unique identifier for the execution
31
+ # @raise [ExecutionInProgressError] if an execution is already running
32
+ # @return [void]
33
+ def start_execution(execution_id)
34
+ @mutex.synchronize do
35
+ if @status == :running
36
+ raise ExecutionInProgressError,
37
+ "Execution #{@current_execution_id} already running"
38
+ end
39
+
40
+ @current_execution_id = execution_id
41
+ @started_at = Time.now
42
+ @status = :running
43
+ end
44
+ end
45
+
46
+ # Mark execution as completed
47
+ #
48
+ # @param result [Object] Optional execution result
49
+ # @return [Object] The result passed in
50
+ def complete_execution(result = nil)
51
+ @mutex.synchronize do
52
+ @status = :completed
53
+ result
54
+ end
55
+ end
56
+
57
+ # Mark execution as failed
58
+ #
59
+ # @param error [StandardError] The error that caused the failure
60
+ # @return [void]
61
+ def fail_execution(error)
62
+ @mutex.synchronize do
63
+ @status = :failed
64
+ @last_error = error
65
+ end
66
+ end
67
+
68
+ # Check if an execution is currently running
69
+ #
70
+ # @return [Boolean] true if execution is in progress
71
+ def running?
72
+ @mutex.synchronize { @status == :running }
73
+ end
74
+
75
+ # Get current execution information
76
+ #
77
+ # @return [Hash] Current execution state details
78
+ def current_info
79
+ @mutex.synchronize do
80
+ {
81
+ status: @status,
82
+ execution_id: @current_execution_id,
83
+ started_at: @started_at
84
+ }
85
+ end
86
+ end
87
+ end
88
+
89
+ # Error raised when attempting to start an execution while one is already running
90
+ class ExecutionInProgressError < StandardError; end
91
+ end
92
+ end
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../loggable'
4
+
5
+ module LanguageOperator
6
+ module Agent
7
+ # Agent Metadata Collector
8
+ #
9
+ # Collects runtime and configuration metadata about an agent for use in
10
+ # persona-driven system prompts and conversation context.
11
+ #
12
+ # Provides information about:
13
+ # - Agent identity (name, description, persona)
14
+ # - Runtime environment (cluster, namespace, mode)
15
+ # - Operational state (uptime, workspace, status)
16
+ # - Configuration details (capabilities, constraints)
17
+ #
18
+ # @example
19
+ # collector = MetadataCollector.new(agent)
20
+ # metadata = collector.collect
21
+ # puts metadata[:identity][:name] # => "my-agent"
22
+ # puts metadata[:runtime][:uptime] # => "2h 15m"
23
+ class MetadataCollector
24
+ include LanguageOperator::Loggable
25
+
26
+ attr_reader :agent, :start_time
27
+
28
+ # Initialize metadata collector
29
+ #
30
+ # @param agent [LanguageOperator::Agent::Base] The agent instance
31
+ def initialize(agent)
32
+ @agent = agent
33
+ @start_time = Time.now
34
+ @logger = logger
35
+ end
36
+
37
+ # Collect all available metadata
38
+ #
39
+ # @return [Hash] Complete metadata structure
40
+ def collect
41
+ {
42
+ identity: collect_identity,
43
+ runtime: collect_runtime,
44
+ environment: collect_environment,
45
+ operational: collect_operational,
46
+ capabilities: collect_capabilities
47
+ }
48
+ end
49
+
50
+ # Collect basic identity information
51
+ #
52
+ # @return [Hash] Identity metadata
53
+ def collect_identity
54
+ config = @agent.config || {}
55
+ agent_config = config.dig('agent') || {}
56
+
57
+ {
58
+ name: ENV.fetch('AGENT_NAME', agent_config['name'] || 'unknown'),
59
+ description: agent_config['instructions'] || agent_config['description'] || 'AI Agent',
60
+ persona: agent_config['persona'] || ENV.fetch('PERSONA_NAME', nil),
61
+ mode: @agent.mode || 'unknown',
62
+ version: LanguageOperator::VERSION
63
+ }
64
+ end
65
+
66
+ # Collect runtime environment information
67
+ #
68
+ # @return [Hash] Runtime metadata
69
+ def collect_runtime
70
+ {
71
+ uptime: calculate_uptime,
72
+ started_at: @start_time.iso8601,
73
+ process_id: Process.pid,
74
+ workspace_available: @agent.workspace_available?,
75
+ mcp_servers_connected: @agent.respond_to?(:servers_info) ? @agent.servers_info.length : 0
76
+ }
77
+ end
78
+
79
+ # Collect deployment environment information
80
+ #
81
+ # @return [Hash] Environment metadata
82
+ def collect_environment
83
+ {
84
+ cluster: ENV.fetch('AGENT_CLUSTER', nil),
85
+ namespace: ENV.fetch('AGENT_NAMESPACE', ENV.fetch('KUBERNETES_NAMESPACE', nil)),
86
+ workspace_path: @agent.workspace_path,
87
+ kubernetes_enabled: !ENV.fetch('KUBERNETES_SERVICE_HOST', nil).nil?,
88
+ telemetry_enabled: !ENV.fetch('OTEL_EXPORTER_OTLP_ENDPOINT', nil).nil?
89
+ }
90
+ end
91
+
92
+ # Collect operational state information
93
+ #
94
+ # @return [Hash] Operational metadata
95
+ def collect_operational
96
+ status = determine_agent_status
97
+
98
+ {
99
+ status: status,
100
+ ready: status == 'ready',
101
+ mode: @agent.mode,
102
+ workspace: {
103
+ path: @agent.workspace_path,
104
+ available: @agent.workspace_available?,
105
+ writable: workspace_writable?
106
+ }
107
+ }
108
+ end
109
+
110
+ # Collect agent capabilities and constraints
111
+ #
112
+ # @return [Hash] Capabilities metadata
113
+ def collect_capabilities
114
+ config = @agent.config || {}
115
+
116
+ # Extract MCP server tools if available
117
+ tools = []
118
+ if @agent.respond_to?(:servers_info)
119
+ @agent.servers_info.each do |server|
120
+ tools << {
121
+ server: server[:name],
122
+ tool_count: server[:tool_count] || 0
123
+ }
124
+ end
125
+ end
126
+
127
+ # Extract constraints if configured
128
+ constraints = config.dig('constraints') || {}
129
+
130
+ {
131
+ tools: tools,
132
+ total_tools: tools.sum { |t| t[:tool_count] },
133
+ constraints: constraints.empty? ? nil : constraints,
134
+ llm_provider: config.dig('llm', 'provider') || ENV.fetch('LLM_PROVIDER', 'unknown'),
135
+ llm_model: config.dig('llm', 'model') || ENV.fetch('MODEL', 'unknown')
136
+ }
137
+ end
138
+
139
+ # Get formatted summary suitable for system prompts
140
+ #
141
+ # @return [Hash] Formatted summary for prompt injection
142
+ def summary_for_prompt
143
+ metadata = collect
144
+ identity = metadata[:identity]
145
+ runtime = metadata[:runtime]
146
+ environment = metadata[:environment]
147
+ operational = metadata[:operational]
148
+ capabilities = metadata[:capabilities]
149
+
150
+ {
151
+ agent_name: identity[:name],
152
+ agent_description: identity[:description],
153
+ agent_mode: identity[:mode],
154
+ uptime: runtime[:uptime],
155
+ cluster: environment[:cluster],
156
+ namespace: environment[:namespace],
157
+ status: operational[:status],
158
+ workspace_available: operational[:ready],
159
+ tool_count: capabilities[:total_tools],
160
+ llm_model: capabilities[:llm_model]
161
+ }
162
+ end
163
+
164
+ private
165
+
166
+ def logger_component
167
+ 'Agent::MetadataCollector'
168
+ end
169
+
170
+ # Calculate human-readable uptime
171
+ #
172
+ # @return [String] Formatted uptime string
173
+ def calculate_uptime
174
+ seconds = Time.now - @start_time
175
+ return 'just started' if seconds < 60
176
+
177
+ minutes = (seconds / 60).floor
178
+ hours = (minutes / 60).floor
179
+ days = (hours / 24).floor
180
+
181
+ if days > 0
182
+ "#{days}d #{hours % 24}h #{minutes % 60}m"
183
+ elsif hours > 0
184
+ "#{hours}h #{minutes % 60}m"
185
+ else
186
+ "#{minutes}m"
187
+ end
188
+ end
189
+
190
+ # Determine current agent status
191
+ #
192
+ # @return [String] Status string
193
+ def determine_agent_status
194
+ return 'not_ready' unless @agent.workspace_available?
195
+ return 'starting' if calculate_uptime == 'just started'
196
+
197
+ # Check if agent is connected and functional
198
+ if @agent.respond_to?(:servers_info) && @agent.servers_info.any?
199
+ 'ready'
200
+ elsif @agent.respond_to?(:servers_info) && @agent.servers_info.empty?
201
+ 'ready_no_tools'
202
+ else
203
+ 'ready'
204
+ end
205
+ end
206
+
207
+ # Check if workspace is writable
208
+ #
209
+ # @return [Boolean] True if workspace is writable
210
+ def workspace_writable?
211
+ return false unless @agent.workspace_available?
212
+
213
+ test_file = File.join(@agent.workspace_path, '.write_test')
214
+ File.write(test_file, 'test')
215
+ File.delete(test_file)
216
+ true
217
+ rescue StandardError
218
+ false
219
+ end
220
+ end
221
+ end
222
+ end
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../loggable'
4
+ require_relative 'metadata_collector'
5
+
6
+ module LanguageOperator
7
+ module Agent
8
+ # Dynamic Prompt Builder
9
+ #
10
+ # Generates persona-driven system prompts by combining static persona configuration
11
+ # with dynamic agent metadata and operational context.
12
+ #
13
+ # Supports multiple template styles and configurable levels of context injection.
14
+ # Falls back to static prompts for backward compatibility.
15
+ #
16
+ # @example Basic usage
17
+ # builder = PromptBuilder.new(agent, chat_endpoint_config)
18
+ # prompt = builder.build_system_prompt
19
+ #
20
+ # @example With custom template
21
+ # builder = PromptBuilder.new(agent, config, template: :detailed)
22
+ # prompt = builder.build_system_prompt
23
+ class PromptBuilder
24
+ include LanguageOperator::Loggable
25
+
26
+ attr_reader :agent, :chat_config, :metadata_collector
27
+
28
+ # Template levels for different amounts of context injection
29
+ TEMPLATE_LEVELS = {
30
+ minimal: :build_minimal_template,
31
+ standard: :build_standard_template,
32
+ detailed: :build_detailed_template,
33
+ comprehensive: :build_comprehensive_template
34
+ }.freeze
35
+
36
+ # Initialize prompt builder
37
+ #
38
+ # @param agent [LanguageOperator::Agent::Base] The agent instance
39
+ # @param chat_config [LanguageOperator::Dsl::ChatEndpointDefinition] Chat endpoint configuration
40
+ # @param options [Hash] Additional options
41
+ # @option options [Symbol] :template Template level (:minimal, :standard, :detailed, :comprehensive)
42
+ # @option options [Boolean] :enable_identity_awareness Enable identity context injection
43
+ def initialize(agent, chat_config, **options)
44
+ @agent = agent
45
+ @chat_config = chat_config
46
+ @options = options
47
+ @metadata_collector = MetadataCollector.new(agent)
48
+
49
+ # Configuration
50
+ @template_level = options[:template] || chat_config&.prompt_template_level || :standard
51
+ @identity_awareness_enabled = if options.key?(:enable_identity_awareness)
52
+ options[:enable_identity_awareness]
53
+ elsif chat_config&.identity_awareness_enabled.nil?
54
+ true
55
+ else
56
+ chat_config.identity_awareness_enabled
57
+ end
58
+ @static_prompt = chat_config&.system_prompt
59
+ end
60
+
61
+ # Build complete system prompt with persona and context
62
+ #
63
+ # @return [String] Generated system prompt
64
+ def build_system_prompt
65
+ # Return static prompt if identity awareness is disabled
66
+ unless @identity_awareness_enabled
67
+ return @static_prompt || build_fallback_prompt
68
+ end
69
+
70
+ # Collect metadata for context injection
71
+ metadata = @metadata_collector.summary_for_prompt
72
+
73
+ # Build dynamic prompt based on template level
74
+ if TEMPLATE_LEVELS.key?(@template_level)
75
+ method_name = TEMPLATE_LEVELS[@template_level]
76
+ send(method_name, metadata)
77
+ else
78
+ logger.warn("Unknown template level: #{@template_level}, falling back to standard")
79
+ build_standard_template(metadata)
80
+ end
81
+ rescue StandardError => e
82
+ logger.error('Failed to build dynamic system prompt, falling back to static',
83
+ error: e.message)
84
+ @static_prompt || build_fallback_prompt
85
+ end
86
+
87
+ # Build prompt for conversation context (shorter version)
88
+ #
89
+ # @return [String] Conversation context prompt
90
+ def build_conversation_context
91
+ return nil unless @identity_awareness_enabled
92
+
93
+ metadata = @metadata_collector.summary_for_prompt
94
+ build_conversation_context_template(metadata)
95
+ rescue StandardError => e
96
+ logger.error('Failed to build conversation context', error: e.message)
97
+ nil
98
+ end
99
+
100
+ private
101
+
102
+ def logger_component
103
+ 'Agent::PromptBuilder'
104
+ end
105
+
106
+ # Minimal template - basic identity only
107
+ def build_minimal_template(metadata)
108
+ base_prompt = @static_prompt || "You are an AI assistant."
109
+
110
+ <<~PROMPT.strip
111
+ #{base_prompt}
112
+
113
+ You are #{metadata[:agent_name]}, running in #{metadata[:cluster] || 'a Kubernetes cluster'}.
114
+ PROMPT
115
+ end
116
+
117
+ # Standard template - identity + basic operational context
118
+ def build_standard_template(metadata)
119
+ base_prompt = @static_prompt || load_persona_prompt || "You are an AI assistant."
120
+
121
+ identity_context = build_identity_context(metadata)
122
+ operational_context = build_basic_operational_context(metadata)
123
+
124
+ <<~PROMPT.strip
125
+ #{base_prompt}
126
+
127
+ #{identity_context}
128
+
129
+ #{operational_context}
130
+
131
+ You can discuss your role, capabilities, and current operational state. Respond as an intelligent agent with awareness of your function and environment.
132
+ PROMPT
133
+ end
134
+
135
+ # Detailed template - full context with capabilities
136
+ def build_detailed_template(metadata)
137
+ base_prompt = @static_prompt || load_persona_prompt || "You are an AI assistant."
138
+
139
+ identity_context = build_identity_context(metadata)
140
+ operational_context = build_detailed_operational_context(metadata)
141
+ capabilities_context = build_capabilities_context(metadata)
142
+
143
+ <<~PROMPT.strip
144
+ #{base_prompt}
145
+
146
+ #{identity_context}
147
+
148
+ #{operational_context}
149
+
150
+ #{capabilities_context}
151
+
152
+ You should:
153
+ - Demonstrate awareness of your identity and purpose
154
+ - Provide context about your operational environment when relevant
155
+ - Discuss your capabilities and tools naturally in conversation
156
+ - Respond as a professional, context-aware agent rather than a generic chatbot
157
+ PROMPT
158
+ end
159
+
160
+ # Comprehensive template - all available context
161
+ def build_comprehensive_template(metadata)
162
+ base_prompt = @static_prompt || load_persona_prompt || "You are an AI assistant."
163
+
164
+ sections = [
165
+ base_prompt,
166
+ build_identity_context(metadata),
167
+ build_detailed_operational_context(metadata),
168
+ build_capabilities_context(metadata),
169
+ build_environment_context(metadata),
170
+ build_behavioral_guidelines
171
+ ].compact
172
+
173
+ sections.join("\n\n")
174
+ end
175
+
176
+ # Short context for ongoing conversations
177
+ def build_conversation_context_template(metadata)
178
+ "Agent: #{metadata[:agent_name]} | Mode: #{metadata[:agent_mode]} | Uptime: #{metadata[:uptime]} | Status: #{metadata[:status]}"
179
+ end
180
+
181
+ # Build identity context section
182
+ def build_identity_context(metadata)
183
+ lines = []
184
+ lines << "You are #{metadata[:agent_name]}, a language agent."
185
+ lines << "Your primary function is: #{metadata[:agent_description]}" if metadata[:agent_description] != 'AI Agent'
186
+ lines << "You are currently running in #{metadata[:agent_mode]} mode."
187
+ lines.join(' ')
188
+ end
189
+
190
+ # Build basic operational context
191
+ def build_basic_operational_context(metadata)
192
+ context_parts = []
193
+
194
+ if metadata[:cluster]
195
+ context_parts << "running in the '#{metadata[:cluster]}' cluster"
196
+ end
197
+
198
+ if metadata[:uptime] != 'just started'
199
+ context_parts << "active for #{metadata[:uptime]}"
200
+ else
201
+ context_parts << "recently started"
202
+ end
203
+
204
+ if metadata[:status] == 'ready'
205
+ context_parts << "currently operational"
206
+ else
207
+ context_parts << "status: #{metadata[:status]}"
208
+ end
209
+
210
+ "You are #{context_parts.join(', ')}."
211
+ end
212
+
213
+ # Build detailed operational context
214
+ def build_detailed_operational_context(metadata)
215
+ lines = []
216
+ lines << build_basic_operational_context(metadata)
217
+
218
+ if metadata[:workspace_available]
219
+ lines << "Your workspace is available and ready for file operations."
220
+ else
221
+ lines << "Your workspace is currently unavailable."
222
+ end
223
+
224
+ lines.join(' ')
225
+ end
226
+
227
+ # Build capabilities context
228
+ def build_capabilities_context(metadata)
229
+ return nil if metadata[:tool_count].to_i.zero?
230
+
231
+ if metadata[:tool_count] == 1
232
+ "You have access to 1 tool to help accomplish tasks."
233
+ else
234
+ "You have access to #{metadata[:tool_count]} tools to help accomplish tasks."
235
+ end
236
+ end
237
+
238
+ # Build environment context
239
+ def build_environment_context(metadata)
240
+ context_parts = []
241
+
242
+ if metadata[:namespace]
243
+ context_parts << "Namespace: #{metadata[:namespace]}"
244
+ end
245
+
246
+ if metadata[:llm_model] && metadata[:llm_model] != 'unknown'
247
+ context_parts << "Model: #{metadata[:llm_model]}"
248
+ end
249
+
250
+ return nil if context_parts.empty?
251
+
252
+ "Environment details: #{context_parts.join(', ')}"
253
+ end
254
+
255
+ # Build behavioral guidelines
256
+ def build_behavioral_guidelines
257
+ <<~GUIDELINES.strip
258
+ Behavioral Guidelines:
259
+ - Maintain awareness of your identity and operational context
260
+ - Provide helpful, accurate responses within your capabilities
261
+ - Reference your environment and tools naturally when relevant
262
+ - Act as a knowledgeable agent rather than a generic assistant
263
+ - Be professional yet personable in your interactions
264
+ GUIDELINES
265
+ end
266
+
267
+ # Load persona prompt if available
268
+ def load_persona_prompt
269
+ return nil unless @agent.config&.dig('agent', 'persona')
270
+
271
+ # In a full implementation, this would load the persona from Kubernetes
272
+ # For now, we'll rely on the static prompt from chat config
273
+ nil
274
+ end
275
+
276
+ # Build fallback prompt when nothing else is available
277
+ def build_fallback_prompt
278
+ "You are an AI assistant running as a language operator agent. You can help with various tasks and questions."
279
+ end
280
+ end
281
+ end
282
+ end