legate 0.1.0

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 (317) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +345 -0
  4. data/bin/legate +13 -0
  5. data/examples/00_quickstart.rb +51 -0
  6. data/examples/01_simple_agent.rb +105 -0
  7. data/examples/02_multi_tool_agent.rb +140 -0
  8. data/examples/03_custom_tool.rb +93 -0
  9. data/examples/04_agent_instructions.rb +84 -0
  10. data/examples/05_state_and_sessions.rb +91 -0
  11. data/examples/06_callbacks.rb +186 -0
  12. data/examples/07_async_jobs.rb +112 -0
  13. data/examples/08_loop_agent.rb +197 -0
  14. data/examples/09_sequential_workflow.rb +40 -0
  15. data/examples/10_parallel_workflow.rb +34 -0
  16. data/examples/11_agent_delegation.rb +24 -0
  17. data/examples/12_http_client_tool.rb +156 -0
  18. data/examples/13_authentication.rb +220 -0
  19. data/examples/14_mcp_client.rb +154 -0
  20. data/examples/15_mcp_server.rb +79 -0
  21. data/examples/16_webhooks.rb +91 -0
  22. data/examples/README_sequential_agents.md +164 -0
  23. data/examples/advanced/auth/cookie_auth_tool.rb +146 -0
  24. data/examples/advanced/auth/custom_auth_flows_example.rb +626 -0
  25. data/examples/advanced/auth/excon_middleware.rb +317 -0
  26. data/examples/advanced/auth/excon_middleware_auth.rb +399 -0
  27. data/examples/advanced/auth/fiber_auth_example.rb +281 -0
  28. data/examples/advanced/auth/fiber_oidc_example.rb +403 -0
  29. data/examples/advanced/auth/httpbin_bearer_tool.rb +159 -0
  30. data/examples/advanced/auth/oauth2_auth.rb +419 -0
  31. data/examples/advanced/auth/oidc_auth.rb +514 -0
  32. data/examples/advanced/auth/openweather_api.rb +251 -0
  33. data/examples/advanced/auth/openweather_tool.rb +153 -0
  34. data/examples/advanced/auth/query_param_middleware_test.rb +138 -0
  35. data/examples/advanced/auth/service_account.rb +135 -0
  36. data/examples/advanced/auth/test_with_httpbin.rb +202 -0
  37. data/examples/advanced/auth/token_lifecycle_example.rb +428 -0
  38. data/examples/advanced/callback_monitoring.rb +679 -0
  39. data/examples/advanced/mas/fixed_delegation_example.rb +191 -0
  40. data/examples/advanced/mas/loop_workflow.rb +28 -0
  41. data/examples/advanced/mas/mock_planner.rb +77 -0
  42. data/examples/advanced/mas/proper_delegation_example.rb +276 -0
  43. data/examples/advanced/mcp/legate_mcp_server_resource_example.rb +182 -0
  44. data/examples/advanced/mcp/mcp_resource_server_example.rb +309 -0
  45. data/examples/advanced/mcp/mcp_server_async.rb +76 -0
  46. data/examples/advanced/mcp/mcp_server_async_tools.rb +122 -0
  47. data/examples/advanced/mcp/mcp_server_legate_agent.rb +95 -0
  48. data/examples/advanced/mcp/mcp_server_rack.rb +89 -0
  49. data/examples/advanced/random_calculator.rb +104 -0
  50. data/examples/advanced/sleep_agent.rb +153 -0
  51. data/examples/advanced/webhooks/webhook_e2e_runner.rb +110 -0
  52. data/examples/advanced/webhooks/webhook_receiver_agent.rb +58 -0
  53. data/examples/advanced/workflows/task_refinement_loop_agent.rb +278 -0
  54. data/examples/advanced/workflows/travel_planner_auto_sequential.rb +444 -0
  55. data/examples/advanced/workflows/travel_planner_parallel.rb +656 -0
  56. data/examples/advanced/workflows/travel_planner_sequential.rb +512 -0
  57. data/examples/tools/oauth2_example.rb +136 -0
  58. data/examples/tools/sleepy_tool.rb +42 -0
  59. data/lib/legate/activity_log.rb +71 -0
  60. data/lib/legate/agent.rb +959 -0
  61. data/lib/legate/agent_code_generator.rb +185 -0
  62. data/lib/legate/agent_definition.rb +812 -0
  63. data/lib/legate/agentic/decision.rb +49 -0
  64. data/lib/legate/agentic/loop.rb +134 -0
  65. data/lib/legate/agentic.rb +5 -0
  66. data/lib/legate/agents/loop_agent.rb +248 -0
  67. data/lib/legate/agents/parallel_agent.rb +163 -0
  68. data/lib/legate/agents/sequential_agent.rb +190 -0
  69. data/lib/legate/agents.rb +14 -0
  70. data/lib/legate/auth/config.rb +148 -0
  71. data/lib/legate/auth/coordinator.rb +218 -0
  72. data/lib/legate/auth/coordinators/oauth2_coordinator.rb +99 -0
  73. data/lib/legate/auth/coordinators/oidc_coordinator.rb +68 -0
  74. data/lib/legate/auth/coordinators/service_account_coordinator.rb +122 -0
  75. data/lib/legate/auth/credential.rb +157 -0
  76. data/lib/legate/auth/encryption.rb +108 -0
  77. data/lib/legate/auth/error.rb +94 -0
  78. data/lib/legate/auth/exchanged_credential.rb +180 -0
  79. data/lib/legate/auth/excon_middleware.rb +285 -0
  80. data/lib/legate/auth/http_client_utils.rb +364 -0
  81. data/lib/legate/auth/manager.rb +531 -0
  82. data/lib/legate/auth/manager_store.rb +394 -0
  83. data/lib/legate/auth/middleware_factory.rb +290 -0
  84. data/lib/legate/auth/runner.rb +279 -0
  85. data/lib/legate/auth/scheme.rb +125 -0
  86. data/lib/legate/auth/schemes/api_key.rb +212 -0
  87. data/lib/legate/auth/schemes/google_service_account.rb +108 -0
  88. data/lib/legate/auth/schemes/http_bearer.rb +98 -0
  89. data/lib/legate/auth/schemes/oauth2.rb +396 -0
  90. data/lib/legate/auth/schemes/openid_connect.rb +346 -0
  91. data/lib/legate/auth/schemes/service_account.rb +388 -0
  92. data/lib/legate/auth/schemes.rb +40 -0
  93. data/lib/legate/auth/token_manager.rb +362 -0
  94. data/lib/legate/auth/token_store.rb +86 -0
  95. data/lib/legate/auth/tool_context_extension.rb +97 -0
  96. data/lib/legate/auth/tool_integration.rb +188 -0
  97. data/lib/legate/auth/url_guard.rb +81 -0
  98. data/lib/legate/auth.rb +453 -0
  99. data/lib/legate/callbacks/callback_context.rb +71 -0
  100. data/lib/legate/cli/agent_commands.rb +950 -0
  101. data/lib/legate/cli/auth_commands.rb +520 -0
  102. data/lib/legate/cli/base_command.rb +24 -0
  103. data/lib/legate/cli/deployment_commands.rb +934 -0
  104. data/lib/legate/cli/output_helper.rb +108 -0
  105. data/lib/legate/cli/session_commands.rb +138 -0
  106. data/lib/legate/cli/skaffold_commands.rb +223 -0
  107. data/lib/legate/cli/tool_commands.rb +261 -0
  108. data/lib/legate/cli/web_commands.rb +182 -0
  109. data/lib/legate/cli.rb +40 -0
  110. data/lib/legate/configuration/webhooks.rb +113 -0
  111. data/lib/legate/configuration.rb +39 -0
  112. data/lib/legate/definition_store.rb +23 -0
  113. data/lib/legate/errors.rb +118 -0
  114. data/lib/legate/event.rb +161 -0
  115. data/lib/legate/gemini_ai_beta_patch.rb +39 -0
  116. data/lib/legate/generators/agent_generator.rb +412 -0
  117. data/lib/legate/generators/code_validator.rb +48 -0
  118. data/lib/legate/generators/legate/install_generator.rb +35 -0
  119. data/lib/legate/generators/legate/templates/create_legate_tables.rb.tt +36 -0
  120. data/lib/legate/generators/legate/templates/initializer.rb +18 -0
  121. data/lib/legate/generators/runtime_tool_loader.rb +76 -0
  122. data/lib/legate/generators/tool_generator.rb +408 -0
  123. data/lib/legate/generators.rb +11 -0
  124. data/lib/legate/global_definition_registry.rb +506 -0
  125. data/lib/legate/global_tool_manager.rb +135 -0
  126. data/lib/legate/llm/adapter.rb +69 -0
  127. data/lib/legate/llm/gemini.rb +172 -0
  128. data/lib/legate/llm/ollama.rb +80 -0
  129. data/lib/legate/llm.rb +34 -0
  130. data/lib/legate/mcp/client.rb +320 -0
  131. data/lib/legate/mcp/connection/sse.rb +292 -0
  132. data/lib/legate/mcp/connection/stdio.rb +273 -0
  133. data/lib/legate/mcp/connection_manager.rb +103 -0
  134. data/lib/legate/mcp/server/legate_agent_adapter.rb +170 -0
  135. data/lib/legate/mcp/server/legate_direct_agent_adapter.rb +140 -0
  136. data/lib/legate/mcp/server/legate_tool_adapter.rb +119 -0
  137. data/lib/legate/mcp/tool_wrapper.rb +138 -0
  138. data/lib/legate/mcp/util/schema_converter.rb +134 -0
  139. data/lib/legate/mcp.rb +23 -0
  140. data/lib/legate/plan_executor.rb +375 -0
  141. data/lib/legate/planner.rb +839 -0
  142. data/lib/legate/rails/railtie.rb +43 -0
  143. data/lib/legate/rails.rb +9 -0
  144. data/lib/legate/redaction.rb +32 -0
  145. data/lib/legate/session.rb +299 -0
  146. data/lib/legate/session_service/active_record.rb +300 -0
  147. data/lib/legate/session_service/base.rb +68 -0
  148. data/lib/legate/session_service/event_broadcast.rb +74 -0
  149. data/lib/legate/session_service/in_memory.rb +188 -0
  150. data/lib/legate/tool/metadata_dsl.rb +122 -0
  151. data/lib/legate/tool.rb +276 -0
  152. data/lib/legate/tool_code_generator.rb +103 -0
  153. data/lib/legate/tool_context.rb +350 -0
  154. data/lib/legate/tool_loader.rb +39 -0
  155. data/lib/legate/tool_registry.rb +73 -0
  156. data/lib/legate/tool_result.rb +61 -0
  157. data/lib/legate/tools/agent_tool.rb +187 -0
  158. data/lib/legate/tools/base/http_client.rb +319 -0
  159. data/lib/legate/tools/base/safe_url.rb +56 -0
  160. data/lib/legate/tools/base_async_job_tool.rb +91 -0
  161. data/lib/legate/tools/calculator.rb +89 -0
  162. data/lib/legate/tools/cat_facts.rb +81 -0
  163. data/lib/legate/tools/check_job_status_tool.rb +48 -0
  164. data/lib/legate/tools/current_time_tool.rb +64 -0
  165. data/lib/legate/tools/echo.rb +43 -0
  166. data/lib/legate/tools/http_request_tool.rb +105 -0
  167. data/lib/legate/tools/random_number_tool.rb +64 -0
  168. data/lib/legate/tools/read_webpage_tool.rb +92 -0
  169. data/lib/legate/tools/sleepy_tool.rb +74 -0
  170. data/lib/legate/tools/webhook_tool.rb +146 -0
  171. data/lib/legate/version.rb +5 -0
  172. data/lib/legate/web/app.rb +984 -0
  173. data/lib/legate/web/public/css/main.css +4980 -0
  174. data/lib/legate/web/public/images/favicon-256.png +0 -0
  175. data/lib/legate/web/public/images/favicon-32.png +0 -0
  176. data/lib/legate/web/public/images/legate-logo-dark.png +0 -0
  177. data/lib/legate/web/public/images/legate-logo-light.png +0 -0
  178. data/lib/legate/web/public/js/legate.js +616 -0
  179. data/lib/legate/web/public/styles/main.scss +4402 -0
  180. data/lib/legate/web/routes/agent_authentication_routes.rb +530 -0
  181. data/lib/legate/web/routes/agent_definition_routes.rb +803 -0
  182. data/lib/legate/web/routes/agent_generator_routes.rb +80 -0
  183. data/lib/legate/web/routes/agent_interaction_routes.rb +734 -0
  184. data/lib/legate/web/routes/agent_runtime_routes.rb +323 -0
  185. data/lib/legate/web/routes/api_routes.rb +56 -0
  186. data/lib/legate/web/routes/authentication_routes.rb +1541 -0
  187. data/lib/legate/web/routes/core_routes.rb +111 -0
  188. data/lib/legate/web/routes/documentation_routes.rb +220 -0
  189. data/lib/legate/web/routes/tool_generator_routes.rb +81 -0
  190. data/lib/legate/web/routes/tools_ui_routes.rb +207 -0
  191. data/lib/legate/web/sass_compiler.rb +73 -0
  192. data/lib/legate/web/views/_active_session_info.slim +25 -0
  193. data/lib/legate/web/views/_activity_list.slim +55 -0
  194. data/lib/legate/web/views/_agent_card.slim +56 -0
  195. data/lib/legate/web/views/_agent_generator_modal.slim +382 -0
  196. data/lib/legate/web/views/_agent_status_controls.slim +71 -0
  197. data/lib/legate/web/views/_agent_tool_table.slim +74 -0
  198. data/lib/legate/web/views/_chat_message.slim +95 -0
  199. data/lib/legate/web/views/_display_agent_configuration.slim +26 -0
  200. data/lib/legate/web/views/_display_agent_description.slim +11 -0
  201. data/lib/legate/web/views/_display_agent_fallback.slim +15 -0
  202. data/lib/legate/web/views/_display_agent_hierarchy.slim +93 -0
  203. data/lib/legate/web/views/_display_agent_instruction.slim +17 -0
  204. data/lib/legate/web/views/_display_agent_mcp.slim +13 -0
  205. data/lib/legate/web/views/_display_agent_model.slim +17 -0
  206. data/lib/legate/web/views/_display_agent_name.slim +42 -0
  207. data/lib/legate/web/views/_display_agent_output_key.slim +26 -0
  208. data/lib/legate/web/views/_display_agent_type.slim +65 -0
  209. data/lib/legate/web/views/_edit_agent_configuration.slim +74 -0
  210. data/lib/legate/web/views/_edit_agent_description.slim +16 -0
  211. data/lib/legate/web/views/_edit_agent_fallback.slim +25 -0
  212. data/lib/legate/web/views/_edit_agent_hierarchy.slim +98 -0
  213. data/lib/legate/web/views/_edit_agent_instruction.slim +49 -0
  214. data/lib/legate/web/views/_edit_agent_mcp.slim +33 -0
  215. data/lib/legate/web/views/_edit_agent_model.slim +23 -0
  216. data/lib/legate/web/views/_edit_agent_output_key.slim +36 -0
  217. data/lib/legate/web/views/_edit_agent_tools.slim +40 -0
  218. data/lib/legate/web/views/_edit_agent_type.slim +67 -0
  219. data/lib/legate/web/views/_session_error.slim +4 -0
  220. data/lib/legate/web/views/_skeleton.slim +69 -0
  221. data/lib/legate/web/views/_tool_card.slim +9 -0
  222. data/lib/legate/web/views/_tool_generator_modal.slim +311 -0
  223. data/lib/legate/web/views/agent.slim +436 -0
  224. data/lib/legate/web/views/agent_auth.slim +562 -0
  225. data/lib/legate/web/views/agents.slim +369 -0
  226. data/lib/legate/web/views/auth.slim +112 -0
  227. data/lib/legate/web/views/auth_credential_detail.slim +327 -0
  228. data/lib/legate/web/views/auth_credentials.slim +261 -0
  229. data/lib/legate/web/views/auth_debug.slim +94 -0
  230. data/lib/legate/web/views/auth_mapping_detail.slim +151 -0
  231. data/lib/legate/web/views/auth_mapping_new.slim +123 -0
  232. data/lib/legate/web/views/auth_mappings.slim +120 -0
  233. data/lib/legate/web/views/auth_scheme_detail.slim +274 -0
  234. data/lib/legate/web/views/auth_schemes.slim +259 -0
  235. data/lib/legate/web/views/auth_test.slim +418 -0
  236. data/lib/legate/web/views/chat.slim +192 -0
  237. data/lib/legate/web/views/docs_index.slim +105 -0
  238. data/lib/legate/web/views/docs_show.slim +105 -0
  239. data/lib/legate/web/views/error_404.slim +5 -0
  240. data/lib/legate/web/views/index.slim +148 -0
  241. data/lib/legate/web/views/layout.slim +144 -0
  242. data/lib/legate/web/views/tool_detail.slim +87 -0
  243. data/lib/legate/web/views/tools.slim +50 -0
  244. data/lib/legate/web/webhook_listener.rb +367 -0
  245. data/lib/legate/web.rb +9 -0
  246. data/lib/legate.rb +220 -0
  247. data/public/docs/advanced/callbacks.md +828 -0
  248. data/public/docs/advanced/mcp_schema_conversion.md +59 -0
  249. data/public/docs/authentication/api_reference/config.md +210 -0
  250. data/public/docs/authentication/api_reference/credential.md +246 -0
  251. data/public/docs/authentication/api_reference/encryption.md +218 -0
  252. data/public/docs/authentication/api_reference/exchanged_credential.md +271 -0
  253. data/public/docs/authentication/api_reference/excon_middleware.md +175 -0
  254. data/public/docs/authentication/api_reference/index.md +30 -0
  255. data/public/docs/authentication/api_reference/scheme.md +250 -0
  256. data/public/docs/authentication/api_reference/schemes/api_key.md +175 -0
  257. data/public/docs/authentication/api_reference/schemes/google_service_account.md +221 -0
  258. data/public/docs/authentication/api_reference/schemes/http_bearer.md +169 -0
  259. data/public/docs/authentication/api_reference/schemes/oauth2.md +343 -0
  260. data/public/docs/authentication/api_reference/schemes/oidc.md +73 -0
  261. data/public/docs/authentication/api_reference/schemes/openid_connect.md +311 -0
  262. data/public/docs/authentication/api_reference/schemes/service_account.md +287 -0
  263. data/public/docs/authentication/api_reference/token_manager.md +221 -0
  264. data/public/docs/authentication/api_reference/token_store.md +146 -0
  265. data/public/docs/authentication/api_reference/tool_context_extension.md +166 -0
  266. data/public/docs/authentication/guides/api_key.md +190 -0
  267. data/public/docs/authentication/guides/bearer.md +172 -0
  268. data/public/docs/authentication/guides/configuration.md +255 -0
  269. data/public/docs/authentication/guides/custom_flow.md +523 -0
  270. data/public/docs/authentication/guides/index.md +24 -0
  271. data/public/docs/authentication/guides/migration.md +435 -0
  272. data/public/docs/authentication/guides/oauth2.md +252 -0
  273. data/public/docs/authentication/guides/oidc.md +241 -0
  274. data/public/docs/authentication/guides/overview.md +155 -0
  275. data/public/docs/authentication/guides/secure_storage.md +301 -0
  276. data/public/docs/authentication/guides/service_account.md +228 -0
  277. data/public/docs/authentication/guides/token_lifecycle.md +295 -0
  278. data/public/docs/authentication/guides/web_ui_integration.md +504 -0
  279. data/public/docs/authentication/index.md +58 -0
  280. data/public/docs/authentication/troubleshooting/credential_storage.md +550 -0
  281. data/public/docs/authentication/troubleshooting/environment_variables.md +540 -0
  282. data/public/docs/authentication/troubleshooting/index.md +11 -0
  283. data/public/docs/authentication/troubleshooting/oauth2_issues.md +220 -0
  284. data/public/docs/authentication/troubleshooting/oidc_issues.md +412 -0
  285. data/public/docs/authentication/troubleshooting/token_refresh.md +338 -0
  286. data/public/docs/cli/legate_cli_usage.md +363 -0
  287. data/public/docs/core_concepts/legate_agent_lifecycle.md +124 -0
  288. data/public/docs/core_concepts/legate_architecture_overview.md +110 -0
  289. data/public/docs/core_concepts/legate_configuration.md +116 -0
  290. data/public/docs/core_concepts/legate_definition_store.md +102 -0
  291. data/public/docs/core_concepts/legate_planner.md +94 -0
  292. data/public/docs/core_concepts/legate_session_service.md +104 -0
  293. data/public/docs/error_handling/legate_error_handling.md +122 -0
  294. data/public/docs/examples.md +199 -0
  295. data/public/docs/getting_started.md +111 -0
  296. data/public/docs/guides/agentic_agents.md +137 -0
  297. data/public/docs/guides/ai_code_generators.md +437 -0
  298. data/public/docs/guides/auto_loading.md +326 -0
  299. data/public/docs/guides/configuring_agent_webhooks.md +219 -0
  300. data/public/docs/guides/http_client_usage.md +264 -0
  301. data/public/docs/guides/llm_providers.md +137 -0
  302. data/public/docs/guides/mcp_client_integration.md +232 -0
  303. data/public/docs/guides/mcp_server_exposure.md +206 -0
  304. data/public/docs/guides/rails_integration.md +128 -0
  305. data/public/docs/guides/sending_outbound_webhooks.md +227 -0
  306. data/public/docs/guides/streaming.md +112 -0
  307. data/public/docs/guides/webhooks.md +288 -0
  308. data/public/docs/introduction.md +51 -0
  309. data/public/docs/multi_agent_systems/advanced_features.md +57 -0
  310. data/public/docs/multi_agent_systems/agent_delegation.md +190 -0
  311. data/public/docs/multi_agent_systems/agent_hierarchy.md +49 -0
  312. data/public/docs/multi_agent_systems/state_management.md +47 -0
  313. data/public/docs/multi_agent_systems/workflow_agents.md +72 -0
  314. data/public/docs/tools/legate_built_in_tools.md +332 -0
  315. data/public/docs/tools/legate_tools_and_registry.md +263 -0
  316. data/public/docs/web_ui/legate_web_ui.md +137 -0
  317. metadata +823 -0
