botiasloop 0.0.1 → 0.0.7
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 +4 -4
- data/LICENSE +7 -0
- data/README.md +266 -122
- data/bin/botiasloop +65 -15
- data/lib/botiasloop/agent.rb +25 -12
- data/lib/botiasloop/auto_label.rb +117 -0
- data/lib/botiasloop/channels/base.rb +48 -44
- data/lib/botiasloop/channels/cli.rb +14 -18
- data/lib/botiasloop/channels/telegram.rb +95 -42
- data/lib/botiasloop/channels_manager.rb +23 -30
- data/lib/botiasloop/chat.rb +122 -0
- data/lib/botiasloop/commands/archive.rb +34 -11
- data/lib/botiasloop/commands/compact.rb +1 -1
- data/lib/botiasloop/commands/context.rb +6 -6
- data/lib/botiasloop/commands/conversations.rb +11 -6
- data/lib/botiasloop/commands/label.rb +9 -11
- data/lib/botiasloop/commands/new.rb +2 -2
- data/lib/botiasloop/commands/status.rb +2 -2
- data/lib/botiasloop/commands/switch.rb +5 -7
- data/lib/botiasloop/commands/verbose.rb +29 -0
- data/lib/botiasloop/commands.rb +1 -0
- data/lib/botiasloop/config.rb +16 -0
- data/lib/botiasloop/conversation.rb +100 -11
- data/lib/botiasloop/database.rb +16 -4
- data/lib/botiasloop/human_id.rb +58 -0
- data/lib/botiasloop/logger.rb +45 -0
- data/lib/botiasloop/loop.rb +88 -7
- data/lib/botiasloop/systemd_service.rb +20 -10
- data/lib/botiasloop/tools/shell.rb +5 -0
- data/lib/botiasloop/version.rb +1 -1
- data/lib/botiasloop.rb +8 -1
- metadata +46 -27
- data/lib/botiasloop/conversation_manager.rb +0 -225
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
module Botiasloop
|
|
4
4
|
module Commands
|
|
5
5
|
# Context object passed to command executions
|
|
6
|
-
# Provides access to conversation, config, channel, and user info
|
|
6
|
+
# Provides access to conversation, chat, config, channel, and user info
|
|
7
7
|
class Context
|
|
8
8
|
# @return [Conversation] The current conversation
|
|
9
9
|
attr_accessor :conversation
|
|
10
10
|
|
|
11
|
-
# @return [
|
|
12
|
-
attr_reader :
|
|
11
|
+
# @return [Chat] The chat this conversation belongs to
|
|
12
|
+
attr_reader :chat
|
|
13
13
|
|
|
14
14
|
# @return [Channels::Base, nil] The channel instance (nil in CLI)
|
|
15
15
|
attr_reader :channel
|
|
@@ -20,12 +20,12 @@ module Botiasloop
|
|
|
20
20
|
# Initialize context
|
|
21
21
|
#
|
|
22
22
|
# @param conversation [Conversation] The current conversation
|
|
23
|
-
# @param
|
|
23
|
+
# @param chat [Chat] The chat this conversation belongs to
|
|
24
24
|
# @param channel [Channels::Base, nil] The channel instance (nil in CLI)
|
|
25
25
|
# @param user_id [String, nil] The user/source identifier
|
|
26
|
-
def initialize(conversation:,
|
|
26
|
+
def initialize(conversation:, chat:, channel: nil, user_id: nil)
|
|
27
27
|
@conversation = conversation
|
|
28
|
-
@
|
|
28
|
+
@chat = chat
|
|
29
29
|
@channel = channel
|
|
30
30
|
@user_id = user_id
|
|
31
31
|
end
|
|
@@ -16,10 +16,15 @@ module Botiasloop
|
|
|
16
16
|
# @return [String] Formatted list of conversations
|
|
17
17
|
def execute(context, args = nil)
|
|
18
18
|
show_archived = args.to_s.strip.downcase == "archived"
|
|
19
|
-
|
|
20
|
-
current_uuid = ConversationManager.current_uuid_for(context.user_id)
|
|
19
|
+
current_conversation = context.conversation
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
if show_archived
|
|
22
|
+
conversations = context.chat.archived_conversations
|
|
23
|
+
lines = ["**Archived Conversations**"]
|
|
24
|
+
else
|
|
25
|
+
conversations = context.chat.active_conversations
|
|
26
|
+
lines = ["**Conversations**"]
|
|
27
|
+
end
|
|
23
28
|
|
|
24
29
|
if conversations.empty?
|
|
25
30
|
lines << (show_archived ? "No archived conversations found." : "No conversations found.")
|
|
@@ -27,10 +32,10 @@ module Botiasloop
|
|
|
27
32
|
end
|
|
28
33
|
|
|
29
34
|
conversations.each do |conv|
|
|
30
|
-
prefix = (conv
|
|
31
|
-
label = conv
|
|
35
|
+
prefix = (conv.id == current_conversation.id) ? "[current] " : ""
|
|
36
|
+
label = conv.label
|
|
32
37
|
suffix = label ? " (#{label})" : ""
|
|
33
|
-
lines << "#{prefix}#{conv
|
|
38
|
+
lines << "#{prefix}#{conv.id}#{suffix}"
|
|
34
39
|
end
|
|
35
40
|
|
|
36
41
|
lines.join("\n")
|
|
@@ -14,7 +14,6 @@ module Botiasloop
|
|
|
14
14
|
# @return [String] Command response
|
|
15
15
|
def execute(context, args = nil)
|
|
16
16
|
conversation = context.conversation
|
|
17
|
-
user_id = context.user_id
|
|
18
17
|
|
|
19
18
|
if args.nil? || args.strip.empty?
|
|
20
19
|
# Show current label
|
|
@@ -23,7 +22,7 @@ module Botiasloop
|
|
|
23
22
|
|
|
24
23
|
# Set label
|
|
25
24
|
label_value = args.strip
|
|
26
|
-
set_label(conversation,
|
|
25
|
+
set_label(conversation, label_value)
|
|
27
26
|
end
|
|
28
27
|
|
|
29
28
|
private
|
|
@@ -36,28 +35,27 @@ module Botiasloop
|
|
|
36
35
|
end
|
|
37
36
|
end
|
|
38
37
|
|
|
39
|
-
def set_label(conversation,
|
|
38
|
+
def set_label(conversation, label_value)
|
|
40
39
|
# Validate label format
|
|
41
40
|
unless label_value.match?(/\A[a-zA-Z0-9_-]+\z/)
|
|
42
41
|
return "Invalid label format. Use only letters, numbers, dashes, and underscores."
|
|
43
42
|
end
|
|
44
43
|
|
|
45
|
-
# Check uniqueness
|
|
46
|
-
if label_in_use?(
|
|
44
|
+
# Check uniqueness globally
|
|
45
|
+
if label_in_use?(label_value, conversation.id)
|
|
47
46
|
return "Label '#{label_value}' already in use by another conversation."
|
|
48
47
|
end
|
|
49
48
|
|
|
50
|
-
conversation.
|
|
49
|
+
conversation.label = label_value
|
|
50
|
+
conversation.save
|
|
51
51
|
"Label set to: #{label_value}"
|
|
52
52
|
rescue Botiasloop::Error => e
|
|
53
53
|
"Error setting label: #{e.message}"
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
def label_in_use?(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
existing_uuid = ConversationManager.find_by_label(user_id, label)
|
|
60
|
-
existing_uuid && existing_uuid != current_uuid
|
|
56
|
+
def label_in_use?(label, current_id)
|
|
57
|
+
other = Conversation.find(label: label)
|
|
58
|
+
other && other.id != current_id
|
|
61
59
|
end
|
|
62
60
|
end
|
|
63
61
|
end
|
|
@@ -10,10 +10,10 @@ module Botiasloop
|
|
|
10
10
|
description "Start a new conversation"
|
|
11
11
|
|
|
12
12
|
def execute(context, _args = nil)
|
|
13
|
-
new_conversation =
|
|
13
|
+
new_conversation = context.chat.create_new_conversation
|
|
14
14
|
context.conversation = new_conversation
|
|
15
15
|
|
|
16
|
-
"**New conversation started (
|
|
16
|
+
"**New conversation started (ID: #{new_conversation.uuid}).**\n" \
|
|
17
17
|
"Use `/switch #{new_conversation.uuid}` to return later."
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -9,10 +9,10 @@ module Botiasloop
|
|
|
9
9
|
|
|
10
10
|
def execute(context, _args = nil)
|
|
11
11
|
conversation = context.conversation
|
|
12
|
-
config =
|
|
12
|
+
config = Config.instance
|
|
13
13
|
|
|
14
14
|
lines = ["**Conversation Status**"]
|
|
15
|
-
lines << "
|
|
15
|
+
lines << "ID: #{conversation.uuid}"
|
|
16
16
|
lines << "Label: #{format_label(conversation)}"
|
|
17
17
|
lines << "Model: #{config.providers["openrouter"]["model"]}"
|
|
18
18
|
lines << "Max iterations: #{config.max_iterations}"
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module Botiasloop
|
|
4
4
|
module Commands
|
|
5
|
-
# Switch command - switches to a different conversation by label or
|
|
5
|
+
# Switch command - switches to a different conversation by label or ID
|
|
6
6
|
class Switch < Base
|
|
7
7
|
command :switch
|
|
8
|
-
description "Switch to a different conversation by label or
|
|
8
|
+
description "Switch to a different conversation by label or ID"
|
|
9
9
|
|
|
10
10
|
# Execute the switch command
|
|
11
11
|
#
|
|
@@ -15,11 +15,9 @@ module Botiasloop
|
|
|
15
15
|
def execute(context, args = nil)
|
|
16
16
|
identifier = args.to_s.strip
|
|
17
17
|
|
|
18
|
-
if identifier.empty?
|
|
19
|
-
return "Usage: /switch <label-or-uuid>"
|
|
20
|
-
end
|
|
18
|
+
return "Usage: /switch <label-or-id>" if identifier.empty?
|
|
21
19
|
|
|
22
|
-
new_conversation =
|
|
20
|
+
new_conversation = context.chat.switch_conversation(identifier)
|
|
23
21
|
context.conversation = new_conversation
|
|
24
22
|
|
|
25
23
|
format_switch_response(new_conversation)
|
|
@@ -31,7 +29,7 @@ module Botiasloop
|
|
|
31
29
|
|
|
32
30
|
def format_switch_response(conversation)
|
|
33
31
|
lines = ["**Conversation switched**"]
|
|
34
|
-
lines << "-
|
|
32
|
+
lines << "- ID: #{conversation.uuid}"
|
|
35
33
|
|
|
36
34
|
lines << if conversation.label?
|
|
37
35
|
"- Label: #{conversation.label}"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Botiasloop
|
|
4
|
+
module Commands
|
|
5
|
+
# Verbose command - controls verbose mode for tool call output
|
|
6
|
+
class Verbose < Base
|
|
7
|
+
command :verbose
|
|
8
|
+
description "Toggle verbose mode (shows reasoning and tool calls). Usage: /verbose [on|off]"
|
|
9
|
+
|
|
10
|
+
def execute(context, args = nil)
|
|
11
|
+
conversation = context.conversation
|
|
12
|
+
|
|
13
|
+
case args&.downcase&.strip
|
|
14
|
+
when "on"
|
|
15
|
+
conversation.update(verbose: true)
|
|
16
|
+
"Verbose mode enabled. Tool calls will be shown."
|
|
17
|
+
when "off"
|
|
18
|
+
conversation.update(verbose: false)
|
|
19
|
+
"Verbose mode disabled. Tool calls will be hidden."
|
|
20
|
+
when nil, ""
|
|
21
|
+
status = conversation.verbose ? "on" : "off"
|
|
22
|
+
"Verbose mode is currently #{status}. Usage: /verbose [on|off]"
|
|
23
|
+
else
|
|
24
|
+
"Unknown argument: #{args}. Usage: /verbose [on|off]"
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/botiasloop/commands.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "commands/base"
|
|
|
5
5
|
require_relative "commands/registry"
|
|
6
6
|
require_relative "commands/help"
|
|
7
7
|
require_relative "commands/status"
|
|
8
|
+
require_relative "commands/verbose"
|
|
8
9
|
require_relative "commands/reset"
|
|
9
10
|
require_relative "commands/new"
|
|
10
11
|
require_relative "commands/compact"
|
data/lib/botiasloop/config.rb
CHANGED
|
@@ -6,8 +6,19 @@ Anyway::Settings.default_config_path = ->(_) { File.expand_path("~/.config/botia
|
|
|
6
6
|
|
|
7
7
|
module Botiasloop
|
|
8
8
|
class Config < Anyway::Config
|
|
9
|
+
@instance = nil
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def instance
|
|
13
|
+
@instance ||= new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
attr_writer :instance
|
|
17
|
+
end
|
|
18
|
+
|
|
9
19
|
attr_config \
|
|
10
20
|
max_iterations: 20,
|
|
21
|
+
log_level: "info",
|
|
11
22
|
tools: {
|
|
12
23
|
web_search: {}
|
|
13
24
|
},
|
|
@@ -23,6 +34,11 @@ module Botiasloop
|
|
|
23
34
|
},
|
|
24
35
|
commands: {
|
|
25
36
|
summarize: {}
|
|
37
|
+
},
|
|
38
|
+
features: {
|
|
39
|
+
auto_labelling: {
|
|
40
|
+
enabled: true
|
|
41
|
+
}
|
|
26
42
|
}
|
|
27
43
|
|
|
28
44
|
# Validation
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "securerandom"
|
|
4
3
|
require "time"
|
|
5
4
|
|
|
6
5
|
module Botiasloop
|
|
7
6
|
# Conversation model - represents a chat conversation with messages
|
|
8
7
|
# Direct Sequel model with all database and business logic combined
|
|
9
8
|
class Conversation < Sequel::Model(:conversations)
|
|
9
|
+
# Valid label format: alphanumeric, dashes, and underscores
|
|
10
|
+
LABEL_REGEX = /\A[a-zA-Z0-9_-]+\z/
|
|
11
|
+
|
|
10
12
|
# Message model nested within Conversation namespace
|
|
11
13
|
class Message < Sequel::Model(:messages)
|
|
12
14
|
plugin :validation_helpers
|
|
@@ -17,7 +19,7 @@ module Botiasloop
|
|
|
17
19
|
# Validations
|
|
18
20
|
def validate
|
|
19
21
|
super
|
|
20
|
-
validates_presence [
|
|
22
|
+
validates_presence %i[conversation_id role content]
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
# Convert message to hash for API compatibility
|
|
@@ -42,21 +44,21 @@ module Botiasloop
|
|
|
42
44
|
# Allow setting primary key (id) for UUID
|
|
43
45
|
unrestrict_primary_key
|
|
44
46
|
|
|
45
|
-
# Auto-generate
|
|
47
|
+
# Auto-generate human-readable ID before creation if not provided
|
|
46
48
|
def before_create
|
|
47
|
-
self.id ||=
|
|
49
|
+
self.id ||= HumanId.generate
|
|
48
50
|
super
|
|
49
51
|
end
|
|
50
52
|
|
|
51
53
|
# Validations
|
|
52
54
|
def validate
|
|
53
55
|
super
|
|
54
|
-
validates_presence [:user_id]
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
return unless label && !label.to_s.empty?
|
|
58
|
+
|
|
59
|
+
validates_format LABEL_REGEX, :label,
|
|
60
|
+
message: "Invalid label format. Use only letters, numbers, dashes, and underscores."
|
|
61
|
+
validates_unique :label, message: "Label '#{label}' already in use by another conversation"
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
# Check if this conversation has a label
|
|
@@ -66,6 +68,30 @@ module Botiasloop
|
|
|
66
68
|
!label.nil? && !label.to_s.empty?
|
|
67
69
|
end
|
|
68
70
|
|
|
71
|
+
# Check if this conversation is archived
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean] True if archived
|
|
74
|
+
def archived?
|
|
75
|
+
archived == true
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Set the label for this conversation
|
|
79
|
+
#
|
|
80
|
+
# @param value [String] Label value
|
|
81
|
+
def label=(value)
|
|
82
|
+
# Allow empty string to be treated as nil (clearing the label)
|
|
83
|
+
value = nil if value.to_s.empty?
|
|
84
|
+
super
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Archive this conversation
|
|
88
|
+
#
|
|
89
|
+
# @return [Conversation] self
|
|
90
|
+
def archive!
|
|
91
|
+
update(archived: true)
|
|
92
|
+
self
|
|
93
|
+
end
|
|
94
|
+
|
|
69
95
|
# Get the timestamp of the last activity in the conversation
|
|
70
96
|
#
|
|
71
97
|
# @return [String, nil] ISO8601 timestamp of last message, or nil if no messages
|
|
@@ -126,10 +152,10 @@ module Botiasloop
|
|
|
126
152
|
messages_dataset.order(:timestamp).map(&:to_hash)
|
|
127
153
|
end
|
|
128
154
|
|
|
129
|
-
# @return [String]
|
|
155
|
+
# @return [String] Human-readable ID of the conversation
|
|
130
156
|
def uuid
|
|
131
157
|
# Return existing id or generate a new one for unsaved records
|
|
132
|
-
self.id ||=
|
|
158
|
+
self.id ||= HumanId.generate
|
|
133
159
|
end
|
|
134
160
|
|
|
135
161
|
# Reset conversation - clear all messages and reset token counts
|
|
@@ -183,7 +209,70 @@ module Botiasloop
|
|
|
183
209
|
SKILLS
|
|
184
210
|
end
|
|
185
211
|
|
|
212
|
+
prompt += build_operator_section
|
|
213
|
+
prompt += build_identity_section
|
|
214
|
+
|
|
186
215
|
prompt
|
|
187
216
|
end
|
|
217
|
+
|
|
218
|
+
private
|
|
219
|
+
|
|
220
|
+
def build_identity_section
|
|
221
|
+
path = File.expand_path("~/IDENTITY.md")
|
|
222
|
+
content = File.exist?(path) ? File.read(path).strip : nil
|
|
223
|
+
|
|
224
|
+
section = "\n\nIDENTITY.md\nDefines who you are - your name, personality, and how you behave.\n\n"
|
|
225
|
+
|
|
226
|
+
if content && !content.empty?
|
|
227
|
+
section += "#{content}\n\n"
|
|
228
|
+
section += "You can update ~/IDENTITY.md when the operator wants you to act differently or call you differently (eg. 'Be more concise from now on')."
|
|
229
|
+
elsif content && content.empty?
|
|
230
|
+
section += "CRITICAL: This file is empty. After setting up OPERATOR.md, ask the operator:\n\n"
|
|
231
|
+
section += "1. What name should I use for myself?\n"
|
|
232
|
+
section += "2. How formal or casual should I be?\n"
|
|
233
|
+
section += "3. Should I be concise or detailed in responses?\n"
|
|
234
|
+
section += "4. Any other personality preferences?\n\n"
|
|
235
|
+
section += "Then create ~/IDENTITY.md with this information."
|
|
236
|
+
else
|
|
237
|
+
section += "CRITICAL: This file does not exist. After setting up OPERATOR.md, ask the operator:\n\n"
|
|
238
|
+
section += "1. What name should I use for myself?\n"
|
|
239
|
+
section += "2. How formal or casual should I be?\n"
|
|
240
|
+
section += "3. Should I be concise or detailed in responses?\n"
|
|
241
|
+
section += "4. Any other personality preferences?\n\n"
|
|
242
|
+
section += "Then create ~/IDENTITY.md with: `cat > ~/IDENTITY.md << 'EOF'\n[content here]\nEOF`"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
section
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def build_operator_section
|
|
249
|
+
path = File.expand_path("~/OPERATOR.md")
|
|
250
|
+
content = File.exist?(path) ? File.read(path).strip : nil
|
|
251
|
+
|
|
252
|
+
section = "\n\nOPERATOR.md\nInformation about the operator - their preferences, context, background, and more.\n\n"
|
|
253
|
+
|
|
254
|
+
if content && !content.empty?
|
|
255
|
+
section += "#{content}\n\n"
|
|
256
|
+
section += "You can update ~/OPERATOR.md when you learn something new about the operator (eg. their name, birthday, what they like, etc.)."
|
|
257
|
+
elsif content && content.empty?
|
|
258
|
+
section += "CRITICAL: This file is empty. Before helping with other tasks, you MUST:\n\n"
|
|
259
|
+
section += "1. Ask the operator their name\n"
|
|
260
|
+
section += "2. Ask their location and timezone\n"
|
|
261
|
+
section += "3. Ask their preferred language\n"
|
|
262
|
+
section += "4. Ask about their interests, hobbies, or work\n"
|
|
263
|
+
section += "5. Ask any relevant personal context they're comfortable sharing (birthday, family, etc.)\n\n"
|
|
264
|
+
section += "Then create ~/OPERATOR.md with this information."
|
|
265
|
+
else
|
|
266
|
+
section += "CRITICAL: This file does not exist. Before helping with other tasks, you MUST:\n\n"
|
|
267
|
+
section += "1. Ask the operator their name\n"
|
|
268
|
+
section += "2. Ask their location and timezone\n"
|
|
269
|
+
section += "3. Ask their preferred language\n"
|
|
270
|
+
section += "4. Ask about their interests, hobbies, or work\n"
|
|
271
|
+
section += "5. Ask any relevant personal context they're comfortable sharing (birthday, family, etc.)\n\n"
|
|
272
|
+
section += "Then create ~/OPERATOR.md with: `cat > ~/OPERATOR.md << 'EOF'\n[content here]\nEOF`"
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
section
|
|
276
|
+
end
|
|
188
277
|
end
|
|
189
278
|
end
|
data/lib/botiasloop/database.rb
CHANGED
|
@@ -34,6 +34,7 @@ module Botiasloop
|
|
|
34
34
|
db = connect
|
|
35
35
|
db[:messages].delete if db.table_exists?(:messages)
|
|
36
36
|
db[:conversations].delete if db.table_exists?(:conversations)
|
|
37
|
+
db[:chats].delete if db.table_exists?(:chats)
|
|
37
38
|
end
|
|
38
39
|
|
|
39
40
|
# Close database connection
|
|
@@ -52,20 +53,31 @@ module Botiasloop
|
|
|
52
53
|
# Ensure directory exists
|
|
53
54
|
FileUtils.mkdir_p(File.dirname(DEFAULT_PATH))
|
|
54
55
|
|
|
56
|
+
# Create chats table
|
|
57
|
+
db.create_table?(:chats) do
|
|
58
|
+
primary_key :id
|
|
59
|
+
String :channel, null: false
|
|
60
|
+
String :external_id, null: false
|
|
61
|
+
String :user_identifier
|
|
62
|
+
String :current_conversation_id
|
|
63
|
+
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
|
|
64
|
+
DateTime :updated_at, default: Sequel::CURRENT_TIMESTAMP
|
|
65
|
+
|
|
66
|
+
index %i[channel external_id], unique: true
|
|
67
|
+
end
|
|
68
|
+
|
|
55
69
|
# Create conversations table
|
|
56
70
|
db.create_table?(:conversations) do
|
|
57
71
|
String :id, primary_key: true
|
|
58
|
-
String :user_id, null: false
|
|
59
72
|
String :label
|
|
60
|
-
TrueClass :is_current, default: false
|
|
61
73
|
TrueClass :archived, default: false
|
|
74
|
+
TrueClass :verbose, default: false
|
|
62
75
|
Integer :input_tokens, default: 0
|
|
63
76
|
Integer :output_tokens, default: 0
|
|
64
77
|
DateTime :created_at, default: Sequel::CURRENT_TIMESTAMP
|
|
65
78
|
DateTime :updated_at, default: Sequel::CURRENT_TIMESTAMP
|
|
66
79
|
|
|
67
|
-
index
|
|
68
|
-
index [:user_id, :archived]
|
|
80
|
+
index :label, unique: true
|
|
69
81
|
end
|
|
70
82
|
|
|
71
83
|
# Create messages table
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffaker"
|
|
4
|
+
|
|
5
|
+
module Botiasloop
|
|
6
|
+
# Generates human-readable identifiers for conversations
|
|
7
|
+
# Format: color-animal-XXX (e.g., blue-dog-123)
|
|
8
|
+
# All IDs are stored and compared in lowercase for case-insensitivity
|
|
9
|
+
module HumanId
|
|
10
|
+
MAX_RETRIES = 10
|
|
11
|
+
|
|
12
|
+
# Generate a unique human-readable ID
|
|
13
|
+
# Checks database for collisions and retries if needed
|
|
14
|
+
#
|
|
15
|
+
# @return [String] Unique ID in format color-animal-XXX
|
|
16
|
+
# @raise [Error] If unable to generate unique ID after max retries
|
|
17
|
+
def self.generate
|
|
18
|
+
retries = 0
|
|
19
|
+
|
|
20
|
+
loop do
|
|
21
|
+
id = build_id
|
|
22
|
+
return id unless exists?(id)
|
|
23
|
+
|
|
24
|
+
retries += 1
|
|
25
|
+
raise Error, "Failed to generate unique ID after #{MAX_RETRIES} attempts" if retries >= MAX_RETRIES
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Normalize an ID to lowercase for storage and comparison
|
|
30
|
+
#
|
|
31
|
+
# @param id [String, nil] ID to normalize
|
|
32
|
+
# @return [String] Normalized lowercase ID
|
|
33
|
+
def self.normalize(id)
|
|
34
|
+
id.to_s.downcase.strip
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Check if an ID already exists in the database
|
|
38
|
+
# Case-insensitive comparison
|
|
39
|
+
#
|
|
40
|
+
# @param id [String] ID to check
|
|
41
|
+
# @return [Boolean] True if ID exists
|
|
42
|
+
def self.exists?(id)
|
|
43
|
+
normalized = normalize(id)
|
|
44
|
+
Conversation.where(Sequel.function(:lower, :id) => normalized).count > 0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Build a single ID attempt
|
|
48
|
+
#
|
|
49
|
+
# @return [String] ID in format color-animal-XXX
|
|
50
|
+
def self.build_id
|
|
51
|
+
color = FFaker::Color.name.downcase.tr(" ", "-")
|
|
52
|
+
animal = FFaker::AnimalUS.common_name.downcase.tr(" ", "-")
|
|
53
|
+
number = rand(100..999)
|
|
54
|
+
|
|
55
|
+
"#{color}-#{animal}-#{number}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "logger"
|
|
4
|
+
|
|
5
|
+
module Botiasloop
|
|
6
|
+
module Logger
|
|
7
|
+
class << self
|
|
8
|
+
def logger
|
|
9
|
+
@logger ||= create_logger
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def debug(msg)
|
|
13
|
+
logger.debug(msg)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def info(msg)
|
|
17
|
+
logger.info(msg)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def warn(msg)
|
|
21
|
+
logger.warn(msg)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def error(msg)
|
|
25
|
+
logger.error(msg)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def create_logger
|
|
31
|
+
level = begin
|
|
32
|
+
config = Botiasloop::Config.new
|
|
33
|
+
::Logger.const_get(config.log_level.to_s.upcase)
|
|
34
|
+
rescue
|
|
35
|
+
::Logger::INFO
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
log = ::Logger.new($stderr)
|
|
39
|
+
log.level = level
|
|
40
|
+
log.formatter = proc { |_severity, _datetime, _progname, msg| "#{msg}\n" }
|
|
41
|
+
log
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|