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.
Files changed (169) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/deploy-github-pages.yml +38 -0
  3. data/.gitignore +5 -0
  4. data/CHANGELOG.md +30 -0
  5. data/Gemfile.lock +35 -4
  6. data/README.md +169 -71
  7. data/Rakefile +29 -4
  8. data/docs/assets/images/ddq_architecture.svg +130 -0
  9. data/docs/assets/images/dlq_architecture.svg +115 -0
  10. data/docs/assets/images/enhanced-dual-publishing.svg +136 -0
  11. data/docs/assets/images/enhanced-fluent-api.svg +149 -0
  12. data/docs/assets/images/enhanced-microservices-routing.svg +115 -0
  13. data/docs/assets/images/enhanced-pattern-matching.svg +107 -0
  14. data/docs/assets/images/fluent-api-demo.svg +59 -0
  15. data/docs/assets/images/performance-comparison.svg +161 -0
  16. data/docs/assets/images/redis-basic-architecture.svg +53 -0
  17. data/docs/assets/images/redis-enhanced-architecture.svg +88 -0
  18. data/docs/assets/images/redis-queue-architecture.svg +101 -0
  19. data/docs/assets/images/smart_message.jpg +0 -0
  20. data/docs/assets/images/smart_message_walking.jpg +0 -0
  21. data/docs/assets/images/smartmessage_architecture_overview.svg +173 -0
  22. data/docs/assets/images/transport-comparison-matrix.svg +171 -0
  23. data/docs/assets/javascripts/mathjax.js +17 -0
  24. data/docs/assets/stylesheets/extra.css +51 -0
  25. data/docs/{addressing.md → core-concepts/addressing.md} +5 -7
  26. data/docs/{architecture.md → core-concepts/architecture.md} +78 -138
  27. data/docs/{dispatcher.md → core-concepts/dispatcher.md} +21 -21
  28. data/docs/{message_filtering.md → core-concepts/message-filtering.md} +2 -3
  29. data/docs/{message_processing.md → core-concepts/message-processing.md} +17 -17
  30. data/docs/{troubleshooting.md → development/troubleshooting.md} +7 -7
  31. data/docs/{examples.md → getting-started/examples.md} +115 -89
  32. data/docs/{getting-started.md → getting-started/quick-start.md} +47 -18
  33. data/docs/guides/redis-queue-getting-started.md +697 -0
  34. data/docs/guides/redis-queue-patterns.md +889 -0
  35. data/docs/guides/redis-queue-production.md +1091 -0
  36. data/docs/index.md +64 -0
  37. data/docs/{dead_letter_queue.md → reference/dead-letter-queue.md} +2 -3
  38. data/docs/{logging.md → reference/logging.md} +1 -1
  39. data/docs/{message_deduplication.md → reference/message-deduplication.md} +1 -0
  40. data/docs/{proc_handlers_summary.md → reference/proc-handlers.md} +7 -6
  41. data/docs/{serializers.md → reference/serializers.md} +3 -5
  42. data/docs/{transports.md → reference/transports.md} +133 -11
  43. data/docs/transports/memory-transport.md +374 -0
  44. data/docs/transports/redis-enhanced-transport.md +524 -0
  45. data/docs/transports/redis-queue-transport.md +1304 -0
  46. data/docs/transports/redis-transport-comparison.md +496 -0
  47. data/docs/transports/redis-transport.md +509 -0
  48. data/examples/README.md +98 -5
  49. data/examples/city_scenario/911_emergency_call_flow.svg +99 -0
  50. data/examples/city_scenario/README.md +515 -0
  51. data/examples/city_scenario/ai_visitor_intelligence_flow.svg +108 -0
  52. data/examples/city_scenario/citizen.rb +195 -0
  53. data/examples/city_scenario/city_diagram.svg +125 -0
  54. data/examples/city_scenario/common/health_monitor.rb +80 -0
  55. data/examples/city_scenario/common/logger.rb +30 -0
  56. data/examples/city_scenario/emergency_dispatch_center.rb +270 -0
  57. data/examples/city_scenario/fire_department.rb +446 -0
  58. data/examples/city_scenario/fire_emergency_flow.svg +95 -0
  59. data/examples/city_scenario/health_department.rb +100 -0
  60. data/examples/city_scenario/health_monitoring_system.svg +130 -0
  61. data/examples/city_scenario/house.rb +244 -0
  62. data/examples/city_scenario/local_bank.rb +217 -0
  63. data/examples/city_scenario/messages/emergency_911_message.rb +81 -0
  64. data/examples/city_scenario/messages/emergency_resolved_message.rb +43 -0
  65. data/examples/city_scenario/messages/fire_dispatch_message.rb +43 -0
  66. data/examples/city_scenario/messages/fire_emergency_message.rb +45 -0
  67. data/examples/city_scenario/messages/health_check_message.rb +22 -0
  68. data/examples/city_scenario/messages/health_status_message.rb +35 -0
  69. data/examples/city_scenario/messages/police_dispatch_message.rb +46 -0
  70. data/examples/city_scenario/messages/silent_alarm_message.rb +38 -0
  71. data/examples/city_scenario/police_department.rb +316 -0
  72. data/examples/city_scenario/redis_monitor.rb +129 -0
  73. data/examples/city_scenario/redis_stats.rb +743 -0
  74. data/examples/city_scenario/room_for_improvement.md +240 -0
  75. data/examples/city_scenario/security_emergency_flow.svg +95 -0
  76. data/examples/city_scenario/service_internal_architecture.svg +154 -0
  77. data/examples/city_scenario/smart_message_ai_agent.rb +364 -0
  78. data/examples/city_scenario/start_demo.sh +236 -0
  79. data/examples/city_scenario/stop_demo.sh +106 -0
  80. data/examples/city_scenario/visitor.rb +631 -0
  81. data/examples/{10_message_deduplication.rb → memory/01_message_deduplication_demo.rb} +1 -1
  82. data/examples/{09_dead_letter_queue_demo.rb → memory/02_dead_letter_queue_demo.rb} +13 -40
  83. data/examples/{01_point_to_point_orders.rb → memory/03_point_to_point_orders.rb} +1 -1
  84. data/examples/{02_publish_subscribe_events.rb → memory/04_publish_subscribe_events.rb} +2 -2
  85. data/examples/{03_many_to_many_chat.rb → memory/05_many_to_many_chat.rb} +4 -4
  86. data/examples/{show_me.rb → memory/06_pretty_print_demo.rb} +1 -1
  87. data/examples/{05_proc_handlers.rb → memory/07_proc_handlers_demo.rb} +2 -2
  88. data/examples/{06_custom_logger_example.rb → memory/08_custom_logger_demo.rb} +17 -14
  89. data/examples/{07_error_handling_scenarios.rb → memory/09_error_handling_demo.rb} +4 -4
  90. data/examples/{08_entity_addressing_basic.rb → memory/10_entity_addressing_basic.rb} +8 -8
  91. data/examples/{08_entity_addressing_with_filtering.rb → memory/11_entity_addressing_with_filtering.rb} +6 -6
  92. data/examples/{09_regex_filtering_microservices.rb → memory/12_regex_filtering_microservices.rb} +2 -2
  93. data/examples/{10_header_block_configuration.rb → memory/13_header_block_configuration.rb} +6 -6
  94. data/examples/{11_global_configuration_example.rb → memory/14_global_configuration_demo.rb} +19 -8
  95. data/examples/{show_logger.rb → memory/15_logger_demo.rb} +1 -1
  96. data/examples/memory/README.md +163 -0
  97. data/examples/memory/memory_transport_architecture.svg +90 -0
  98. data/examples/memory/point_to_point_pattern.svg +94 -0
  99. data/examples/memory/publish_subscribe_pattern.svg +125 -0
  100. data/examples/{04_redis_smart_home_iot.rb → redis/01_smart_home_iot_demo.rb} +5 -5
  101. data/examples/redis/README.md +230 -0
  102. data/examples/redis/alert_system_flow.svg +127 -0
  103. data/examples/redis/dashboard_status_flow.svg +107 -0
  104. data/examples/redis/device_command_flow.svg +113 -0
  105. data/examples/redis/redis_transport_architecture.svg +115 -0
  106. data/examples/{smart_home_iot_dataflow.md → redis/smart_home_iot_dataflow.md} +4 -116
  107. data/examples/redis/smart_home_system_architecture.svg +133 -0
  108. data/examples/redis_enhanced/README.md +319 -0
  109. data/examples/redis_enhanced/enhanced_01_basic_patterns.rb +233 -0
  110. data/examples/redis_enhanced/enhanced_02_fluent_api.rb +331 -0
  111. data/examples/redis_enhanced/enhanced_03_dual_publishing.rb +281 -0
  112. data/examples/redis_enhanced/enhanced_04_advanced_routing.rb +419 -0
  113. data/examples/redis_queue/01_basic_messaging.rb +221 -0
  114. data/examples/redis_queue/01_comprehensive_examples.rb +508 -0
  115. data/examples/redis_queue/02_pattern_routing.rb +405 -0
  116. data/examples/redis_queue/03_fluent_api.rb +422 -0
  117. data/examples/redis_queue/04_load_balancing.rb +486 -0
  118. data/examples/redis_queue/05_microservices.rb +735 -0
  119. data/examples/redis_queue/06_emergency_alerts.rb +777 -0
  120. data/examples/redis_queue/07_queue_management.rb +587 -0
  121. data/examples/redis_queue/README.md +366 -0
  122. data/examples/redis_queue/enhanced_01_basic_patterns.rb +233 -0
  123. data/examples/redis_queue/enhanced_02_fluent_api.rb +331 -0
  124. data/examples/redis_queue/enhanced_03_dual_publishing.rb +281 -0
  125. data/examples/redis_queue/enhanced_04_advanced_routing.rb +419 -0
  126. data/examples/redis_queue/redis_queue_architecture.svg +148 -0
  127. data/ideas/README.md +41 -0
  128. data/ideas/agents.md +1001 -0
  129. data/ideas/database_transport.md +980 -0
  130. data/ideas/improvement.md +359 -0
  131. data/ideas/meshage.md +1788 -0
  132. data/ideas/message_discovery.md +178 -0
  133. data/ideas/message_schema.md +1381 -0
  134. data/lib/smart_message/.idea/.gitignore +8 -0
  135. data/lib/smart_message/.idea/markdown.xml +6 -0
  136. data/lib/smart_message/.idea/misc.xml +4 -0
  137. data/lib/smart_message/.idea/modules.xml +8 -0
  138. data/lib/smart_message/.idea/smart_message.iml +16 -0
  139. data/lib/smart_message/.idea/vcs.xml +6 -0
  140. data/lib/smart_message/addressing.rb +15 -0
  141. data/lib/smart_message/base.rb +0 -2
  142. data/lib/smart_message/configuration.rb +1 -1
  143. data/lib/smart_message/logger.rb +15 -4
  144. data/lib/smart_message/plugins.rb +5 -2
  145. data/lib/smart_message/serializer.rb +14 -0
  146. data/lib/smart_message/transport/redis_enhanced_transport.rb +399 -0
  147. data/lib/smart_message/transport/redis_queue_transport.rb +555 -0
  148. data/lib/smart_message/transport/registry.rb +1 -0
  149. data/lib/smart_message/transport.rb +34 -1
  150. data/lib/smart_message/version.rb +1 -1
  151. data/lib/smart_message.rb +5 -52
  152. data/mkdocs.yml +184 -0
  153. data/p2p_plan.md +326 -0
  154. data/p2p_roadmap.md +287 -0
  155. data/smart_message.gemspec +2 -0
  156. data/smart_message.svg +51 -0
  157. metadata +170 -44
  158. data/docs/README.md +0 -57
  159. data/examples/dead_letters.jsonl +0 -12
  160. data/examples/temp.txt +0 -94
  161. data/examples/tmux_chat/README.md +0 -283
  162. data/examples/tmux_chat/bot_agent.rb +0 -278
  163. data/examples/tmux_chat/human_agent.rb +0 -199
  164. data/examples/tmux_chat/room_monitor.rb +0 -160
  165. data/examples/tmux_chat/shared_chat_system.rb +0 -328
  166. data/examples/tmux_chat/start_chat_demo.sh +0 -190
  167. data/examples/tmux_chat/stop_chat_demo.sh +0 -22
  168. /data/docs/{properties.md → core-concepts/properties.md} +0 -0
  169. /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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
2
  # examples/10_message_deduplication.rb
3
3
 
4
- require_relative '../lib/smart_message'
4
+ require_relative '../../lib/smart_message'
5
5
 
6
6
  # Example demonstrating message deduplication with DDQ (Deduplication Queue)
7
7