ai_sentinel 0.2.2 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 426089deb83c2bedf49d58d170b9241d0cd14a0284fa7b23acc7e8b5932f666f
4
- data.tar.gz: 7fd0fc6880e2cacc358a2e32bcbed7636e70aade5d688294c5ef79961cb20c92
3
+ metadata.gz: 57b7b717300a40c7538e4a7da84bd574677c9bd38fa55c094ad44725354e07d8
4
+ data.tar.gz: 1897eb521d5a9b2c71a1b1d54e7aa015172466accb65ef6c0bfa6a3b9ac5879a
5
5
  SHA512:
6
- metadata.gz: b19b08eafd1dc89bcb3ca42f29ae483e6751a1bbf50546b6be65a4b2840705cf705d25d340af8d81b0a443267f20227e1f492b5d44403e0a0cd5ef1d5f93f643
7
- data.tar.gz: 1eb5109adbf99a1802e393942282f05e7635274e0d658e4799488f9a0526cdf94db249df83cfaf65d0097c155448b8445f05bf6fce330d15fdd7f3bff6c51d1e
6
+ metadata.gz: 46a5467ba869e34a6b2e4f942c23cdcb719a8b2d8d6583ba29237873bb0145bed9ef736d6949bdf0434bf0f05fce8529b790b89f691594f40545667be12b574d
7
+ data.tar.gz: 8d05fe5f7477260054a4d9083fa2cbdcecb0effc5c2c701b1768fae3882604e9a8840e0cb5ceba0fb11b61a2fc5a488f1e3eef61d298f3d1e06c6d6e60327e64
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # AiSentinel
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/ai_sentinel.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/ai_sentinel)
4
+
3
5
  A lightweight Ruby gem for scheduling AI-driven tasks. Define workflows in a YAML config file that run on a cron schedule, process data through LLMs, and take conditional actions based on the results. Designed to be self-hostable on minimal hardware -- just Ruby and SQLite.
4
6
 
5
7
  ## Table of contents
data/exe/ai_sentinel CHANGED
@@ -2,4 +2,10 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require 'ai_sentinel'
5
- AiSentinel::CLI.start(ARGV)
5
+
6
+ begin
7
+ AiSentinel::CLI.start(ARGV)
8
+ rescue StandardError => e
9
+ AiSentinel.log_error(e, context: 'Fatal error')
10
+ exit 1
11
+ end
@@ -75,8 +75,8 @@ module AiSentinel
75
75
  result = tool_executor.execute(tool_name, tool_input)
76
76
  AiSentinel.logger.info(" Tool result: #{result.to_s[0..200]}")
77
77
  { 'type' => 'tool_result', 'tool_use_id' => tool_id, 'content' => result.to_s }
78
- rescue Error => e
79
- AiSentinel.logger.warn(" Tool error: #{e.message}")
78
+ rescue StandardError => e
79
+ AiSentinel.log_error(e, context: "Tool '#{tool_name}' error")
80
80
  { 'type' => 'tool_result', 'tool_use_id' => tool_id, 'content' => "Error: #{e.message}",
81
81
  'is_error' => true }
82
82
  end
@@ -94,7 +94,7 @@ module AiSentinel
94
94
  def compact_context(context_key)
95
95
  ContextCompactor.new(context_key: context_key, configuration: configuration).compact_if_needed
96
96
  rescue StandardError => e
97
- AiSentinel.logger.warn("Context compaction failed for '#{context_key}': #{e.message}")
97
+ AiSentinel.log_error(e, context: "Context compaction failed for '#{context_key}'")
98
98
  end
99
99
 
100
100
  def prune_old_messages(context_id)
@@ -61,19 +61,17 @@ module AiSentinel
61
61
  def execute_tool_call(tool_executor, tool_call, round)
62
62
  function = tool_call['function']
63
63
  tool_name = function['name']
64
- tool_input = JSON.parse(function['arguments'])
65
64
  tool_id = tool_call['id']
66
65
 
67
66
  AiSentinel.logger.info(" Tool call [round #{round + 1}]: #{tool_name}(#{function['arguments']})")
68
67
 
