smart_message 0.0.10 → 0.0.12
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/.github/workflows/deploy-github-pages.yml +38 -0
- data/.gitignore +5 -0
- data/CHANGELOG.md +30 -0
- data/Gemfile.lock +35 -4
- data/README.md +169 -71
- data/Rakefile +29 -4
- data/docs/assets/images/ddq_architecture.svg +130 -0
- data/docs/assets/images/dlq_architecture.svg +115 -0
- data/docs/assets/images/enhanced-dual-publishing.svg +136 -0
- data/docs/assets/images/enhanced-fluent-api.svg +149 -0
- data/docs/assets/images/enhanced-microservices-routing.svg +115 -0
- data/docs/assets/images/enhanced-pattern-matching.svg +107 -0
- data/docs/assets/images/fluent-api-demo.svg +59 -0
- data/docs/assets/images/performance-comparison.svg +161 -0
- data/docs/assets/images/redis-basic-architecture.svg +53 -0
- data/docs/assets/images/redis-enhanced-architecture.svg +88 -0
- data/docs/assets/images/redis-queue-architecture.svg +101 -0
- data/docs/assets/images/smart_message.jpg +0 -0
- data/docs/assets/images/smart_message_walking.jpg +0 -0
- data/docs/assets/images/smartmessage_architecture_overview.svg +173 -0
- data/docs/assets/images/transport-comparison-matrix.svg +171 -0
- data/docs/assets/javascripts/mathjax.js +17 -0
- data/docs/assets/stylesheets/extra.css +51 -0
- data/docs/{addressing.md → core-concepts/addressing.md} +5 -7
- data/docs/{architecture.md → core-concepts/architecture.md} +78 -138
- data/docs/{dispatcher.md → core-concepts/dispatcher.md} +21 -21
- data/docs/{message_filtering.md → core-concepts/message-filtering.md} +2 -3
- data/docs/{message_processing.md → core-concepts/message-processing.md} +17 -17
- data/docs/{troubleshooting.md → development/troubleshooting.md} +7 -7
- data/docs/{examples.md → getting-started/examples.md} +115 -89
- data/docs/{getting-started.md → getting-started/quick-start.md} +47 -18
- data/docs/guides/redis-queue-getting-started.md +697 -0
- data/docs/guides/redis-queue-patterns.md +889 -0
- data/docs/guides/redis-queue-production.md +1091 -0
- data/docs/index.md +64 -0
- data/docs/{dead_letter_queue.md → reference/dead-letter-queue.md} +2 -3
- data/docs/{logging.md → reference/logging.md} +1 -1
- data/docs/{message_deduplication.md → reference/message-deduplication.md} +1 -0
- data/docs/{proc_handlers_summary.md → reference/proc-handlers.md} +7 -6
- data/docs/{serializers.md → reference/serializers.md} +3 -5
- data/docs/{transports.md → reference/transports.md} +133 -11
- data/docs/transports/memory-transport.md +374 -0
- data/docs/transports/redis-enhanced-transport.md +524 -0
- data/docs/transports/redis-queue-transport.md +1304 -0
- data/docs/transports/redis-transport-comparison.md +496 -0
- data/docs/transports/redis-transport.md +509 -0
- data/examples/README.md +98 -5
- data/examples/city_scenario/911_emergency_call_flow.svg +99 -0
- data/examples/city_scenario/README.md +515 -0
- data/examples/city_scenario/ai_visitor_intelligence_flow.svg +108 -0
- data/examples/city_scenario/citizen.rb +195 -0
- data/examples/city_scenario/city_diagram.svg +125 -0
- data/examples/city_scenario/common/health_monitor.rb +80 -0
- data/examples/city_scenario/common/logger.rb +30 -0
- data/examples/city_scenario/emergency_dispatch_center.rb +270 -0
- data/examples/city_scenario/fire_department.rb +446 -0
- data/examples/city_scenario/fire_emergency_flow.svg +95 -0
- data/examples/city_scenario/health_department.rb +100 -0
- data/examples/city_scenario/health_monitoring_system.svg +130 -0
- data/examples/city_scenario/house.rb +244 -0
- data/examples/city_scenario/local_bank.rb +217 -0
- data/examples/city_scenario/messages/emergency_911_message.rb +81 -0
- data/examples/city_scenario/messages/emergency_resolved_message.rb +43 -0
- data/examples/city_scenario/messages/fire_dispatch_message.rb +43 -0
- data/examples/city_scenario/messages/fire_emergency_message.rb +45 -0
- data/examples/city_scenario/messages/health_check_message.rb +22 -0
- data/examples/city_scenario/messages/health_status_message.rb +35 -0
- data/examples/city_scenario/messages/police_dispatch_message.rb +46 -0
- data/examples/city_scenario/messages/silent_alarm_message.rb +38 -0
- data/examples/city_scenario/police_department.rb +316 -0
- data/examples/city_scenario/redis_monitor.rb +129 -0
- data/examples/city_scenario/redis_stats.rb +743 -0
- data/examples/city_scenario/room_for_improvement.md +240 -0
- data/examples/city_scenario/security_emergency_flow.svg +95 -0
- data/examples/city_scenario/service_internal_architecture.svg +154 -0
- data/examples/city_scenario/smart_message_ai_agent.rb +364 -0
- data/examples/city_scenario/start_demo.sh +236 -0
- data/examples/city_scenario/stop_demo.sh +106 -0
- data/examples/city_scenario/visitor.rb +631 -0
- data/examples/{10_message_deduplication.rb → memory/01_message_deduplication_demo.rb} +1 -1
- data/examples/{09_dead_letter_queue_demo.rb → memory/02_dead_letter_queue_demo.rb} +13 -40
- data/examples/{01_point_to_point_orders.rb → memory/03_point_to_point_orders.rb} +1 -1
- data/examples/{02_publish_subscribe_events.rb → memory/04_publish_subscribe_events.rb} +2 -2
- data/examples/{03_many_to_many_chat.rb → memory/05_many_to_many_chat.rb} +4 -4
- data/examples/{show_me.rb → memory/06_pretty_print_demo.rb} +1 -1
- data/examples/{05_proc_handlers.rb → memory/07_proc_handlers_demo.rb} +2 -2
- data/examples/{06_custom_logger_example.rb → memory/08_custom_logger_demo.rb} +17 -14
- data/examples/{07_error_handling_scenarios.rb → memory/09_error_handling_demo.rb} +4 -4
- data/examples/{08_entity_addressing_basic.rb → memory/10_entity_addressing_basic.rb} +8 -8
- data/examples/{08_entity_addressing_with_filtering.rb → memory/11_entity_addressing_with_filtering.rb} +6 -6
- data/examples/{09_regex_filtering_microservices.rb → memory/12_regex_filtering_microservices.rb} +2 -2
- data/examples/{10_header_block_configuration.rb → memory/13_header_block_configuration.rb} +6 -6
- data/examples/{11_global_configuration_example.rb → memory/14_global_configuration_demo.rb} +19 -8
- data/examples/{show_logger.rb → memory/15_logger_demo.rb} +1 -1
- data/examples/memory/README.md +163 -0
- data/examples/memory/memory_transport_architecture.svg +90 -0
- data/examples/memory/point_to_point_pattern.svg +94 -0
- data/examples/memory/publish_subscribe_pattern.svg +125 -0
- data/examples/{04_redis_smart_home_iot.rb → redis/01_smart_home_iot_demo.rb} +5 -5
- data/examples/redis/README.md +230 -0
- data/examples/redis/alert_system_flow.svg +127 -0
- data/examples/redis/dashboard_status_flow.svg +107 -0
- data/examples/redis/device_command_flow.svg +113 -0
- data/examples/redis/redis_transport_architecture.svg +115 -0
- data/examples/{smart_home_iot_dataflow.md → redis/smart_home_iot_dataflow.md} +4 -116
- data/examples/redis/smart_home_system_architecture.svg +133 -0
- data/examples/redis_enhanced/README.md +319 -0
- data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +233 -0
- data/examples/redis_enhanced/enhanced_02_fluent_api.rb +331 -0
- data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +281 -0
- data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +419 -0
- data/examples/redis_queue/01_basic_messaging.rb +221 -0
- data/examples/redis_queue/01_comprehensive_examples.rb +508 -0
- data/examples/redis_queue/02_pattern_routing.rb +405 -0
- data/examples/redis_queue/03_fluent_api.rb +422 -0
- data/examples/redis_queue/04_load_balancing.rb +486 -0
- data/examples/redis_queue/05_microservices.rb +735 -0
- data/examples/redis_queue/06_emergency_alerts.rb +777 -0
- data/examples/redis_queue/07_queue_management.rb +587 -0
- data/examples/redis_queue/README.md +366 -0
- data/examples/redis_queue/enhanced_01_basic_patterns.rb +233 -0
- data/examples/redis_queue/enhanced_02_fluent_api.rb +331 -0
- data/examples/redis_queue/enhanced_03_dual_publishing.rb +281 -0
- data/examples/redis_queue/enhanced_04_advanced_routing.rb +419 -0
- data/examples/redis_queue/redis_queue_architecture.svg +148 -0
- data/ideas/README.md +41 -0
- data/ideas/agents.md +1001 -0
- data/ideas/database_transport.md +980 -0
- data/ideas/improvement.md +359 -0
- data/ideas/meshage.md +1788 -0
- data/ideas/message_discovery.md +178 -0
- data/ideas/message_schema.md +1381 -0
- data/lib/smart_message/.idea/.gitignore +8 -0
- data/lib/smart_message/.idea/markdown.xml +6 -0
- data/lib/smart_message/.idea/misc.xml +4 -0
- data/lib/smart_message/.idea/modules.xml +8 -0
- data/lib/smart_message/.idea/smart_message.iml +16 -0
- data/lib/smart_message/.idea/vcs.xml +6 -0
- data/lib/smart_message/addressing.rb +15 -0
- data/lib/smart_message/base.rb +0 -2
- data/lib/smart_message/configuration.rb +1 -1
- data/lib/smart_message/logger.rb +15 -4
- data/lib/smart_message/plugins.rb +5 -2
- data/lib/smart_message/serializer.rb +14 -0
- data/lib/smart_message/transport/redis_enhanced_transport.rb +399 -0
- data/lib/smart_message/transport/redis_queue_transport.rb +555 -0
- data/lib/smart_message/transport/registry.rb +1 -0
- data/lib/smart_message/transport.rb +34 -1
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message.rb +5 -52
- data/mkdocs.yml +184 -0
- data/p2p_plan.md +326 -0
- data/p2p_roadmap.md +287 -0
- data/smart_message.gemspec +2 -0
- data/smart_message.svg +51 -0
- metadata +170 -44
- data/docs/README.md +0 -57
- data/examples/dead_letters.jsonl +0 -12
- data/examples/temp.txt +0 -94
- data/examples/tmux_chat/README.md +0 -283
- data/examples/tmux_chat/bot_agent.rb +0 -278
- data/examples/tmux_chat/human_agent.rb +0 -199
- data/examples/tmux_chat/room_monitor.rb +0 -160
- data/examples/tmux_chat/shared_chat_system.rb +0 -328
- data/examples/tmux_chat/start_chat_demo.sh +0 -190
- data/examples/tmux_chat/stop_chat_demo.sh +0 -22
- /data/docs/{properties.md → core-concepts/properties.md} +0 -0
- /data/docs/{ideas_to_think_about.md → development/ideas.md} +0 -0
@@ -0,0 +1,631 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../../lib/smart_message'
|
4
|
+
require 'ruby_llm'
|
5
|
+
require 'json'
|
6
|
+
|
7
|
+
require_relative 'common/logger'
|
8
|
+
|
9
|
+
# Dynamically require all message files in the messages directory
|
10
|
+
Dir[File.join(__dir__, 'messages', '*.rb')].each { |file| require file }
|
11
|
+
|
12
|
+
class Visitor
|
13
|
+
include Common::Logger
|
14
|
+
|
15
|
+
def initialize
|
16
|
+
@service_name = 'visitor'
|
17
|
+
|
18
|
+
setup_ai
|
19
|
+
logger.info("Visitor initialized - AI-powered message generation system ready")
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
def setup_ai
|
24
|
+
# Initialize the AI model for intelligent message selection
|
25
|
+
begin
|
26
|
+
configure_rubyllm
|
27
|
+
# RubyLLM.chat returns a Chat instance, we can use it directly
|
28
|
+
@llm = RubyLLM.chat
|
29
|
+
@ai_available = true
|
30
|
+
logger.info("AI model initialized for message analysis")
|
31
|
+
rescue => e
|
32
|
+
@ai_available = false
|
33
|
+
logger.warn("AI model not available: #{e.message}. Using fallback logic.")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def configure_rubyllm
|
39
|
+
RubyLLM.configure do |config|
|
40
|
+
config.anthropic_api_key = ENV.fetch('ANTHROPIC_API_KEY', nil)
|
41
|
+
config.deepseek_api_key = ENV.fetch('DEEPSEEK_API_KEY', nil)
|
42
|
+
config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)
|
43
|
+
config.gpustack_api_key = ENV.fetch('GPUSTACK_API_KEY', nil)
|
44
|
+
config.mistral_api_key = ENV.fetch('MISTRAL_API_KEY', nil)
|
45
|
+
config.openrouter_api_key = ENV.fetch('OPENROUTER_API_KEY', nil)
|
46
|
+
config.perplexity_api_key = ENV.fetch('PERPLEXITY_API_KEY', nil)
|
47
|
+
|
48
|
+
# These providers require a little something extra
|
49
|
+
config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
|
50
|
+
config.openai_organization_id = ENV.fetch('OPENAI_ORGANIZATION_ID', nil)
|
51
|
+
config.openai_project_id = ENV.fetch('OPENAI_PROJECT_ID', nil)
|
52
|
+
|
53
|
+
config.bedrock_api_key = ENV.fetch('BEDROCK_ACCESS_KEY_ID', nil)
|
54
|
+
config.bedrock_secret_key = ENV.fetch('BEDROCK_SECRET_ACCESS_KEY', nil)
|
55
|
+
config.bedrock_region = ENV.fetch('BEDROCK_REGION', nil)
|
56
|
+
config.bedrock_session_token = ENV.fetch('BEDROCK_SESSION_TOKEN', nil)
|
57
|
+
|
58
|
+
# Ollama is based upon the OpenAI API so it needs to over-ride a few things
|
59
|
+
config.ollama_api_base = ENV.fetch('OLLAMA_API_BASE', nil)
|
60
|
+
|
61
|
+
# --- Custom OpenAI Endpoint ---
|
62
|
+
# Use this for Azure OpenAI, proxies, or self-hosted models via OpenAI-compatible APIs.
|
63
|
+
config.openai_api_base = ENV.fetch('OPENAI_API_BASE', nil) # e.g., "https://your-azure.openai.azure.com"
|
64
|
+
|
65
|
+
# --- Default Models ---
|
66
|
+
# Used by RubyLLM.chat, RubyLLM.embed, RubyLLM.paint if no model is specified.
|
67
|
+
# config.default_model = 'gpt-4.1-nano' # Default: 'gpt-4.1-nano'
|
68
|
+
# config.default_embedding_model = 'text-embedding-3-small' # Default: 'text-embedding-3-small'
|
69
|
+
# config.default_image_model = 'dall-e-3' # Default: 'dall-e-3'
|
70
|
+
|
71
|
+
# --- Connection Settings ---
|
72
|
+
# config.request_timeout = 120 # Request timeout in seconds (default: 120)
|
73
|
+
# config.max_retries = 3 # Max retries on transient network errors (default: 3)
|
74
|
+
# config.retry_interval = 0.1 # Initial delay in seconds (default: 0.1)
|
75
|
+
# config.retry_backoff_factor = 2 # Multiplier for subsequent retries (default: 2)
|
76
|
+
# config.retry_interval_randomness = 0.5 # Jitter factor (default: 0.5)
|
77
|
+
|
78
|
+
# --- Logging Settings ---
|
79
|
+
config.log_file = 'log/ruby_llm.log'
|
80
|
+
config.log_level = :debug # debug level can also be set to debug by setting RUBYLLM_DEBUG envar to true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
def generate_observation_scenario
|
86
|
+
# Various scenarios the visitor might observe
|
87
|
+
scenarios = [
|
88
|
+
{
|
89
|
+
type: 'crime',
|
90
|
+
description: 'Witnessed armed robbery at convenience store',
|
91
|
+
context: 'A visitor witnessed an armed robbery at a local convenience store. Two suspects with weapons.'
|
92
|
+
},
|
93
|
+
{
|
94
|
+
type: 'fire',
|
95
|
+
description: 'Smoke coming from apartment building',
|
96
|
+
context: 'A visitor noticed heavy smoke coming from a third-floor apartment window.'
|
97
|
+
},
|
98
|
+
{
|
99
|
+
type: 'accident',
|
100
|
+
description: 'Multi-car collision at intersection',
|
101
|
+
context: 'A visitor witnessed a three-car collision at a busy intersection. People may be injured.'
|
102
|
+
},
|
103
|
+
{
|
104
|
+
type: 'medical',
|
105
|
+
description: 'Person collapsed on sidewalk',
|
106
|
+
context: 'A visitor found an elderly person who collapsed on the sidewalk, unconscious but breathing.'
|
107
|
+
},
|
108
|
+
{
|
109
|
+
type: 'suspicious',
|
110
|
+
description: 'Someone breaking into parked cars',
|
111
|
+
context: 'A visitor observed someone systematically checking car doors and breaking into unlocked vehicles.'
|
112
|
+
},
|
113
|
+
{
|
114
|
+
type: 'hazmat',
|
115
|
+
description: 'Chemical smell from abandoned truck',
|
116
|
+
context: 'A visitor noticed a strong chemical odor coming from an abandoned delivery truck.'
|
117
|
+
},
|
118
|
+
{
|
119
|
+
type: 'rescue',
|
120
|
+
description: 'Child stuck on roof',
|
121
|
+
context: 'A visitor spotted a child stuck on a roof, unable to get down safely.'
|
122
|
+
}
|
123
|
+
]
|
124
|
+
|
125
|
+
scenarios.sample
|
126
|
+
end
|
127
|
+
|
128
|
+
|
129
|
+
def report_observation(scenario = nil)
|
130
|
+
scenario ||= generate_observation_scenario
|
131
|
+
logger.info("Starting observation reporting process for: #{scenario[:type]}")
|
132
|
+
|
133
|
+
# Step 1: Collect all message descriptions
|
134
|
+
message_descriptions = collect_message_descriptions
|
135
|
+
logger.info("Collected descriptions for #{message_descriptions.size} message types")
|
136
|
+
|
137
|
+
# Step 2: Ask AI to select appropriate message for the scenario
|
138
|
+
selected_message_class = ask_ai_for_message_selection(message_descriptions, scenario)
|
139
|
+
logger.info("AI selected message type: #{selected_message_class}")
|
140
|
+
|
141
|
+
# Step 3: Collect property descriptions for selected message
|
142
|
+
property_descriptions = collect_property_descriptions(selected_message_class)
|
143
|
+
logger.info("Collected #{property_descriptions.size} properties for #{selected_message_class}")
|
144
|
+
|
145
|
+
# Step 4: Generate message instance using AI with retry on validation errors
|
146
|
+
max_retries = 3
|
147
|
+
retry_count = 0
|
148
|
+
message_instance = nil
|
149
|
+
validation_errors = []
|
150
|
+
|
151
|
+
while retry_count < max_retries
|
152
|
+
message_instance = generate_message_instance(selected_message_class, property_descriptions, validation_errors, scenario)
|
153
|
+
logger.info("Generated message instance with AI-provided values (attempt #{retry_count + 1})")
|
154
|
+
|
155
|
+
# Try to publish the message
|
156
|
+
begin
|
157
|
+
publish_message(message_instance)
|
158
|
+
logger.info("Successfully published robbery report message")
|
159
|
+
break
|
160
|
+
rescue => e
|
161
|
+
retry_count += 1
|
162
|
+
error_msg = e.message
|
163
|
+
logger.warn("Publishing failed (attempt #{retry_count}): #{error_msg}")
|
164
|
+
|
165
|
+
# Parse validation error for specific property and valid values
|
166
|
+
validation_error_details = parse_validation_error(error_msg)
|
167
|
+
|
168
|
+
if validation_error_details
|
169
|
+
# Build detailed error context for AI
|
170
|
+
validation_errors = [
|
171
|
+
"Property '#{validation_error_details[:property]}' has invalid value.",
|
172
|
+
"Error: #{validation_error_details[:message]}",
|
173
|
+
validation_error_details[:valid_values] ? "Valid values are: #{validation_error_details[:valid_values]}" : nil
|
174
|
+
].compact
|
175
|
+
|
176
|
+
if retry_count < max_retries && @ai_available
|
177
|
+
logger.info("Attempting to fix validation error with AI assistance")
|
178
|
+
logger.info("Error details: property=#{validation_error_details[:property]}, valid_values=#{validation_error_details[:valid_values]}")
|
179
|
+
# Continue to next iteration to regenerate with error context
|
180
|
+
else
|
181
|
+
logger.error("Max retries reached or AI unavailable. Cannot fix validation error.")
|
182
|
+
raise
|
183
|
+
end
|
184
|
+
else
|
185
|
+
# Non-validation error, re-raise
|
186
|
+
logger.error("Non-validation error encountered: #{error_msg}")
|
187
|
+
raise
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
message_instance
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
def run_continuous(interval_seconds = 15)
|
197
|
+
puts "👁️ Smart Visitor - AI-Powered Observation Reporting System"
|
198
|
+
puts " Continuously observing the city and reporting incidents"
|
199
|
+
puts " Generating new observations every #{interval_seconds} seconds"
|
200
|
+
puts " Press Ctrl+C to stop\n\n"
|
201
|
+
|
202
|
+
observation_count = 0
|
203
|
+
|
204
|
+
# Set up signal handler for graceful shutdown
|
205
|
+
Signal.trap('INT') do
|
206
|
+
puts "\n\n👋 Visitor signing off after #{observation_count} observations."
|
207
|
+
logger.info("Visitor stopped after #{observation_count} observations")
|
208
|
+
exit(0)
|
209
|
+
end
|
210
|
+
|
211
|
+
Signal.trap('TERM') do
|
212
|
+
puts "\n👋 Visitor terminated after #{observation_count} observations."
|
213
|
+
exit(0)
|
214
|
+
end
|
215
|
+
|
216
|
+
loop do
|
217
|
+
observation_count += 1
|
218
|
+
scenario = generate_observation_scenario
|
219
|
+
|
220
|
+
puts "\n" + "="*60
|
221
|
+
puts "📍 OBSERVATION ##{observation_count} - #{Time.now.strftime('%H:%M:%S')}"
|
222
|
+
puts " Type: #{scenario[:type].upcase}"
|
223
|
+
puts " What I see: #{scenario[:description]}"
|
224
|
+
puts " Reporting to emergency services..."
|
225
|
+
|
226
|
+
begin
|
227
|
+
message = report_observation(scenario)
|
228
|
+
|
229
|
+
if message
|
230
|
+
puts " ✅ Report sent via #{message.class.to_s.split('::').last}"
|
231
|
+
logger.info("Observation ##{observation_count} reported successfully via #{message.class}")
|
232
|
+
else
|
233
|
+
puts " ⚠️ Unable to send report"
|
234
|
+
logger.warn("Observation ##{observation_count} could not be reported")
|
235
|
+
end
|
236
|
+
rescue => e
|
237
|
+
puts " ❌ Error: #{e.message}"
|
238
|
+
logger.error("Error reporting observation ##{observation_count}: #{e.message}")
|
239
|
+
end
|
240
|
+
|
241
|
+
puts " 💤 Waiting #{interval_seconds} seconds before next observation..."
|
242
|
+
sleep(interval_seconds)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
private
|
247
|
+
|
248
|
+
def parse_validation_error(error_message)
|
249
|
+
# Parse validation errors like:
|
250
|
+
# "Messages::Emergency911Message#emergency_type: Emergency type must be one of: fire, medical, crime, accident, hazmat, rescue, other"
|
251
|
+
# "Messages::SilentAlarmMessage#alarm_type: Alarm type must be: robbery, vault_breach, suspicious_activity"
|
252
|
+
|
253
|
+
# Try to match the pattern: ClassName#property: message
|
254
|
+
if error_message =~ /Messages::(\w+)#(\w+):\s*(.+)/
|
255
|
+
class_name = $1
|
256
|
+
property = $2
|
257
|
+
message = $3
|
258
|
+
|
259
|
+
# Extract valid values if present
|
260
|
+
valid_values = nil
|
261
|
+
if message =~ /must be(?:\s+one\s+of)?:\s*(.+)/i
|
262
|
+
values_str = $1.strip
|
263
|
+
# Clean up the values string and split
|
264
|
+
valid_values = values_str.split(/,\s*/).map(&:strip)
|
265
|
+
end
|
266
|
+
|
267
|
+
return {
|
268
|
+
class_name: class_name,
|
269
|
+
property: property,
|
270
|
+
message: message,
|
271
|
+
valid_values: valid_values
|
272
|
+
}
|
273
|
+
end
|
274
|
+
|
275
|
+
# Try alternate format for required properties
|
276
|
+
if error_message =~ /property\s+'(\w+)'\s+is\s+required/i
|
277
|
+
return {
|
278
|
+
property: $1,
|
279
|
+
message: error_message,
|
280
|
+
valid_values: nil
|
281
|
+
}
|
282
|
+
end
|
283
|
+
|
284
|
+
# Check if it's a validation error even if we can't parse details
|
285
|
+
if error_message.include?("ValidationError") || error_message.include?("must be") || error_message.include?("required")
|
286
|
+
return {
|
287
|
+
property: "unknown",
|
288
|
+
message: error_message,
|
289
|
+
valid_values: nil
|
290
|
+
}
|
291
|
+
end
|
292
|
+
|
293
|
+
nil
|
294
|
+
end
|
295
|
+
|
296
|
+
def collect_message_descriptions
|
297
|
+
logger.info("Scanning Messages module for available message classes")
|
298
|
+
|
299
|
+
message_descriptions = {}
|
300
|
+
|
301
|
+
# Get all message classes from the Messages module
|
302
|
+
Messages.constants.each do |const_name|
|
303
|
+
const = Messages.const_get(const_name)
|
304
|
+
|
305
|
+
# Check if it's a SmartMessage class
|
306
|
+
if const.is_a?(Class) && const < SmartMessage::Base
|
307
|
+
description = const.respond_to?(:description) ? const.description : "No description available"
|
308
|
+
message_descriptions[const_name.to_s] = {
|
309
|
+
class: const,
|
310
|
+
description: description
|
311
|
+
}
|
312
|
+
logger.info("Found message class: #{const_name} - #{description[0..100]}...")
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
message_descriptions
|
317
|
+
end
|
318
|
+
|
319
|
+
def ask_ai_for_message_selection(message_descriptions, scenario = nil)
|
320
|
+
logger.info("Asking AI to select appropriate message type for robbery reporting")
|
321
|
+
|
322
|
+
if @ai_available
|
323
|
+
# Build prompt for AI
|
324
|
+
descriptions_text = message_descriptions.map do |class_name, info|
|
325
|
+
"#{class_name}: #{info[:description]}"
|
326
|
+
end.join("\n\n")
|
327
|
+
|
328
|
+
scenario_context = scenario ? scenario[:context] : "A visitor witnessed a robbery at a local business."
|
329
|
+
|
330
|
+
prompt = <<~PROMPT
|
331
|
+
You are helping a visitor to a city report an incident they witnessed.
|
332
|
+
|
333
|
+
Scenario: #{scenario_context}
|
334
|
+
|
335
|
+
Below are the available message types in the city's emergency communication system:
|
336
|
+
|
337
|
+
#{descriptions_text}
|
338
|
+
|
339
|
+
Based on this scenario, which message type would be most appropriate for reporting this incident?
|
340
|
+
Note: For a visitor witnessing an incident, Emergency911Message is usually most appropriate.
|
341
|
+
|
342
|
+
Please respond with ONLY the exact class name (e.g., "Emergency911Message") - no additional text or explanation.
|
343
|
+
PROMPT
|
344
|
+
|
345
|
+
logger.info("Sending prompt to AI for message type selection")
|
346
|
+
logger.info("=== AI PROMPT ===\n#{prompt}\n=== END PROMPT ===")
|
347
|
+
begin
|
348
|
+
response = @llm.ask(prompt)
|
349
|
+
logger.info("=== AI RESPONSE ===\n#{response}\n=== END RESPONSE ===")
|
350
|
+
selected_class_name = response.content.strip
|
351
|
+
|
352
|
+
logger.info("AI response processed: #{selected_class_name}")
|
353
|
+
|
354
|
+
# Validate the AI's selection
|
355
|
+
if message_descriptions[selected_class_name]
|
356
|
+
selected_class = message_descriptions[selected_class_name][:class]
|
357
|
+
logger.info("AI selection validated: #{selected_class}")
|
358
|
+
return selected_class
|
359
|
+
end
|
360
|
+
rescue => e
|
361
|
+
logger.error("AI request failed: #{e.message}")
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
# Fallback logic when AI is not available or fails
|
366
|
+
logger.info("Using fallback message selection logic")
|
367
|
+
|
368
|
+
# Look for Emergency911Message first (most appropriate for general robbery reporting by witnesses)
|
369
|
+
if message_descriptions['Emergency911Message']
|
370
|
+
logger.info("Selected Emergency911Message for robbery reporting")
|
371
|
+
return message_descriptions['Emergency911Message'][:class]
|
372
|
+
end
|
373
|
+
|
374
|
+
# If no Emergency911Message, look for SilentAlarmMessage (for bank-specific robberies)
|
375
|
+
if message_descriptions['SilentAlarmMessage']
|
376
|
+
logger.info("Selected SilentAlarmMessage as fallback for robbery reporting")
|
377
|
+
return message_descriptions['SilentAlarmMessage'][:class]
|
378
|
+
end
|
379
|
+
|
380
|
+
# If neither, find any message with "alarm" or "emergency" in the name
|
381
|
+
emergency_message = message_descriptions.find { |name, _| name.downcase.include?('emergency') || name.downcase.include?('911') }
|
382
|
+
if emergency_message
|
383
|
+
logger.info("Selected #{emergency_message[0]} as emergency message")
|
384
|
+
return emergency_message[1][:class]
|
385
|
+
end
|
386
|
+
|
387
|
+
# Last resort: use first available message
|
388
|
+
first_message = message_descriptions.first
|
389
|
+
if first_message
|
390
|
+
logger.info("Using first available message type: #{first_message[0]}")
|
391
|
+
return first_message[1][:class]
|
392
|
+
end
|
393
|
+
|
394
|
+
raise "No valid message type found for robbery reporting"
|
395
|
+
end
|
396
|
+
|
397
|
+
def collect_property_descriptions(message_class)
|
398
|
+
logger.info("Collecting property descriptions for #{message_class}")
|
399
|
+
|
400
|
+
# Get property descriptions and validation info
|
401
|
+
if message_class.respond_to?(:property_descriptions)
|
402
|
+
property_descriptions = message_class.property_descriptions
|
403
|
+
logger.info("Retrieved #{property_descriptions.size} property descriptions from class method")
|
404
|
+
|
405
|
+
# Enhance descriptions with validation constraints
|
406
|
+
enhanced_descriptions = {}
|
407
|
+
|
408
|
+
property_descriptions.each do |prop, desc|
|
409
|
+
enhanced_desc = desc.to_s
|
410
|
+
|
411
|
+
# Add validation info if available
|
412
|
+
if message_class.respond_to?(:property_validations) && message_class.property_validations[prop]
|
413
|
+
validation = message_class.property_validations[prop]
|
414
|
+
if validation[:validation_message]
|
415
|
+
enhanced_desc += " (#{validation[:validation_message]})"
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# Check for constant arrays that define valid values (e.g., VALID_ALARM_TYPES)
|
420
|
+
const_name = "VALID_#{prop.to_s.upcase}S"
|
421
|
+
if message_class.const_defined?(const_name)
|
422
|
+
valid_values = message_class.const_get(const_name)
|
423
|
+
enhanced_desc += " Valid values: #{valid_values.join(', ')}"
|
424
|
+
end
|
425
|
+
|
426
|
+
enhanced_descriptions[prop] = enhanced_desc
|
427
|
+
logger.info("Property: #{prop} - #{enhanced_desc}")
|
428
|
+
end
|
429
|
+
|
430
|
+
return enhanced_descriptions
|
431
|
+
else
|
432
|
+
logger.warn("Property descriptions method not available on #{message_class}")
|
433
|
+
return {}
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
437
|
+
def generate_message_instance(message_class, property_descriptions, validation_errors = [], scenario = nil)
|
438
|
+
logger.info("Asking AI to generate property values for #{message_class}")
|
439
|
+
|
440
|
+
property_values = nil
|
441
|
+
|
442
|
+
if @ai_available
|
443
|
+
# Build prompt for AI to generate property values
|
444
|
+
properties_text = property_descriptions.map do |prop, desc|
|
445
|
+
"#{prop}: #{desc}"
|
446
|
+
end.join("\n")
|
447
|
+
|
448
|
+
# Add error context if this is a retry
|
449
|
+
error_context = ""
|
450
|
+
if validation_errors.any?
|
451
|
+
error_context = <<~ERROR
|
452
|
+
|
453
|
+
⚠️ PREVIOUS ATTEMPT FAILED WITH VALIDATION ERRORS:
|
454
|
+
#{validation_errors.map { |e| " • #{e}" }.join("\n")}
|
455
|
+
|
456
|
+
REQUIRED FIX: You MUST correct these specific properties with valid values.
|
457
|
+
Only change the properties mentioned in the errors above.
|
458
|
+
Keep all other property values the same.
|
459
|
+
ERROR
|
460
|
+
end
|
461
|
+
|
462
|
+
scenario_context = scenario ? scenario[:context] : "A visitor witnessed a robbery at a local business."
|
463
|
+
scenario_desc = scenario ? scenario[:description] : "Witnessed armed robbery"
|
464
|
+
|
465
|
+
prompt = <<~PROMPT
|
466
|
+
You are helping generate an emergency report message for an incident a visitor witnessed.
|
467
|
+
|
468
|
+
Scenario: #{scenario_context}
|
469
|
+
|
470
|
+
Please provide values for the following properties of a #{message_class} message:
|
471
|
+
|
472
|
+
#{properties_text}
|
473
|
+
|
474
|
+
Context: #{scenario_desc}
|
475
|
+
The visitor is reporting this incident, so the 'from' field should be set to 'visitor'.
|
476
|
+
|
477
|
+
IMPORTANT: Some properties have validation constraints shown in parentheses or as "Valid values". You MUST use only the specified valid values for those properties.
|
478
|
+
#{error_context}
|
479
|
+
Please respond with a JSON object containing the property values. Use realistic values that make sense for a robbery report.
|
480
|
+
For timestamps, use the current date/time format in ISO 8601 format (YYYY-MM-DDTHH:MM:SSZ).
|
481
|
+
|
482
|
+
Example format:
|
483
|
+
{
|
484
|
+
"property1": "value1",
|
485
|
+
"property2": "value2"
|
486
|
+
}
|
487
|
+
PROMPT
|
488
|
+
|
489
|
+
logger.info("Sending property generation prompt to AI")
|
490
|
+
logger.info("=== AI PROPERTY PROMPT ===\n#{prompt}\n=== END PROMPT ===")
|
491
|
+
begin
|
492
|
+
response = @llm.ask(prompt)
|
493
|
+
logger.info("=== AI PROPERTY RESPONSE ===\n#{response}\n=== END RESPONSE ===")
|
494
|
+
|
495
|
+
# Parse AI response as JSON
|
496
|
+
property_values = JSON.parse(response.content)
|
497
|
+
logger.info("Successfully parsed AI response as JSON")
|
498
|
+
rescue JSON::ParserError => e
|
499
|
+
logger.error("Failed to parse AI response as JSON: #{e.message}")
|
500
|
+
property_values = nil
|
501
|
+
rescue => e
|
502
|
+
logger.error("AI request failed: #{e.message}")
|
503
|
+
property_values = nil
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
# Fallback to hardcoded values if AI failed
|
508
|
+
if property_values.nil?
|
509
|
+
logger.info("Using fallback property values")
|
510
|
+
property_values = generate_fallback_values(message_class, validation_errors)
|
511
|
+
end
|
512
|
+
|
513
|
+
# Ensure 'from' is set to 'visitor'
|
514
|
+
property_values['from'] = 'visitor'
|
515
|
+
|
516
|
+
# Create message instance using keyword arguments
|
517
|
+
begin
|
518
|
+
# Convert hash to keyword arguments
|
519
|
+
kwargs = property_values.transform_keys(&:to_sym)
|
520
|
+
message_instance = message_class.new(**kwargs)
|
521
|
+
logger.info("Successfully created message instance")
|
522
|
+
return message_instance
|
523
|
+
rescue => e
|
524
|
+
logger.error("Failed to create message instance: #{e.message}")
|
525
|
+
# Try with fallback values
|
526
|
+
fallback_values = generate_fallback_values(message_class)
|
527
|
+
fallback_values['from'] = 'visitor'
|
528
|
+
fallback_kwargs = fallback_values.transform_keys(&:to_sym)
|
529
|
+
message_instance = message_class.new(**fallback_kwargs)
|
530
|
+
logger.info("Created message instance with fallback values")
|
531
|
+
return message_instance
|
532
|
+
end
|
533
|
+
end
|
534
|
+
|
535
|
+
def generate_fallback_values(message_class, validation_errors = [])
|
536
|
+
logger.info("Generating fallback values for #{message_class}")
|
537
|
+
|
538
|
+
# Basic fallback values for common message types
|
539
|
+
case message_class.to_s
|
540
|
+
when /Emergency911Message/
|
541
|
+
{
|
542
|
+
'caller_name' => 'Anonymous Visitor',
|
543
|
+
'caller_phone' => '555-0123',
|
544
|
+
'caller_location' => '456 Oak Street',
|
545
|
+
'emergency_type' => 'crime',
|
546
|
+
'description' => 'Witnessed armed robbery at local store',
|
547
|
+
'severity' => 'high',
|
548
|
+
'injuries_reported' => false,
|
549
|
+
'fire_involved' => false,
|
550
|
+
'weapons_involved' => true,
|
551
|
+
'suspects_on_scene' => true,
|
552
|
+
'timestamp' => Time.now.iso8601,
|
553
|
+
'from' => 'visitor',
|
554
|
+
'to' => '911'
|
555
|
+
}
|
556
|
+
when /SilentAlarmMessage/
|
557
|
+
{
|
558
|
+
'bank_name' => 'First National Bank',
|
559
|
+
'location' => '123 Main Street',
|
560
|
+
'alarm_type' => 'robbery',
|
561
|
+
'timestamp' => Time.now.strftime('%Y-%m-%d %H:%M:%S'),
|
562
|
+
'severity' => 'high',
|
563
|
+
'details' => 'Visitor reported armed robbery in progress',
|
564
|
+
'from' => 'visitor'
|
565
|
+
}
|
566
|
+
when /PoliceDispatchMessage/
|
567
|
+
{
|
568
|
+
'dispatch_id' => SecureRandom.hex(4),
|
569
|
+
'units_assigned' => ['Unit-101', 'Unit-102'],
|
570
|
+
'location' => '123 Main Street',
|
571
|
+
'incident_type' => 'robbery',
|
572
|
+
'priority' => 'emergency',
|
573
|
+
'estimated_arrival' => '3 minutes',
|
574
|
+
'timestamp' => Time.now.strftime('%Y-%m-%d %H:%M:%S'),
|
575
|
+
'from' => 'visitor'
|
576
|
+
}
|
577
|
+
else
|
578
|
+
{
|
579
|
+
'timestamp' => Time.now.strftime('%Y-%m-%d %H:%M:%S'),
|
580
|
+
'details' => 'Visitor reported robbery incident',
|
581
|
+
'from' => 'visitor'
|
582
|
+
}
|
583
|
+
end
|
584
|
+
end
|
585
|
+
|
586
|
+
def publish_message(message_instance)
|
587
|
+
logger.info("Publishing message: #{message_instance.class}")
|
588
|
+
|
589
|
+
begin
|
590
|
+
message_instance.publish
|
591
|
+
logger.info("Message published successfully to Redis transport")
|
592
|
+
|
593
|
+
# Log the message content for debugging
|
594
|
+
logger.info("Published message content: #{message_instance.to_h}")
|
595
|
+
rescue => e
|
596
|
+
logger.error("Failed to publish message: #{e.message}")
|
597
|
+
raise
|
598
|
+
end
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
# Main execution
|
603
|
+
if __FILE__ == $0
|
604
|
+
begin
|
605
|
+
visitor = Visitor.new
|
606
|
+
|
607
|
+
# Check for command-line arguments
|
608
|
+
if ARGV[0] == '--once' || ARGV[0] == '-o'
|
609
|
+
# Single observation mode
|
610
|
+
puts "🎯 Smart Visitor - Single Observation Mode"
|
611
|
+
scenario = visitor.generate_observation_scenario
|
612
|
+
puts " Observing: #{scenario[:description]}"
|
613
|
+
|
614
|
+
message = visitor.report_observation(scenario)
|
615
|
+
|
616
|
+
if message
|
617
|
+
puts "\n✅ Report successfully generated and published!"
|
618
|
+
puts " Message Type: #{message.class}"
|
619
|
+
puts " Check visitor.log for details"
|
620
|
+
end
|
621
|
+
else
|
622
|
+
# Continuous mode (default)
|
623
|
+
interval = ARGV[0]&.to_i || 15
|
624
|
+
visitor.run_continuous(interval)
|
625
|
+
end
|
626
|
+
rescue => e
|
627
|
+
puts "\n❌ Error in visitor program: #{e.message}"
|
628
|
+
puts " Check visitor.log for details"
|
629
|
+
exit(1)
|
630
|
+
end
|
631
|
+
end
|