ai_sentinel 0.2.2 → 0.4.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: 54f5dcacdba120c16ed3a4c09384f8b981aef569b9fb66a350400ec8a3f13b4b
4
+ data.tar.gz: 0b15c9519a2085dcc7b3a3b920ff4b91e89f282da8346429f6152b886334225f
5
5
  SHA512:
6
- metadata.gz: b19b08eafd1dc89bcb3ca42f29ae483e6751a1bbf50546b6be65a4b2840705cf705d25d340af8d81b0a443267f20227e1f492b5d44403e0a0cd5ef1d5f93f643
7
- data.tar.gz: 1eb5109adbf99a1802e393942282f05e7635274e0d658e4799488f9a0526cdf94db249df83cfaf65d0097c155448b8445f05bf6fce330d15fdd7f3bff6c51d1e
6
+ metadata.gz: 02f23261d5e52c11c03b9c15c7633f5eb12b54daacb49062d3cc8eb5be7168b72c1069e0c1607d3c02d13867af4545455dbd995eb49cc01dfc25007a8ed5d4ff
7
+ data.tar.gz: 8a9a3159203a5e5e580dfe2801237ff68067a3ef9b30b23389e4e05204851d5135ad6d38093d568bbfd6b328186180d8df56d706363cfc5d5059fb8a110ff067
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
@@ -32,7 +32,8 @@ module AiSentinel
32
32
  prompt_template: step.params[:prompt],
33
33
  system_template: step.params[:system],
34
34
  tool_executor: tool_executor,
35
- max_tool_rounds: step.params.fetch(:max_tool_rounds, configuration.max_tool_rounds)
35
+ max_tool_rounds: step.params.fetch(:max_tool_rounds, configuration.max_tool_rounds),
36
+ compaction_prompt: step.params[:compaction_prompt]
36
37
  )
37
38
  end
38
39
 
@@ -10,11 +10,12 @@ module AiSentinel
10
10
  Respond with ONLY the summary, no preamble or explanation.
11
11
  PROMPT
12
12
 
13
- attr_reader :context_key, :configuration
13
+ attr_reader :context_key, :configuration, :custom_compaction_prompt
14
14
 
15
- def initialize(context_key:, configuration:)
15
+ def initialize(context_key:, configuration:, compaction_prompt: nil)
16
16
  @context_key = context_key
17
17
  @configuration = configuration
18
+ @custom_compaction_prompt = compaction_prompt
18
19
  end
19
20
 
20
21
  def compact_if_needed
@@ -74,7 +75,7 @@ module AiSentinel
74
75
 
75
76
  result = provider.chat(
76
77
  prompt: prompt,
77
- system: SUMMARIZATION_PROMPT,
78
+ system: custom_compaction_prompt || SUMMARIZATION_PROMPT,
78
79
  workflow_name: nil,
79
80
  step_name: nil,
80
81
  remember: false
@@ -9,7 +9,8 @@ module AiSentinel
9
9
  API_VERSION = '2023-06-01'
10
10
 
11
11
  def chat(prompt:, system: nil, model: nil, workflow_name: nil, step_name: nil, remember: true,
12
- prompt_template: nil, system_template: nil, tool_executor: nil, max_tool_rounds: 10)
12
+ prompt_template: nil, system_template: nil, tool_executor: nil, max_tool_rounds: 10,
13
+ compaction_prompt: nil)
13
14
  model ||= configuration.model
14
15
  context_key = "#{workflow_name}:#{step_name}"
15
16
 
@@ -26,7 +27,8 @@ module AiSentinel
26
27
  assistant_text = extract_text(response_data)
27
28
  if remember
28
29
  save_context(context_key, prompt, assistant_text,
29
- prompt_template: prompt_template, system_template: system_template)
30
+ prompt_template: prompt_template, system_template: system_template,
31
+ compaction_prompt: compaction_prompt)
30
32
  end
31
33
 
