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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/components/agent/Gemfile +1 -1
- data/docs/persona-driven-system-prompts.md +346 -0
- data/examples/basic_agent_with_default_chat.rb +99 -0
- data/examples/chat_endpoint_agent.rb +7 -19
- data/examples/identity_aware_chat_agent.rb +83 -0
- data/examples/ux_helpers_demo.rb +0 -0
- data/lib/language_operator/agent/metadata_collector.rb +222 -0
- data/lib/language_operator/agent/prompt_builder.rb +282 -0
- data/lib/language_operator/agent/web_server.rb +83 -20
- data/lib/language_operator/dsl/agent_definition.rb +3 -53
- data/lib/language_operator/dsl/chat_endpoint_definition.rb +112 -2
- data/lib/language_operator/templates/schema/agent_dsl_openapi.yaml +1 -1
- data/lib/language_operator/templates/schema/agent_dsl_schema.json +1 -1
- data/lib/language_operator/version.rb +1 -1
- data/synth/003/Makefile +6 -0
- metadata +6 -1
|
@@ -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
|
-
#
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
|
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: @
|
|
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, @
|
|
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
|
-
#
|
|
498
|
-
|
|
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
|