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
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Botiasloop
|
|
4
|
-
# Manages conversation state globally, mapping user IDs to conversation UUIDs
|
|
5
|
-
# Handles all business logic: switching, finding current, labeling, etc.
|
|
6
|
-
class ConversationManager
|
|
7
|
-
# Valid label format: alphanumeric, dashes, and underscores
|
|
8
|
-
LABEL_REGEX = /\A[a-zA-Z0-9_-]+\z/
|
|
9
|
-
|
|
10
|
-
class << self
|
|
11
|
-
# Get or create the current conversation for a user
|
|
12
|
-
#
|
|
13
|
-
# @param user_id [String] User identifier
|
|
14
|
-
# @return [Conversation] Current conversation for the user
|
|
15
|
-
def current_for(user_id)
|
|
16
|
-
user_key = user_id.to_s
|
|
17
|
-
conversation = Conversation.where(user_id: user_key, is_current: true, archived: false).first
|
|
18
|
-
|
|
19
|
-
if conversation
|
|
20
|
-
Conversation[conversation.id]
|
|
21
|
-
else
|
|
22
|
-
create_new(user_key)
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Switch a user to a different conversation by label or UUID
|
|
27
|
-
# Auto-unarchives archived conversations when switching to them
|
|
28
|
-
#
|
|
29
|
-
# @param user_id [String] User identifier
|
|
30
|
-
# @param identifier [String] Conversation label or UUID to switch to
|
|
31
|
-
# @return [Conversation] The switched-to conversation
|
|
32
|
-
# @raise [Error] If conversation with given identifier doesn't exist
|
|
33
|
-
def switch(user_id, identifier)
|
|
34
|
-
user_key = user_id.to_s
|
|
35
|
-
identifier = identifier.to_s.strip
|
|
36
|
-
|
|
37
|
-
raise Error, "Usage: /switch <label-or-uuid>" if identifier.empty?
|
|
38
|
-
|
|
39
|
-
# First try to find by label (include archived)
|
|
40
|
-
conversation = Conversation.where(user_id: user_key, label: identifier).first
|
|
41
|
-
|
|
42
|
-
# If not found by label, treat as UUID (include archived)
|
|
43
|
-
conversation ||= Conversation.find(id: identifier, user_id: user_key)
|
|
44
|
-
|
|
45
|
-
raise Error, "Conversation '#{identifier}' not found" unless conversation
|
|
46
|
-
|
|
47
|
-
# Auto-unarchive if switching to an archived conversation
|
|
48
|
-
conversation.update(archived: false) if conversation.archived
|
|
49
|
-
|
|
50
|
-
# Clear current flag from all user's conversations
|
|
51
|
-
Conversation.where(user_id: user_key).update(is_current: false)
|
|
52
|
-
|
|
53
|
-
# Set new conversation as current
|
|
54
|
-
conversation.update(is_current: true)
|
|
55
|
-
Conversation[conversation.id]
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Create a new conversation and switch the user to it
|
|
59
|
-
#
|
|
60
|
-
# @param user_id [String] User identifier
|
|
61
|
-
# @return [Conversation] The newly created conversation
|
|
62
|
-
def create_new(user_id)
|
|
63
|
-
user_key = user_id.to_s
|
|
64
|
-
|
|
65
|
-
# Clear current flag from all user's conversations
|
|
66
|
-
Conversation.where(user_id: user_key).update(is_current: false)
|
|
67
|
-
|
|
68
|
-
# Create new conversation as current
|
|
69
|
-
conversation = Conversation.create(user_id: user_key, is_current: true)
|
|
70
|
-
Conversation[conversation.id]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Get the UUID for a user's current conversation
|
|
74
|
-
#
|
|
75
|
-
# @param user_id [String] User identifier
|
|
76
|
-
# @return [String, nil] Current conversation UUID or nil if none exists
|
|
77
|
-
def current_uuid_for(user_id)
|
|
78
|
-
conversation = Conversation.where(user_id: user_id.to_s, is_current: true).first
|
|
79
|
-
conversation&.id
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# List all conversation mappings (excluding archived by default)
|
|
83
|
-
#
|
|
84
|
-
# @param include_archived [Boolean] Whether to include archived conversations
|
|
85
|
-
# @return [Hash] Hash mapping UUIDs to {user_id, label} hashes
|
|
86
|
-
def all_mappings(include_archived: false)
|
|
87
|
-
dataset = include_archived ? Conversation.dataset : Conversation.where(archived: false)
|
|
88
|
-
dataset.all.map do |conv|
|
|
89
|
-
[conv.id, {"user_id" => conv.user_id, "label" => conv.label}]
|
|
90
|
-
end.to_h
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
# Remove a user's current conversation
|
|
94
|
-
#
|
|
95
|
-
# @param user_id [String] User identifier
|
|
96
|
-
def remove(user_id)
|
|
97
|
-
conversation = Conversation.where(user_id: user_id.to_s, is_current: true).first
|
|
98
|
-
return unless conversation
|
|
99
|
-
|
|
100
|
-
conversation.destroy
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# Clear all conversations (use with caution)
|
|
104
|
-
def clear_all
|
|
105
|
-
Conversation.db[:conversations].delete
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
# Get the label for a conversation
|
|
109
|
-
#
|
|
110
|
-
# @param uuid [String] Conversation UUID
|
|
111
|
-
# @return [String, nil] Label value or nil
|
|
112
|
-
def label(uuid)
|
|
113
|
-
conversation = Conversation.find(id: uuid)
|
|
114
|
-
conversation&.label
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
# Set the label for a conversation
|
|
118
|
-
#
|
|
119
|
-
# @param uuid [String] Conversation UUID
|
|
120
|
-
# @param value [String] Label value
|
|
121
|
-
# @return [String] The label value
|
|
122
|
-
# @raise [Error] If label format is invalid or already in use
|
|
123
|
-
def set_label(uuid, value)
|
|
124
|
-
conversation = Conversation.find(id: uuid)
|
|
125
|
-
raise Error, "Conversation not found" unless conversation
|
|
126
|
-
|
|
127
|
-
# Validate label format
|
|
128
|
-
unless value.nil? || value.to_s.empty? || value.to_s.match?(LABEL_REGEX)
|
|
129
|
-
raise Error, "Invalid label format. Use only letters, numbers, dashes, and underscores."
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
# Check uniqueness per user (excluding current conversation)
|
|
133
|
-
user_id = conversation.user_id
|
|
134
|
-
if value && !value.to_s.empty? && label_exists?(user_id, value, exclude_uuid: uuid)
|
|
135
|
-
raise Error, "Label '#{value}' already in use by another conversation"
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
# Allow empty string to be treated as nil (clearing the label)
|
|
139
|
-
value = nil if value.to_s.empty?
|
|
140
|
-
|
|
141
|
-
Conversation.where(id: uuid).update(label: value)
|
|
142
|
-
value
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Check if a label exists for a user
|
|
146
|
-
#
|
|
147
|
-
# @param user_id [String] User identifier
|
|
148
|
-
# @param label [String] Label to check
|
|
149
|
-
# @param exclude_uuid [String, nil] UUID to exclude from check
|
|
150
|
-
# @return [Boolean] True if label exists for user
|
|
151
|
-
def label_exists?(user_id, label, exclude_uuid: nil)
|
|
152
|
-
return false unless label
|
|
153
|
-
|
|
154
|
-
query = Conversation.where(user_id: user_id.to_s, label: label)
|
|
155
|
-
query = query.exclude(id: exclude_uuid) if exclude_uuid
|
|
156
|
-
query.count > 0
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
# List all conversations for a user
|
|
160
|
-
# Sorted by updated_at in descending order (most recently updated first)
|
|
161
|
-
#
|
|
162
|
-
# @param user_id [String] User identifier
|
|
163
|
-
# @param archived [Boolean, nil] Filter by archived status (nil = all, true = archived only, false = unarchived only)
|
|
164
|
-
# @return [Array<Hash>] Array of {uuid, label, updated_at} hashes
|
|
165
|
-
def list_by_user(user_id, archived: false)
|
|
166
|
-
dataset = Conversation.where(user_id: user_id.to_s)
|
|
167
|
-
dataset = dataset.where(archived: archived) unless archived.nil?
|
|
168
|
-
dataset.order(Sequel.desc(:updated_at)).all.map do |conv|
|
|
169
|
-
{uuid: conv.id, label: conv.label, updated_at: conv.updated_at}
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
|
-
|
|
173
|
-
# Find conversation UUID by label for a user
|
|
174
|
-
#
|
|
175
|
-
# @param user_id [String] User identifier
|
|
176
|
-
# @param label [String] Label to search for
|
|
177
|
-
# @return [String, nil] UUID or nil if not found
|
|
178
|
-
def find_by_label(user_id, label)
|
|
179
|
-
conversation = Conversation.where(user_id: user_id.to_s, label: label).first
|
|
180
|
-
conversation&.id
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Archive a conversation by label or UUID, or archive current if no identifier given
|
|
184
|
-
# When archiving current conversation, automatically creates a new one
|
|
185
|
-
#
|
|
186
|
-
# @param user_id [String] User identifier
|
|
187
|
-
# @param identifier [String, nil] Conversation label or UUID to archive (nil = archive current)
|
|
188
|
-
# @return [Hash] Hash with :archived and :new_conversation keys
|
|
189
|
-
# @raise [Error] If conversation not found
|
|
190
|
-
def archive(user_id, identifier = nil)
|
|
191
|
-
user_key = user_id.to_s
|
|
192
|
-
identifier = identifier.to_s.strip
|
|
193
|
-
|
|
194
|
-
if identifier.empty?
|
|
195
|
-
# Archive current conversation
|
|
196
|
-
conversation = Conversation.where(user_id: user_key, is_current: true).first
|
|
197
|
-
raise Error, "No current conversation to archive" unless conversation
|
|
198
|
-
|
|
199
|
-
# Archive the current conversation
|
|
200
|
-
conversation.update(archived: true, is_current: false)
|
|
201
|
-
|
|
202
|
-
# Create a new conversation (becomes current)
|
|
203
|
-
new_conversation = create_new(user_key)
|
|
204
|
-
|
|
205
|
-
{
|
|
206
|
-
archived: Conversation[conversation.id],
|
|
207
|
-
new_conversation: new_conversation
|
|
208
|
-
}
|
|
209
|
-
else
|
|
210
|
-
# Archive by label or UUID
|
|
211
|
-
conversation = Conversation.where(user_id: user_key, label: identifier).first
|
|
212
|
-
conversation ||= Conversation.find(id: identifier, user_id: user_key)
|
|
213
|
-
|
|
214
|
-
raise Error, "Conversation '#{identifier}' not found" unless conversation
|
|
215
|
-
|
|
216
|
-
# Cannot archive current conversation (must use archive without args)
|
|
217
|
-
raise Error, "Cannot archive the current conversation. Use /archive without arguments to archive current and start new." if conversation.is_current
|
|
218
|
-
|
|
219
|
-
conversation.update(archived: true, is_current: false)
|
|
220
|
-
{archived: Conversation[conversation.id]}
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|