language-operator 0.1.70 → 0.1.71

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,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
@@ -4,6 +4,7 @@ require 'rack'
4
4
  require 'rackup'
5
5
  require 'mcp'
6
6
  require_relative 'executor'
7
+ require_relative 'prompt_builder'
7
8
 
8
9
  module LanguageOperator
9
10
  module Agent
@@ -110,15 +111,21 @@ module LanguageOperator
110
111
 
111
112
  # Register chat completion endpoint
112
113
  #
113
- # Sets up OpenAI-compatible chat completion endpoint.
114
- # Agents can be used as drop-in LLM replacements.
114
+ # Sets up OpenAI-compatible chat completion endpoint for all agents.
115
+ # Every agent automatically gets identity-aware chat capabilities.
115
116
  #
116
- # @param chat_endpoint_def [LanguageOperator::Dsl::ChatEndpointDefinition] Chat endpoint definition
117
117
  # @param agent [LanguageOperator::Agent::Base] The agent instance
118
118
  # @return [void]
119
- def register_chat_endpoint(chat_endpoint_def, agent)
120
- @chat_endpoint = chat_endpoint_def
119
+ def register_chat_endpoint(agent)
121
120
  @chat_agent = agent
121
+
122
+ # Create simple chat configuration (identity awareness always enabled)
123
+ @chat_config = {
124
+ model_name: ENV.fetch('AGENT_NAME', agent.config&.dig('agent', 'name') || 'agent'),
125
+ system_prompt: build_default_system_prompt(agent),
126
+ temperature: 0.7,
127
+ max_tokens: 2000
128
+ }
122
129
 
123
130
  # Register OpenAI-compatible endpoint
124
131
  register_route('/v1/chat/completions', method: :post) do |context|
@@ -131,19 +138,19 @@ module LanguageOperator
131
138
  object: 'list',
132
139
  data: [
133
140
  {
134
- id: chat_endpoint_def.model_name,
141
+ id: @chat_config[:model_name],
135
142
  object: 'model',
136
143
  created: Time.now.to_i,
137
144
  owned_by: 'language-operator',
138
145
  permission: [],
139
- root: chat_endpoint_def.model_name,
146
+ root: @chat_config[:model_name],
140
147
  parent: nil
141
148
  }
142
149
  ]
143
150
  }
144
151
  end
145
152
 
146
- puts "Registered chat completion endpoint as model: #{chat_endpoint_def.model_name}"
153
+ puts "Registered identity-aware chat endpoint as model: #{@chat_config[:model_name]}"
147
154
  end
148
155
 
149
156
  # Handle incoming HTTP request
@@ -193,6 +200,24 @@ module LanguageOperator
193
200
 
194
201
  private
195
202
 
203
+ # Build default system prompt for agent
204
+ #
205
+ # Creates a basic system prompt based on agent description
206
+ #
207
+ # @param agent [LanguageOperator::Agent::Base] The agent instance
208
+ # @return [String] Default system prompt
209
+ def build_default_system_prompt(agent)
210
+ description = agent.config&.dig('agent', 'instructions') ||
211
+ agent.config&.dig('agent', 'description') ||
212
+ "AI assistant"
213
+
214
+ if description.downcase.start_with?('you are')
215
+ description
216
+ else
217
+ "You are #{description.downcase}. Provide helpful assistance based on your capabilities."
218
+ end
219
+ end
220
+
196
221
  # Setup executor pool for connection reuse
197
222
  #
198
223
  # Creates a thread-safe queue pre-populated with executor instances
@@ -444,7 +469,7 @@ module LanguageOperator
444
469
  id: "chatcmpl-#{SecureRandom.hex(12)}",
445
470
  object: 'chat.completion',
446
471
  created: Time.now.to_i,
447
- model: @chat_endpoint.model_name,
472
+ model: @chat_config[:model_name],
448
473
  choices: [
449
474
  {
450
475
  index: 0,
@@ -480,41 +505,79 @@ module LanguageOperator
480
505
  'Cache-Control' => 'no-cache',
481
506
  'Connection' => 'keep-alive'
482
507
  },
483
- StreamingBody.new(@chat_agent, prompt, @chat_endpoint.model_name)
508
+ StreamingBody.new(@chat_agent, prompt, @chat_config[:model_name])
484
509
  ]
485
510
  end
486
511
 
487
- # Build prompt from OpenAI message format
512
+ # Build prompt from OpenAI message format with identity awareness
488
513
  #
489
514
  # @param messages [Array<Hash>] Array of message objects
490
- # @return [String] Combined prompt
515
+ # @return [String] Combined prompt with agent identity context
491
516
  def build_prompt_from_messages(messages)
492
- # Combine all messages into a single prompt
493
- # System messages become instructions
494
- # User/assistant messages become conversation
495
517
  prompt_parts = []
496
518
 
497
- # Add system prompt if configured
498
- prompt_parts << "System: #{@chat_endpoint.system_prompt}" if @chat_endpoint.system_prompt
519
+ # Build identity-aware system prompt (always enabled)
520
+ system_prompt = build_identity_aware_system_prompt
521
+ prompt_parts << "System: #{system_prompt}" if system_prompt
522
+
523
+ # Add conversation context (always enabled)
524
+ conversation_context = build_conversation_context
525
+ prompt_parts << conversation_context if conversation_context
499
526
 
500
- # Add conversation history
527
+ # Add conversation history (skip system messages from original array since we handle them above)
501
528
  messages.each do |msg|
502
529
  role = msg['role']
503
530
  content = msg['content']
504
531
 
505
532
  case role
506
- when 'system'
507
- prompt_parts << "System: #{content}"
508
533
  when 'user'
509
534
  prompt_parts << "User: #{content}"
510
535
  when 'assistant'
511
536
  prompt_parts << "Assistant: #{content}"
537
+ # Skip system messages - we handle them via PromptBuilder
512
538
  end
513
539
  end
514
540
 
515
541
  prompt_parts.join("\n\n")
516
542
  end
517
543
 
544
+ # Build identity-aware system prompt using PromptBuilder
545
+ #
546
+ # @return [String] Dynamic system prompt with agent identity
547
+ def build_identity_aware_system_prompt
548
+ # Create prompt builder with identity awareness always enabled
549
+ builder = PromptBuilder.new(
550
+ @chat_agent,
551
+ nil, # No chat config needed
552
+ template: :standard, # Good default
553
+ enable_identity_awareness: true
554
+ )
555
+
556
+ builder.build_system_prompt
557
+ rescue StandardError => e
558
+ # Log error and fall back to static prompt
559
+ puts "Warning: Failed to build identity-aware system prompt: #{e.message}"
560
+ @chat_config[:system_prompt]
561
+ end
562
+
563
+ # Build conversation context for ongoing chats
564
+ #
565
+ # @return [String, nil] Conversation context
566
+ def build_conversation_context
567
+ builder = PromptBuilder.new(
568
+ @chat_agent,
569
+ nil, # No chat config needed
570
+ enable_identity_awareness: true
571
+ )
572
+
573
+ context = builder.build_conversation_context
574
+ context ? "Context: #{context}" : nil
575
+ rescue StandardError => e
576
+ # Log error and continue without context
577
+ puts "Warning: Failed to build conversation context: #{e.message}"
578
+ nil
579
+ end
580
+
518
581
  # Estimate token count (rough approximation)
519
582
  #
520
583
  # @param text [String] Text to estimate