69
- begin
70
- result = tool_executor.execute(tool_name, tool_input)
71
- AiSentinel.logger.info(" Tool result: #{result.to_s[0..200]}")
72
- { 'role' => 'tool', 'tool_call_id' => tool_id, 'content' => result.to_s }
73
- rescue Error => e
74
- AiSentinel.logger.warn(" Tool error: #{e.message}")
75
- { 'role' => 'tool', 'tool_call_id' => tool_id, 'content' => "Error: #{e.message}" }
76
- end
68
+ tool_input = JSON.parse(function['arguments'])
69
+ result = tool_executor.execute(tool_name, tool_input)
70
+ AiSentinel.logger.info(" Tool result: #{result.to_s[0..200]}")
71
+ { 'role' => 'tool', 'tool_call_id' => tool_id, 'content' => result.to_s }
72
+ rescue StandardError => e
73
+ AiSentinel.log_error(e, context: "Tool '#{tool_name}' error")
74
+ { 'role' => 'tool', 'tool_call_id' => tool_id, 'content' => "Error: #{e.message}" }
77
75
  end
78
76
 
79
77
  def build_messages(prompt, system, context_key, remember, limit: nil)
@@ -31,7 +31,7 @@ module AiSentinel
31
31
  context
32
32
  rescue StandardError => e
33
33
  Persistence::ExecutionLog.fail(execution_id, e.message)
34
- AiSentinel.logger.error("Workflow '#{workflow.name}' failed: #{e.message}")
34
+ AiSentinel.log_error(e, context: "Workflow '#{workflow.name}' failed")
35
35
  raise
36
36
  end
37
37
 
@@ -17,8 +17,11 @@ module AiSentinel
17
17
  apply_working_directory
18
18
 
19
19
  if daemonize
20
+ Persistence::Database.disconnect
20
21
  Process.daemon(true, true)
22
+ Persistence::Database.setup(configuration.database_path)
21
23
  write_pid_file
24
+ setup_crash_cleanup
22
25
  AiSentinel.logger.info("AiSentinel started in background (PID #{Process.pid}, #{registry.size} workflow(s))")
23
26
  else
24
27
  AiSentinel.logger.info("AiSentinel started (#{registry.size} workflow(s)). Press Ctrl+C to stop.")
@@ -32,10 +35,16 @@ module AiSentinel
32
35
  @rufus.join
33
36
  cleanup_pid_file
34
37
  AiSentinel.logger.info('AiSentinel stopped')
38
+ rescue StandardError => e
39
+ AiSentinel.log_error(e, context: 'Scheduler crashed')
40
+ cleanup_pid_file
41
+ raise
35
42
  end
36
43
 
37
44
  def stop
38
- @rufus.shutdown
45
+ @rufus&.shutdown
46
+ rescue StandardError => e
47
+ AiSentinel.log_error(e, context: 'Error during shutdown')
39
48
  end
40
49
 
41
50
  def trigger(workflow_name)
@@ -72,13 +81,17 @@ module AiSentinel
72
81
  FileUtils.rm_f(pid_file)
73
82
  end
74
83
 
84
+ def setup_crash_cleanup
85
+ at_exit { cleanup_pid_file }
86
+ end
87
+
75
88
  def register_workflows
76
89
  registry.each do |name, workflow|
77
90
  @rufus.cron(workflow.schedule_expression) do
78
91
  runner = Runner.new(workflow: workflow, configuration: configuration)
79
92
  runner.execute
80
93
  rescue StandardError => e
81
- AiSentinel.logger.error("Workflow '#{name}' failed: #{e.message}")
94
+ AiSentinel.log_error(e, context: "Workflow '#{name}' failed")
82
95
  end
83
96
 
84
97
  AiSentinel.logger.info("Registered workflow '#{name}' with schedule '#{workflow.schedule_expression}'")
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AiSentinel
4
- VERSION = '0.2.2'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/ai_sentinel.rb CHANGED
@@ -50,6 +50,14 @@ module AiSentinel
50
50
  configuration.logger
51
51
  end
52
52
 
53
+ def log_error(error, context: nil)
54
+ message = context ? "#{context}: #{error.message}" : error.message
55
+ logger.error(message)
56
+ error.backtrace&.first(10)&.each { |line| logger.error(" #{line}") }
57
+ rescue StandardError
58
+ nil
59
+ end
60
+
53
61
  def resolve_api_key
54
62
  configuration.api_key ||= ENV.fetch(configuration.env_key_name, nil)
55
63
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ai_sentinel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Celi