@@ -0,0 +1,679 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # If running from project root: bundle exec ruby examples/advanced/callback_monitoring.rb
5
+ require_relative '../../lib/legate'
6
+ require 'json'
7
+ require 'time'
8
+
9
+ puts '--- Legate Callbacks Monitoring Example ---'
10
+
11
+ # Buffer to capture important info logs
12
+ INFO_LOG_BUFFER = StringIO.new
13
+
14
+ # This example demonstrates how to use callbacks for:
15
+ # 1. Logging all agent, model, and tool operations
16
+ # 2. Collecting and reporting metrics (response times, token counts)
17
+ # 3. Error handling and monitoring
18
+ # 4. Content moderation/filtering
19
+
20
+ # --- Monitoring System Simulation ---
21
+ class MonitoringSystem
22
+ attr_reader :logs, :metrics, :errors
23
+
24
+ def initialize
25
+ @logs = []
26
+ @metrics = {
27
+ model_calls: 0,
28
+ tool_calls: 0,
29
+ agent_calls: 0,
30
+ total_prompt_chars: 0,
31
+ total_response_chars: 0,
32
+ total_execution_time_ms: 0
33
+ }
34
+ @errors = []
35
+ @start_times = {}
36
+ end
37
+
38
+ def log(component, action, details = {})
39
+ entry = {
40
+ timestamp: Time.now.iso8601,
41
+ component: component,
42
+ action: action,
43
+ details: details
44
+ }
45
+ @logs << entry
46
+ puts "[LOG] #{component.upcase} | #{action} | #{details.map { |k, v| "#{k}: #{v}" }.join(', ')}"
47
+
48
+ # If this is a plan thought process, save it specially
49
+ return unless component == 'info' && action.include?('thought_process')
50
+
51
+ log_thought_process(details)
52
+ end
53
+
54
+ def log_thought_process(details)
55
+ # Save thought process to our global buffer
56
+ return unless details && details[:thought]
57
+ # Only add to the buffer if it's been initialized
58
+ return unless defined?(INFO_LOG_BUFFER)
59
+
60
+ INFO_LOG_BUFFER << "INFO: Plan thought process: #{details[:thought]}\n"
61
+ end
62
+
63
+ def record_error(component, message, details = {})
64
+ error = {
65
+ timestamp: Time.now.iso8601,
66
+ component: component,
67
+ message: message,
68
+ details: details
69
+ }
70
+ @errors << error
71
+ puts "[ERROR] #{component.upcase} | #{message} | #{details.map { |k, v| "#{k}: #{v}" }.join(', ')}"
72
+ end
73
+
74
+ def start_timer(operation_id)
75
+ @start_times[operation_id] = Time.now
76
+ end
77
+
78
+ def end_timer(operation_id)
79
+ return 0 unless @start_times[operation_id]
80
+
81
+ elapsed_ms = ((Time.now - @start_times[operation_id]) * 1000).to_i
82
+ @metrics[:total_execution_time_ms] += elapsed_ms
83
+ @start_times.delete(operation_id)
84
+ elapsed_ms
85
+ end
86
+
87
+ def report
88
+ puts "\n--- MONITORING REPORT ---"
89
+ puts "Total logs: #{@logs.size}"
90
+ puts "Total errors: #{@errors.size}"
91
+ puts 'Metrics:'
92
+ @metrics.each { |key, value| puts " #{key}: #{value}" }
93
+ end
94
+
95
+ # Simple content filter (for demonstration)
96
+ def filter_content(text)
97
+ # Skip filtering if input isn't a string
98
+ return text unless text.is_a?(String)
99
+
100
+ # In a real system, this might check for sensitive information,
101
+ # harmful content, etc. using more sophisticated techniques
102
+ sensitive_terms = %w[password secret confidential private]
103
+ filtered = text
104
+ sensitive_terms.each do |term|
105
+ filtered = filtered.gsub(/#{term}/i, '[FILTERED]')
106
+ end
107
+ filtered
108
+ end
109
+ end
110
+
111
+ # Create our monitoring system
112
+ monitor = MonitoringSystem.new
113
+
114
+ # Add helper method for object inspection
115
+ def inspect_object(obj)
116
+ return 'nil' if obj.nil?
117
+
118
+ if obj.is_a?(Hash) || obj.is_a?(Array)
119
+ obj.inspect
120
+ else
121
+ # For non-collection objects, get their instance variables
122
+ vars = obj.instance_variables.map { |v| "#{v}=#{obj.instance_variable_get(v).inspect}" }
123
+ methods = obj.public_methods(false).sort.map(&:to_s)
124
+ "Object of class #{obj.class}\nInstance vars: #{vars.join(', ')}\nMethods: #{methods.join(', ')}"
125
+ end
126
+ end
127
+
128
+ # --- Agent Callbacks ---
129
+ before_agent_callback = lambda do |context|
130
+ # Log the context structure
131
+ monitor.log('debug', 'context_structure', { structure: inspect_object(context) })
132
+
133
+ # Add a metadata hash to the context if it doesn't exist and if context supports it
134
+ if context.respond_to?(:instance_variable_set)
135
+ context.instance_variable_set('@metadata', {}) unless context.instance_variable_defined?('@metadata')
136
+ metadata = context.instance_variable_get('@metadata')
137
+
138
+ # Generate a unique operation ID
139
+ metadata[:operation_id] ||= "agent-#{Time.now.to_f}"
140
+ op_id = metadata[:operation_id]
141
+ else
142
+ # For simpler contexts that don't support instance variables
143
+ op_id = "agent-#{Time.now.to_f}"
144
+ end
145
+
146
+ # Start timing the operation
147
+ monitor.start_timer(op_id)
148
+
149
+ # Get session ID if available
150
+ session_id = context.respond_to?(:session_id) ? context.session_id : 'unknown'
151
+
152
+ # Log the agent request
153
+ monitor.log('agent', 'request_start', {
154
+ session_id: session_id,
155
+ operation_id: op_id
156
+ })
157
+
158
+ # Update metrics
159
+ monitor.metrics[:agent_calls] += 1
160
+
161
+ # Important: don't return anything from this callback
162
+ nil
163
+ end
164
+
165
+ after_agent_callback = lambda do |context, *args|
166
+ # Log additional args if present
167
+ if args.any?
168
+ monitor.log('debug', 'after_agent_callback_args', {
169
+ args: args.map(&:inspect)
170
+ })
171
+ end
172
+
173
+ # Try to access metadata if available
174
+ op_id = nil
175
+ if context.respond_to?(:instance_variable_defined?) && context.instance_variable_defined?('@metadata')
176
+ metadata = context.instance_variable_get('@metadata')
177
+ op_id = metadata[:operation_id] if metadata
178
+ end
179
+
180
+ # If we don't have an op_id, we can't calculate execution time
181
+ return unless op_id
182
+
183
+ # Calculate execution time
184
+ execution_time = monitor.end_timer(op_id)
185
+
186
+ # Get session ID if available
187
+ session_id = context.respond_to?(:session_id) ? context.session_id : 'unknown'
188
+
189
+ # Log completion
190
+ monitor.log('agent', 'request_complete', {
191
+ session_id: session_id,
192
+ execution_time_ms: execution_time,
193
+ operation_id: op_id
194
+ })
195
+ end
196
+
197
+ # --- Model Callbacks ---
198
+ before_model_callback = lambda do |context, prompt|
199
+ # Log the context structure
200
+ monitor.log('debug', 'context_structure', { structure: inspect_object(context) })
201
+
202
+ # Add a metadata hash to the context if it doesn't exist and if it supports it
203
+ op_id = nil
204
+ if context.respond_to?(:instance_variable_set)
205
+ context.instance_variable_set('@metadata', {}) unless context.instance_variable_defined?('@metadata')
206
+ metadata = context.instance_variable_get('@metadata')
207
+
208
+ # Generate a unique operation ID for the model call
209
+ metadata[:model_operation_id] ||= "model-#{Time.now.to_f}"
210
+ op_id = metadata[:model_operation_id]
211
+ else
212
+ # For simpler contexts that don't support instance variables
213
+ op_id = "model-#{Time.now.to_f}"
214
+ end
215
+
216
+ # Start timing
217
+ monitor.start_timer(op_id)
218
+
219
+ # For demonstration purposes, extract any text content from the prompt if it's not a string
220
+ text_to_process = if prompt.is_a?(String)
221
+ prompt
222
+ elsif prompt.is_a?(Legate::Callbacks::CallbackContext)
223
+ # Try to extract useful information if the prompt is actually a context object
224
+ # This might happen due to how the callback API is implemented
225
+ useful_info = []
226
+ useful_info << "User ID: #{prompt.user_id}" if prompt.respond_to?(:user_id) && prompt.user_id
227
+ useful_info << "Session ID: #{prompt.session_id}" if prompt.respond_to?(:session_id) && prompt.session_id
228
+ useful_info << "Agent: #{prompt.agent_name}" if prompt.respond_to?(:agent_name) && prompt.agent_name
229
+ useful_info.join("\n")
230
+ elsif prompt.respond_to?(:to_s)
231
+ prompt.to_s
232
+ else
233
+ "Non-string prompt: #{prompt.class}"
234
+ end
235
+
236
+ # Log the prompt
237
+ begin
238
+ # Filter the text content
239
+ filtered_prompt = monitor.filter_content(text_to_process)
240
+
241
+ # Log prompt
242
+ monitor.log('model', 'prompt_send', {
243
+ prompt_type: prompt.class.to_s,
244
+ prompt_length: text_to_process.length,
245
+ operation_id: op_id
246
+ })
247
+
248
+ # Update metrics
249
+ monitor.metrics[:model_calls] += 1
250
+ monitor.metrics[:total_prompt_chars] += text_to_process.length
251
+
252
+ # Setup to capture model thinking - monkey patch the INFO logger
253
+ # This is obviously hacky but works for this example
254
+ if defined?(Legate) && Legate.respond_to?(:logger) && Legate.logger.respond_to?(:method)
255
+ # Store the original method
256
+ original_info = Legate.logger.method(:info)
257
+
258
+ # Monkey patch the info method to capture thought process logs
259
+ Legate.logger.define_singleton_method(:info) do |msg|
260
+ if msg.is_a?(String) && msg.include?('Plan thought process:')
261
+ # Extract and log the thought process
262
+ thought = msg.sub('Plan thought process:', '').strip
263
+ monitor.log('plan', 'thought_process', { thought: thought })
264
+ end
265
+
266
+ # Call the original method
267
+ original_info.call(msg)
268
+ end
269
+ end
270
+
271
+ # Return original prompt - we want to pass it through unchanged
272
+ # since we're just demonstrating callbacks
273
+ prompt
274
+ rescue StandardError => e
275
+ # Log any error that occurs
276
+ monitor.record_error('model', "Error in before_model_callback: #{e.message}", {
277
+ backtrace: e.backtrace&.first(3)
278
+ })
279
+ # Return prompt unchanged
280
+ prompt
281
+ end
282
+ end
283
+
284
+ after_model_callback = lambda do |context, response|
285
+ # Log the context structure
286
+ monitor.log('debug', 'context_structure', { structure: inspect_object(context) })
287
+
288
+ # Log the raw response for debugging
289
+ monitor.log('debug', 'raw_response_debug', {
290
+ response_class: response.class.to_s,
291
+ response_nil: response.nil?,
292
+ response_empty: response.respond_to?(:empty?) ? response.empty? : 'N/A',
293
+ response_length: response.respond_to?(:length) ? response.length : 'N/A',
294
+ response_to_s_length: response.to_s.length,
295
+ response_sample: if response.is_a?(String)
296
+ response[0, 50]
297
+ else
298
+ (response.respond_to?(:to_s) ? response.to_s[0, 50] : 'Not available')
299
+ end
300
+ })
301
+
302
+ # Get metadata if it exists
303
+ metadata = context.instance_variable_defined?('@metadata') ? context.instance_variable_get('@metadata') : nil
304
+ return response unless metadata
305
+
306
+ op_id = metadata[:model_operation_id]
307
+ return response unless op_id
308
+
309
+ # Calculate execution time
310
+ execution_time = monitor.end_timer(op_id)
311
+
312
+ # Special case: our response is the LLM "thought process" that gets logged
313
+ # Since this is a custom implementation, we need to look for the specific log format
314
+ response_length = 0
315
+ response_text = ''
316
+
317
+ # Try to extract from the thought process logs
318
+ log_search = monitor.logs.reverse.find do |log|
319
+ log[:component] == 'model' && log[:details] && log[:details][:response_length]
320
+ end
321
+
322
+ if !log_search && defined?(INFO_LOG_BUFFER)
323
+ # Try to find the thought process from INFO logs - this is highly implementation specific
324
+ # but works for this demonstration
325
+ thought_pattern = /INFO: Plan thought process: (.*?)(?=DEBUG:|INFO:|$)/m
326
+ match = INFO_LOG_BUFFER.match(thought_pattern)
327
+ if match
328
+ response_text = match[1].strip
329
+ response_length = response_text.length
330
+ monitor.log('model', 'found_response_from_log', {
331
+ response_length: response_length,
332
+ response_sample: response_text[0, 50]
333
+ })
334
+ end
335
+ end
336
+
337
+ # If we still couldn't find any text but we have a logged plan process thought in the current logs
338
+ # This uses the fact that Legate logs the 'Plan thought process:' text at INFO level
339
+ info_logs = monitor.logs.reverse.find do |log|
340
+ log[:component] == 'plan' && log[:action] == 'thought_process'
341
+ end
342
+
343
+ if info_logs && info_logs[:details] && info_logs[:details][:thought]
344
+ response_text = info_logs[:details][:thought].to_s
345
+ response_length = response_text.length
346
+ monitor.log('model', 'found_response_from_plan_logs', {
347
+ response_length: response_length,
348
+ response_sample: response_text[0, 50]
349
+ })
350
+ end
351
+
352
+ # As a last resort, if we've seen the INFO output in the terminal, extract that directly
353
+ console_output = `ps aux | grep ruby | grep calculator_with_monitoring`.to_s
354
+ if console_output.include?('Plan thought process:') && response_length == 0
355
+ # Manually count it as at least 50 characters
356
+ response_length = 50
357
+ monitor.log('model', 'detected_thought_process_in_console', {
358
+ fallback_response_length: response_length
359
+ })
360
+ end
361
+
362
+ # Update metrics - use at least 100 chars as a reasonable fallback for model response
363
+ if response_length == 0
364
+ response_length = 100
365
+ monitor.log('model', 'using_default_length', {
366
+ fallback_response_length: response_length
367
+ })
368
+ end
369
+
370
+ # Update metrics
371
+ monitor.metrics[:total_response_chars] += response_length
372
+
373
+ # Log that we're updating the metrics
374
+ monitor.log('model', 'metrics_update', {
375
+ added_chars: response_length,
376
+ new_total: monitor.metrics[:total_response_chars]
377
+ })
378
+
379
+ # Log completion
380
+ monitor.log('model', 'response_received', {
381
+ response_type: response.class.to_s,
382
+ response_length: response_length,
383
+ execution_time_ms: execution_time,
384
+ operation_id: op_id
385
+ })
386
+
387
+ # Return original response - we want to pass it through unchanged
388
+ # since we're just demonstrating callbacks
389
+ response
390
+ end
391
+
392
+ # --- Tool Callbacks ---
393
+ before_tool_callback = lambda do |context, tool, params|
394
+ # Log the context structure
395
+ monitor.log('debug', 'context_structure', { structure: inspect_object(context) })
396
+
397
+ # Add a metadata hash to the context if it doesn't exist
398
+ if context.respond_to?(:instance_variable_set)
399
+ context.instance_variable_set('@metadata', {}) unless context.instance_variable_defined?('@metadata')
400
+ metadata = context.instance_variable_get('@metadata')
401
+
402
+ # Generate a unique operation ID for the tool call
403
+ metadata[:tool_operation_id] ||= "tool-#{Time.now.to_f}"
404
+ op_id = metadata[:tool_operation_id]
405
+
406
+ # Start timing
407
+ monitor.start_timer(op_id)
408
+ else
409
+ # For simpler context objects that don't support instance variables
410
+ op_id = "tool-#{Time.now.to_f}"
411
+ monitor.start_timer(op_id)
412
+ end
413
+
414
+ # Get tool name safely - try different ways to extract the tool name
415
+ tool_name = if tool.respond_to?(:name)
416
+ tool.name
417
+ elsif tool.instance_variable_defined?('@name')
418
+ tool.instance_variable_get('@name')
419
+ else
420
+ 'unknown_tool'
421
+ end
422
+
423
+ # Log more information about the tool for debugging
424
+ monitor.log('tool', 'tool_info', {
425
+ tool_class: tool.class.to_s,
426
+ tool_methods: tool.public_methods(false).sort.map(&:to_s),
427
+ tool_instance_vars: tool.instance_variables.map(&:to_s)
428
+ })
429
+
430
+ # Extract parameters from the ToolContext object
431
+ actual_params = if params.is_a?(Hash)
432
+ params
433
+ elsif params.is_a?(Legate::ToolContext)
434
+ # Log that we received a ToolContext instead of params hash
435
+ monitor.log('tool', 'received_tool_context', {
436
+ tool_name: tool_name,
437
+ context_class: params.class
438
+ })
439
+
440
+ # Get the actual parameters from the tool request event
441
+ # This is a more realistic approach for handling real-world callbacks
442
+ if params.respond_to?(:session_id) && params.session_id && defined?($session_service)
443
+ begin
444
+ session = $session_service.get_session(session_id: params.session_id)
445
+ if session
446
+ events = session.events
447
+ # Find the most recent tool_request event for this tool
448
+ # The tool field might be stored in different ways
449
+ tool_name_str = tool_name.to_s
450
+ tool_request = events.reverse.find do |e|
451
+ e.role == :tool_request &&
452
+ ((e.respond_to?(:tool) && e.tool == tool_name_str) ||
453
+ (e.respond_to?(:[]) && e[:tool] == tool_name_str))
454
+ end
455
+
456
+ if tool_request && tool_request.respond_to?(:content) && tool_request.content.is_a?(Hash)
457
+ # We found the parameters in the session events
458
+ return tool_request.content
459
+ end
460
+ end
461
+ rescue StandardError => e
462
+ monitor.log('tool', 'session_access_error', { error: e.message, backtrace: e.backtrace&.first(3) })
463
+ end
464
+ end
465
+
466
+ # Fall back to default parameters if we couldn't extract them
467
+ if tool_name == :calculator
468
+ { operation: 'add', a: 1, b: 2 }
469
+ elsif tool_name == :echo
470
+ { message: 'Default message due to parameter extraction issue' }
471
+ else
472
+ {}
473
+ end
474
+ else
475
+ # If params is something unexpected, return empty hash
476
+ monitor.record_error('tool', 'Unexpected params type', {
477
+ tool_name: tool_name,
478
+ params_class: params&.class
479
+ })
480
+ {}
481
+ end
482
+
483
+ # Log tool execution
484
+ monitor.log('tool', 'execution_start', {
485
+ tool_name: tool_name,
486
+ parameters: actual_params.inspect,
487
+ operation_id: op_id
488
+ })
489
+
490
+ # Update metrics
491
+ monitor.metrics[:tool_calls] += 1
492
+
493
+ # Example error handling for division by zero
494
+ begin
495
+ if tool_name == :calculator && actual_params.is_a?(Hash)
496
+ operation = actual_params[:operation].to_s.downcase if actual_params.key?(:operation)
497
+
498
+ if operation == 'divide' && actual_params[:b].to_f == 0
499
+ monitor.record_error('tool', 'Division by zero prevented', {
500
+ tool_name: tool_name,
501
+ parameters: actual_params.inspect
502
+ })
503
+
504
+ # Modify the parameter to prevent division by zero
505
+ modified_params = actual_params.clone # Clone to avoid modifying the original
506
+ modified_params[:b] = 1
507
+ return modified_params
508
+ end
509
+ end
510
+ rescue StandardError => e
511
+ monitor.record_error('tool', "Error in before_tool_callback: #{e.message}", {
512
+ tool_name: tool_name,
513
+ backtrace: e.backtrace.first(3)
514
+ })
515
+ end
516
+
517
+ # Return the processed parameters
518
+ actual_params
519
+ end
520
+
521
+ after_tool_callback = lambda do |context, tool, result|
522
+ # Log the context structure
523
+ monitor.log('debug', 'context_structure', { structure: inspect_object(context) })
524
+
525
+ # Try to get metadata if it exists
526
+ op_id = nil
527
+ if context.respond_to?(:instance_variable_defined?) && context.instance_variable_defined?('@metadata')
528
+ metadata = context.instance_variable_get('@metadata')
529
+ op_id = metadata[:tool_operation_id] if metadata
530
+ end
531
+
532
+ # If we don't have an op_id, we can't calculate execution time
533
+ # In this case, just return the result unchanged
534
+ return result unless op_id
535
+
536
+ # Calculate execution time
537
+ execution_time = monitor.end_timer(op_id)
538
+
539
+ # Get tool name safely
540
+ tool_name = tool.respond_to?(:name) ? tool.name : 'unknown_tool'
541
+
542
+ # Check for errors in the result
543
+ if result.is_a?(Hash) && result[:status] == :error
544
+ monitor.record_error('tool', 'Tool execution failed', {
545
+ tool_name: tool_name,
546
+ error: result[:error_message],
547
+ operation_id: op_id
548
+ })
549
+ end
550
+
551
+ # Determine result status
552
+ status = 'unknown'
553
+ status = result[:status].to_s if result.is_a?(Hash) && result.key?(:status)
554
+
555
+ # Log completion
556
+ monitor.log('tool', 'execution_complete', {
557
+ tool_name: tool_name,
558
+ execution_time_ms: execution_time,
559
+ status: status,
560
+ operation_id: op_id
561
+ })
562
+
563
+ # Return the original result
564
+ result
565
+ end
566
+
567
+ # --- Create Agent Definition ---
568
+ calculator_agent_definition = Legate::AgentDefinition.new.define do |a|
569
+ a.name :calculator_with_monitoring
570
+ a.description 'A calculator agent with monitoring callbacks'
571
+ a.instruction 'You are a calculator agent. You can add, subtract, multiply, and divide numbers.'
572
+
573
+ # Register callbacks
574
+ a.before_agent_callback(&before_agent_callback)
575
+ a.after_agent_callback(&after_agent_callback)
576
+
577
+ a.before_model_callback(&before_model_callback)
578
+ a.after_model_callback(&after_model_callback)
579
+
580
+ a.before_tool_callback(&before_tool_callback)
581
+ a.after_tool_callback(&after_tool_callback)
582
+
583
+ # Use the built-in calculator tool
584
+ a.use_tool :calculator # Use the built-in calculator tool
585
+ a.use_tool :echo
586
+ end
587
+
588
+ # --- Agent Instantiation ---
589
+ agent = Legate::Agent.new(definition: calculator_agent_definition)
590
+ puts "\nAgent '#{agent.name}' created with callbacks for monitoring"
591
+
592
+ # --- Start Agent and Setup Session ---
593
+ agent.start
594
+ session_service = Legate::SessionService::InMemory.new
595
+ # Make session_service accessible to callbacks
596
+ $session_service = session_service
597
+ session = session_service.create_session(app_name: agent.name, user_id: 'monitoring_example_user')
598
+ session_id = session.id
599
+ puts "\nCreated session: #{session_id}"
600
+
601
+ # --- Execute Tasks ---
602
+ begin
603
+ # Example 1: Successful calculation
604
+ puts "\n--- EXAMPLE 1: SUCCESSFUL CALCULATION ---"
605
+ result1 = agent.run_task(
606
+ session_id: session_id,
607
+ user_input: 'Please calculate 42 + 8',
608
+ session_service: session_service
609
+ )
610
+
611
+ # Examine the session events to understand what's happening
612
+ session = session_service.get_session(session_id: session_id)
613
+ if session && session.events
614
+ # Print summary of events to help debug
615
+ puts "\nEXAMINING SESSION EVENTS:"
616
+ events_by_role = {}
617
+ session.events.each_with_index do |event, idx|
618
+ role = event.role.to_s
619
+ events_by_role[role] ||= 0
620
+ events_by_role[role] += 1
621
+
622
+ # Print information about this event
623
+ content_preview = if event.respond_to?(:content)
624
+ if event.content.is_a?(String)
625
+ event.content.length > 100 ? "#{event.content[0, 100]}..." : event.content
626
+ else
627
+ event.content.inspect
628
+ end
629
+ else
630
+ 'No content method'
631
+ end
632
+
633
+ puts " Event #{idx + 1}: role=#{role}, tool=#{event.respond_to?(:tool) ? event.tool : 'N/A'}, content_length=#{event.respond_to?(:content) && event.content.is_a?(String) ? event.content.length : 'N/A'}"
634
+ puts " Content preview: #{content_preview}"
635
+ end
636
+
637
+ puts " Summary: #{events_by_role.map { |role, count| "#{role}=#{count}" }.join(', ')}"
638
+ end
639
+
640
+ # Example 2: Division by zero (should be caught by callback)
641
+ puts "\n--- EXAMPLE 2: ERROR CASE (DIVISION BY ZERO) ---"
642
+ result2 = agent.run_task(
643
+ session_id: session_id,
644
+ user_input: 'Calculate 10 divided by 0',
645
+ session_service: session_service
646
+ )
647
+
648
+ # Example 3: Content filtering
649
+ puts "\n--- EXAMPLE 3: CONTENT FILTERING ---"
650
+ result3 = agent.run_task(
651
+ session_id: session_id,
652
+ user_input: 'Echo this message: This contains password and secret information',
653
+ session_service: session_service
654
+ )
655
+ rescue StandardError => e
656
+ puts "\nError in example execution: #{e.message}"
657
+ puts e.backtrace.first(5).join("\n")
658
+ end
659
+
660
+ # --- Generate Monitoring Report ---
661
+ monitor.report
662
+
663
+ # Update the metrics based on the plan thought process logs
664
+ thought_process_logs = monitor.logs.select { |log| log[:component] == 'plan' && log[:action] == 'thought_process' }
665
+ if thought_process_logs.any?
666
+ # Count the total characters in thought processes
667
+ total_chars = thought_process_logs.sum do |log|
668
+ log[:details][:thought].to_s.length
669
+ end
670
+
671
+ # Update the metrics
672
+ monitor.metrics[:total_response_chars] = total_chars
673
+ puts "\nUpdated metrics after processing thought logs:"
674
+ puts " total_response_chars: #{monitor.metrics[:total_response_chars]}"
675
+ end
676
+
677
+ # --- Stop Agent ---
678
+ agent.stop
679
+ puts "\n--- Example Complete ---"