32
34
  Actions::AiPrompt::Result.new(
@@ -75,8 +77,8 @@ module AiSentinel
75
77
  result = tool_executor.execute(tool_name, tool_input)
76
78
  AiSentinel.logger.info(" Tool result: #{result.to_s[0..200]}")
77
79
  { 'type' => 'tool_result', 'tool_use_id' => tool_id, 'content' => result.to_s }
78
- rescue Error => e
79
- AiSentinel.logger.warn(" Tool error: #{e.message}")
80
+ rescue StandardError => e
81
+ AiSentinel.log_error(e, context: "Tool '#{tool_name}' error")
80
82
  { 'type' => 'tool_result', 'tool_use_id' => tool_id, 'content' => "Error: #{e.message}",
81
83
  'is_error' => true }
82
84
  end
@@ -66,7 +66,8 @@ module AiSentinel
66
66
  messages
67
67
  end
68
68
 
69
- def save_context(context_key, user_message, assistant_message, prompt_template: nil, system_template: nil)
69
+ def save_context(context_key, user_message, assistant_message, prompt_template: nil, system_template: nil,
70
+ compaction_prompt: nil)
70
71
  return unless Persistence::Database.connected?
71
72
 
72
73
  ctx = Persistence::Database.find_or_create_context(context_key)
@@ -81,7 +82,7 @@ module AiSentinel
81
82
 
82
83
  save_prompt_hash(context_key, prompt_template, system_template) if prompt_template
83
84
  prune_old_messages(ctx[:id])
84
- compact_context(context_key)
85
+ compact_context(context_key, compaction_prompt: compaction_prompt)
85
86
  end
86
87
 
87
88
  def save_prompt_hash(context_key, prompt_template, system_template)
@@ -91,10 +92,11 @@ module AiSentinel
91
92
  )
92
93
  end
93
94
 
94
- def compact_context(context_key)
95
- ContextCompactor.new(context_key: context_key, configuration: configuration).compact_if_needed
95
+ def compact_context(context_key, compaction_prompt: nil)
96
+ ContextCompactor.new(context_key: context_key, configuration: configuration,
97
+ compaction_prompt: compaction_prompt).compact_if_needed
96
98
  rescue StandardError => e
97
- AiSentinel.logger.warn("Context compaction failed for '#{context_key}': #{e.message}")
99
+ AiSentinel.log_error(e, context: "Context compaction failed for '#{context_key}'")
98
100
  end
99
101
 
100
102
  def prune_old_messages(context_id)
@@ -7,7 +7,8 @@ module AiSentinel
7
7
  module Providers
8
8
  class Openai < Base
9
9
  def chat(prompt:, system: nil, model: nil, workflow_name: nil, step_name: nil, remember: true,
10
- prompt_template: nil, system_template: nil, tool_executor: nil, max_tool_rounds: 10)
10
+ prompt_template: nil, system_template: nil, tool_executor: nil, max_tool_rounds: 10,
11
+ compaction_prompt: nil)
11
12
  model ||= configuration.model
12
13
  context_key = "#{workflow_name}:#{step_name}"
13
14
 
@@ -24,7 +25,8 @@ module AiSentinel
24
25
  assistant_text = extract_text(response_data)
25
26
  if remember
26
27
  save_context(context_key, prompt, assistant_text,
27
- prompt_template: prompt_template, system_template: system_template)
28
+ prompt_template: prompt_template, system_template: system_template,
29
+ compaction_prompt: compaction_prompt)
28
30
  end
29
31
 
30
32
  Actions::AiPrompt::Result.new(
@@ -61,19 +63,17 @@ module AiSentinel
61
63
  def execute_tool_call(tool_executor, tool_call, round)
62
64
  function = tool_call['function']
63
65
  tool_name = function['name']
64
- tool_input = JSON.parse(function['arguments'])
65
66
  tool_id = tool_call['id']
66
67
 
67
68
  AiSentinel.logger.info(" Tool call [round #{round + 1}]: #{tool_name}(#{function['arguments']})")
68
69
 
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
70
+ tool_input = JSON.parse(function['arguments'])
71
+ result = tool_executor.execute(tool_name, tool_input)
72
+ AiSentinel.logger.info(" Tool result: #{result.to_s[0..200]}")
73
+ { 'role' => 'tool', 'tool_call_id' => tool_id, 'content' => result.to_s }
74
+ rescue StandardError => e
75
+ AiSentinel.log_error(e, context: "Tool '#{tool_name}' error")
76
+ { 'role' => 'tool', 'tool_call_id' => tool_id, 'content' => "Error: #{e.message}" }
77
77
  end
78
78
 
79
79
  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.4.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.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mario Celi