botiasloop 0.0.1
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 +7 -0
- data/README.md +343 -0
- data/bin/botiasloop +155 -0
- data/data/skills/skill-creator/SKILL.md +329 -0
- data/data/skills/skill-creator/assets/ruby_api_cli_template.rb +151 -0
- data/data/skills/skill-creator/references/specification.md +99 -0
- data/lib/botiasloop/agent.rb +112 -0
- data/lib/botiasloop/channels/base.rb +248 -0
- data/lib/botiasloop/channels/cli.rb +101 -0
- data/lib/botiasloop/channels/telegram.rb +348 -0
- data/lib/botiasloop/channels.rb +64 -0
- data/lib/botiasloop/channels_manager.rb +299 -0
- data/lib/botiasloop/commands/archive.rb +109 -0
- data/lib/botiasloop/commands/base.rb +54 -0
- data/lib/botiasloop/commands/compact.rb +78 -0
- data/lib/botiasloop/commands/context.rb +34 -0
- data/lib/botiasloop/commands/conversations.rb +40 -0
- data/lib/botiasloop/commands/help.rb +30 -0
- data/lib/botiasloop/commands/label.rb +64 -0
- data/lib/botiasloop/commands/new.rb +21 -0
- data/lib/botiasloop/commands/registry.rb +121 -0
- data/lib/botiasloop/commands/reset.rb +18 -0
- data/lib/botiasloop/commands/status.rb +32 -0
- data/lib/botiasloop/commands/switch.rb +76 -0
- data/lib/botiasloop/commands/system_prompt.rb +20 -0
- data/lib/botiasloop/commands.rb +22 -0
- data/lib/botiasloop/config.rb +58 -0
- data/lib/botiasloop/conversation.rb +189 -0
- data/lib/botiasloop/conversation_manager.rb +225 -0
- data/lib/botiasloop/database.rb +92 -0
- data/lib/botiasloop/loop.rb +115 -0
- data/lib/botiasloop/skills/loader.rb +58 -0
- data/lib/botiasloop/skills/registry.rb +42 -0
- data/lib/botiasloop/skills/skill.rb +75 -0
- data/lib/botiasloop/systemd_service.rb +300 -0
- data/lib/botiasloop/tool.rb +24 -0
- data/lib/botiasloop/tools/registry.rb +68 -0
- data/lib/botiasloop/tools/shell.rb +50 -0
- data/lib/botiasloop/tools/web_search.rb +64 -0
- data/lib/botiasloop/version.rb +5 -0
- data/lib/botiasloop.rb +45 -0
- metadata +250 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Botiasloop
|
|
6
|
+
# Systemd user service management for auto-start on boot
|
|
7
|
+
#
|
|
8
|
+
# Manages installation, enablement, and control of botiasloop
|
|
9
|
+
# as a systemd user service. This allows botiasloop to start
|
|
10
|
+
# automatically on user login and run in the background.
|
|
11
|
+
#
|
|
12
|
+
# @example Install and enable the service
|
|
13
|
+
# service = Botiasloop::SystemdService.new(config)
|
|
14
|
+
# service.install
|
|
15
|
+
# service.enable
|
|
16
|
+
# service.start
|
|
17
|
+
#
|
|
18
|
+
# @example Check status
|
|
19
|
+
# service = Botiasloop::SystemdService.new(config)
|
|
20
|
+
# status = service.status
|
|
21
|
+
# puts "Running: #{status[:active]}"
|
|
22
|
+
#
|
|
23
|
+
class SystemdService
|
|
24
|
+
attr_reader :config
|
|
25
|
+
|
|
26
|
+
# Service name used by systemd
|
|
27
|
+
SERVICE_NAME = "botiasloop.service"
|
|
28
|
+
|
|
29
|
+
# Initialize a new SystemdService instance
|
|
30
|
+
#
|
|
31
|
+
# @param config [Config] Configuration instance
|
|
32
|
+
def initialize(config)
|
|
33
|
+
@config = config
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Check if systemd is available on the system
|
|
37
|
+
#
|
|
38
|
+
# @return [Boolean] True if systemctl is available
|
|
39
|
+
def systemd_available?
|
|
40
|
+
!`which systemctl 2>/dev/null`.strip.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Check if the service file is installed
|
|
44
|
+
#
|
|
45
|
+
# @return [Boolean] True if service file exists
|
|
46
|
+
def installed?
|
|
47
|
+
File.exist?(service_file_path)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Check if the service is enabled to start on boot
|
|
51
|
+
#
|
|
52
|
+
# @return [Boolean] True if service is enabled
|
|
53
|
+
def enabled?
|
|
54
|
+
return false unless systemd_available?
|
|
55
|
+
|
|
56
|
+
systemctl_quiet("is-enabled", SERVICE_NAME)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Check if the service is currently active/running
|
|
60
|
+
#
|
|
61
|
+
# @return [Boolean] True if service is active
|
|
62
|
+
def active?
|
|
63
|
+
return false unless systemd_available?
|
|
64
|
+
|
|
65
|
+
systemctl_quiet("is-active", SERVICE_NAME)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Install the service file
|
|
69
|
+
#
|
|
70
|
+
# Creates the systemd user directory if needed and writes
|
|
71
|
+
# the service configuration file.
|
|
72
|
+
#
|
|
73
|
+
# @raise [SystemdError] If installation fails
|
|
74
|
+
# @return [Boolean] True on success
|
|
75
|
+
def install
|
|
76
|
+
FileUtils.mkdir_p(systemd_user_dir)
|
|
77
|
+
File.write(service_file_path, service_template)
|
|
78
|
+
systemctl("daemon-reload")
|
|
79
|
+
true
|
|
80
|
+
rescue => e
|
|
81
|
+
raise SystemdError, "Failed to install service: #{e.message}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Uninstall the service
|
|
85
|
+
#
|
|
86
|
+
# Stops the service if running, disables it, removes the
|
|
87
|
+
# service file, and reloads systemd.
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean] True if uninstalled, false if not installed
|
|
90
|
+
def uninstall
|
|
91
|
+
return false unless installed?
|
|
92
|
+
|
|
93
|
+
stop if active?
|
|
94
|
+
disable if enabled?
|
|
95
|
+
FileUtils.rm_f(service_file_path)
|
|
96
|
+
systemctl("daemon-reload")
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Enable the service to start on boot
|
|
101
|
+
#
|
|
102
|
+
# Enables linger to allow user services to start at boot time,
|
|
103
|
+
# then enables the service. Falls back gracefully if linger fails.
|
|
104
|
+
#
|
|
105
|
+
# @raise [SystemdError] If systemd unavailable or service not installed
|
|
106
|
+
# @return [Boolean] True on success
|
|
107
|
+
def enable
|
|
108
|
+
raise SystemdError, "systemd is not available on this system" unless systemd_available?
|
|
109
|
+
raise SystemdError, "Service is not installed" unless installed?
|
|
110
|
+
|
|
111
|
+
enable_linger
|
|
112
|
+
systemctl("enable", SERVICE_NAME)
|
|
113
|
+
true
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Disable the service from starting on boot
|
|
117
|
+
#
|
|
118
|
+
# Disables the service then disables linger if it was enabled.
|
|
119
|
+
#
|
|
120
|
+
# @raise [SystemdError] If systemd unavailable
|
|
121
|
+
# @return [Boolean] True on success
|
|
122
|
+
def disable
|
|
123
|
+
raise SystemdError, "systemd is not available on this system" unless systemd_available?
|
|
124
|
+
|
|
125
|
+
systemctl("disable", SERVICE_NAME)
|
|
126
|
+
disable_linger
|
|
127
|
+
true
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Check if linger is enabled for the current user
|
|
131
|
+
#
|
|
132
|
+
# Linger allows user services to start at boot time
|
|
133
|
+
# without requiring a user login session.
|
|
134
|
+
#
|
|
135
|
+
# @return [Boolean] True if linger is enabled
|
|
136
|
+
def linger_enabled?
|
|
137
|
+
output = `loginctl show-user $USER --property=Linger 2>/dev/null`.strip
|
|
138
|
+
output == "Linger=yes"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Enable linger for the current user
|
|
142
|
+
#
|
|
143
|
+
# Allows user services to start at boot time.
|
|
144
|
+
# Does nothing if already enabled.
|
|
145
|
+
#
|
|
146
|
+
# @return [Boolean] True on success or already enabled, false on failure
|
|
147
|
+
def enable_linger
|
|
148
|
+
return true if linger_enabled?
|
|
149
|
+
|
|
150
|
+
system("loginctl", "enable-linger", ENV["USER"])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Disable linger for the current user
|
|
154
|
+
#
|
|
155
|
+
# Prevents user services from starting at boot time.
|
|
156
|
+
# Does nothing if already disabled.
|
|
157
|
+
#
|
|
158
|
+
# @return [Boolean] True on success or already disabled, false on failure
|
|
159
|
+
def disable_linger
|
|
160
|
+
return true unless linger_enabled?
|
|
161
|
+
|
|
162
|
+
system("loginctl", "disable-linger", ENV["USER"])
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Start the service
|
|
166
|
+
#
|
|
167
|
+
# @raise [SystemdError] If systemd unavailable or service not installed
|
|
168
|
+
# @return [Boolean] True on success
|
|
169
|
+
def start
|
|
170
|
+
raise SystemdError, "systemd is not available on this system" unless systemd_available?
|
|
171
|
+
raise SystemdError, "Service is not installed" unless installed?
|
|
172
|
+
|
|
173
|
+
systemctl("start", SERVICE_NAME)
|
|
174
|
+
true
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Stop the service
|
|
178
|
+
#
|
|
179
|
+
# @raise [SystemdError] If systemd unavailable
|
|
180
|
+
# @return [Boolean] True on success
|
|
181
|
+
def stop
|
|
182
|
+
raise SystemdError, "systemd is not available on this system" unless systemd_available?
|
|
183
|
+
|
|
184
|
+
systemctl("stop", SERVICE_NAME)
|
|
185
|
+
true
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Restart the service
|
|
189
|
+
#
|
|
190
|
+
# @raise [SystemdError] If systemd unavailable or service not installed
|
|
191
|
+
# @return [Boolean] True on success
|
|
192
|
+
def restart
|
|
193
|
+
raise SystemdError, "systemd is not available on this system" unless systemd_available?
|
|
194
|
+
raise SystemdError, "Service is not installed" unless installed?
|
|
195
|
+
|
|
196
|
+
systemctl("restart", SERVICE_NAME)
|
|
197
|
+
true
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Get service status information
|
|
201
|
+
#
|
|
202
|
+
# @return [Hash] Status with :installed, :enabled, :active, :message keys
|
|
203
|
+
def status
|
|
204
|
+
{
|
|
205
|
+
installed: installed?,
|
|
206
|
+
enabled: enabled?,
|
|
207
|
+
active: active?,
|
|
208
|
+
message: status_message
|
|
209
|
+
}
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
private
|
|
213
|
+
|
|
214
|
+
# Get the systemd user directory path
|
|
215
|
+
#
|
|
216
|
+
# @return [String] Path to ~/.config/systemd/user
|
|
217
|
+
def systemd_user_dir
|
|
218
|
+
File.join(Dir.home, ".config", "systemd", "user")
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Get the full path to the service file
|
|
222
|
+
#
|
|
223
|
+
# @return [String] Path to service file
|
|
224
|
+
def service_file_path
|
|
225
|
+
File.join(systemd_user_dir, SERVICE_NAME)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Generate the service file content
|
|
229
|
+
#
|
|
230
|
+
# @return [String] systemd service unit content
|
|
231
|
+
def service_template
|
|
232
|
+
<<~SERVICE
|
|
233
|
+
[Unit]
|
|
234
|
+
Description=botiasloop - AI Agent Gateway
|
|
235
|
+
Documentation=https://github.com/anomalyco/botiasloop
|
|
236
|
+
After=network.target
|
|
237
|
+
|
|
238
|
+
[Service]
|
|
239
|
+
Type=simple
|
|
240
|
+
ExecStart=#{executable_path} gateway
|
|
241
|
+
Restart=on-failure
|
|
242
|
+
RestartSec=5
|
|
243
|
+
StandardOutput=journal
|
|
244
|
+
StandardError=journal
|
|
245
|
+
Environment="PATH=#{ruby_bin_path}:/usr/local/bin:/usr/bin:/bin"
|
|
246
|
+
|
|
247
|
+
[Install]
|
|
248
|
+
WantedBy=multi-user.target
|
|
249
|
+
SERVICE
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Get the path to the botiasloop executable
|
|
253
|
+
#
|
|
254
|
+
# @return [String] Path to botiasloop binary
|
|
255
|
+
def executable_path
|
|
256
|
+
Gem.bin_path("botiasloop", "botiasloop")
|
|
257
|
+
rescue Gem::Exception
|
|
258
|
+
# Fallback to searching in PATH
|
|
259
|
+
`which botiasloop 2>/dev/null`.strip
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Get the Ruby bin directory for PATH
|
|
263
|
+
#
|
|
264
|
+
# @return [String] Path to Ruby bin directory
|
|
265
|
+
def ruby_bin_path
|
|
266
|
+
File.dirname(RbConfig.ruby)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Execute a systemctl command
|
|
270
|
+
#
|
|
271
|
+
# @param args [Array<String>] Arguments to pass to systemctl
|
|
272
|
+
# @return [Boolean] True if command succeeded
|
|
273
|
+
def systemctl(*args)
|
|
274
|
+
system("systemctl", "--user", *args)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
# Execute a systemctl command quietly (no output)
|
|
278
|
+
#
|
|
279
|
+
# @param args [Array<String>] Arguments to pass to systemctl
|
|
280
|
+
# @return [Boolean] True if command succeeded
|
|
281
|
+
def systemctl_quiet(*args)
|
|
282
|
+
system("systemctl", "--user", *args, out: "/dev/null", err: "/dev/null")
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
# Get a human-readable status message
|
|
286
|
+
#
|
|
287
|
+
# @return [String] Status description
|
|
288
|
+
def status_message
|
|
289
|
+
if !installed?
|
|
290
|
+
"Service not installed"
|
|
291
|
+
elsif active?
|
|
292
|
+
"Service is running"
|
|
293
|
+
elsif enabled?
|
|
294
|
+
"Service enabled but stopped"
|
|
295
|
+
else
|
|
296
|
+
"Service installed but disabled"
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby_llm"
|
|
4
|
+
|
|
5
|
+
module Botiasloop
|
|
6
|
+
class Tool < RubyLLM::Tool
|
|
7
|
+
# Auto-generate tool name from class name in snake_case
|
|
8
|
+
# Example: WebSearch -> "web_search", MyCustomTool -> "my_custom_tool"
|
|
9
|
+
def self.tool_name
|
|
10
|
+
name
|
|
11
|
+
.split("::")
|
|
12
|
+
.last
|
|
13
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
14
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
15
|
+
.downcase
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Override RubyLLM's name method to use our tool_name
|
|
19
|
+
# This ensures the provider receives the correct tool name in schemas
|
|
20
|
+
def name
|
|
21
|
+
self.class.tool_name
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Botiasloop
|
|
4
|
+
module Tools
|
|
5
|
+
class Registry
|
|
6
|
+
attr_reader :tools
|
|
7
|
+
|
|
8
|
+
EMPTY_PARAMETERS_SCHEMA = {
|
|
9
|
+
"type" => "object",
|
|
10
|
+
"properties" => {},
|
|
11
|
+
"required" => [],
|
|
12
|
+
"additionalProperties" => false,
|
|
13
|
+
"strict" => true
|
|
14
|
+
}.freeze
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@tools = {}
|
|
18
|
+
@tool_instances = {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Register a tool class
|
|
22
|
+
#
|
|
23
|
+
# @param tool_class [Class] Tool class to register
|
|
24
|
+
# @param args [Hash] Arguments to pass to tool constructor
|
|
25
|
+
def register(tool_class, **args)
|
|
26
|
+
@tools[tool_class.tool_name] = tool_class
|
|
27
|
+
@tool_instances[tool_class.tool_name] = args
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Deregister a tool by name
|
|
31
|
+
#
|
|
32
|
+
# @param name [String] Tool name to deregister
|
|
33
|
+
def deregister(name)
|
|
34
|
+
@tools.delete(name)
|
|
35
|
+
@tool_instances.delete(name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Generate OpenAI-compatible tool schemas
|
|
39
|
+
# @return [Hash] Hash of tool instances keyed by tool name symbol
|
|
40
|
+
def schemas
|
|
41
|
+
@tools.transform_values do |tool_class|
|
|
42
|
+
args = @tool_instances[tool_class.tool_name]
|
|
43
|
+
args ? tool_class.new(**args) : tool_class.new
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# @return [Array<Class>] Array of registered tool classes
|
|
48
|
+
def tool_classes
|
|
49
|
+
@tools.values
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Execute a tool by name
|
|
53
|
+
#
|
|
54
|
+
# @param name [String] Tool name
|
|
55
|
+
# @param arguments [Hash] Tool arguments
|
|
56
|
+
# @return [Hash] Tool result
|
|
57
|
+
# @raise [Error] If tool not found
|
|
58
|
+
def execute(name, arguments)
|
|
59
|
+
tool_class = @tools[name]
|
|
60
|
+
raise Error, "Unknown tool: #{name}" unless tool_class
|
|
61
|
+
|
|
62
|
+
args = @tool_instances[name]
|
|
63
|
+
tool = args ? tool_class.new(**args) : tool_class.new
|
|
64
|
+
tool.execute(**arguments.transform_keys(&:to_sym))
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require_relative "../tool"
|
|
5
|
+
|
|
6
|
+
module Botiasloop
|
|
7
|
+
module Tools
|
|
8
|
+
class Shell < Tool
|
|
9
|
+
description "Execute a shell command and return the output"
|
|
10
|
+
param :command, type: :string, desc: "The shell command to execute", required: true
|
|
11
|
+
|
|
12
|
+
# Execute a shell command
|
|
13
|
+
#
|
|
14
|
+
# @param command [String] Shell command to execute
|
|
15
|
+
# @return [Hash] Result with stdout, stderr, exit_code, and success?
|
|
16
|
+
def execute(command:)
|
|
17
|
+
stdout, stderr, status = Open3.capture3(command)
|
|
18
|
+
Result.new(stdout, stderr, status.exitstatus).to_h
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Result wrapper for shell execution
|
|
22
|
+
class Result
|
|
23
|
+
attr_reader :stdout, :stderr, :exit_code
|
|
24
|
+
|
|
25
|
+
def initialize(stdout, stderr, exit_code)
|
|
26
|
+
@stdout = stdout
|
|
27
|
+
@stderr = stderr
|
|
28
|
+
@exit_code = exit_code
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def success?
|
|
32
|
+
@exit_code == 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_s
|
|
36
|
+
"Exit: #{@exit_code}\nStdout:\n#{@stdout}\nStderr:\n#{@stderr}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{
|
|
41
|
+
stdout: @stdout,
|
|
42
|
+
stderr: @stderr,
|
|
43
|
+
exit_code: @exit_code,
|
|
44
|
+
success?: success?
|
|
45
|
+
}
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
require "uri"
|
|
6
|
+
require_relative "../tool"
|
|
7
|
+
|
|
8
|
+
module Botiasloop
|
|
9
|
+
module Tools
|
|
10
|
+
class WebSearch < Tool
|
|
11
|
+
description "Search the web using SearXNG"
|
|
12
|
+
param :query, type: :string, desc: "The search query", required: true
|
|
13
|
+
|
|
14
|
+
# Initialize with SearXNG URL
|
|
15
|
+
#
|
|
16
|
+
# @param searxng_url [String] SearXNG instance URL
|
|
17
|
+
def initialize(searxng_url)
|
|
18
|
+
@searxng_url = searxng_url
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Execute web search
|
|
22
|
+
#
|
|
23
|
+
# @param query [String] Search query
|
|
24
|
+
# @return [Hash] Search results
|
|
25
|
+
# @raise [Error] On HTTP or connection errors
|
|
26
|
+
def execute(query:)
|
|
27
|
+
uri = URI("#{@searxng_url}/search")
|
|
28
|
+
uri.query = URI.encode_www_form("q" => query, "format" => "json")
|
|
29
|
+
|
|
30
|
+
response = Net::HTTP.get_response(uri)
|
|
31
|
+
|
|
32
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
33
|
+
raise Error, "Search failed: HTTP #{response.code}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
data = JSON.parse(response.body)
|
|
37
|
+
Result.new(data["results"] || []).to_h
|
|
38
|
+
rescue Errno::ECONNREFUSED => e
|
|
39
|
+
raise Error, "Search failed: #{e.message}"
|
|
40
|
+
rescue JSON::ParserError => e
|
|
41
|
+
raise Error, "Search failed: Invalid JSON response - #{e.message}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Result wrapper for search results
|
|
45
|
+
class Result
|
|
46
|
+
attr_reader :results
|
|
47
|
+
|
|
48
|
+
def initialize(results)
|
|
49
|
+
@results = results
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def to_s
|
|
53
|
+
@results.map do |r|
|
|
54
|
+
"#{r["title"]}\n#{r["url"]}\n#{r["content"]}"
|
|
55
|
+
end.join("\n\n")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def to_h
|
|
59
|
+
{results: @results}
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/botiasloop.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "botiasloop/version"
|
|
4
|
+
require_relative "botiasloop/config"
|
|
5
|
+
require_relative "botiasloop/database"
|
|
6
|
+
|
|
7
|
+
require_relative "botiasloop/conversation"
|
|
8
|
+
require_relative "botiasloop/conversation_manager"
|
|
9
|
+
require_relative "botiasloop/tool"
|
|
10
|
+
require_relative "botiasloop/tools/registry"
|
|
11
|
+
require_relative "botiasloop/tools/shell"
|
|
12
|
+
require_relative "botiasloop/tools/web_search"
|
|
13
|
+
require_relative "botiasloop/skills/skill"
|
|
14
|
+
require_relative "botiasloop/skills/loader"
|
|
15
|
+
require_relative "botiasloop/skills/registry"
|
|
16
|
+
require_relative "botiasloop/commands"
|
|
17
|
+
require_relative "botiasloop/loop"
|
|
18
|
+
require_relative "botiasloop/agent"
|
|
19
|
+
require_relative "botiasloop/channels"
|
|
20
|
+
require_relative "botiasloop/channels/base"
|
|
21
|
+
require_relative "botiasloop/channels/cli"
|
|
22
|
+
require_relative "botiasloop/channels/telegram"
|
|
23
|
+
require_relative "botiasloop/channels_manager"
|
|
24
|
+
require_relative "botiasloop/systemd_service"
|
|
25
|
+
|
|
26
|
+
module Botiasloop
|
|
27
|
+
class Error < StandardError; end
|
|
28
|
+
|
|
29
|
+
# Error raised for systemd service operation failures
|
|
30
|
+
class SystemdError < Error; end
|
|
31
|
+
|
|
32
|
+
# @return [String] Root directory of the gem
|
|
33
|
+
def self.root
|
|
34
|
+
File.dirname(__dir__)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class MaxIterationsExceeded < Error
|
|
38
|
+
attr_reader :max_iterations
|
|
39
|
+
|
|
40
|
+
def initialize(max_iterations)
|
|
41
|
+
@max_iterations = max_iterations
|
|
42
|
+
super("I've reached my thinking limit (#{max_iterations} iterations). Please try a more specific question.")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|