samagotchi 0.10.1 → 0.11.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 +4 -4
- data/CHANGELOG.md +66 -1
- data/docs/architecture.md +19 -0
- data/docs/cli.md +24 -7
- data/docs/configuration.md +44 -11
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +3 -2
- data/docs/internals/context-telemetry.md +4 -2
- data/docs/sessions.md +3 -3
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -2
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +5 -3
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/manifest.yml +3 -3
- data/lib/samagotchi/bundles/skills/plugin.rb +3 -23
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/client.rb +21 -1
- data/lib/samagotchi/config.rb +44 -40
- data/lib/samagotchi/context_status.rb +17 -16
- data/lib/samagotchi/empty_answer_retry.rb +67 -8
- data/lib/samagotchi/engine.rb +64 -20
- data/lib/samagotchi/host_registry.rb +50 -90
- data/lib/samagotchi/kernel_loop.rb +288 -305
- data/lib/samagotchi/llm/chat_loop.rb +78 -87
- data/lib/samagotchi/model_profile.rb +22 -3
- data/lib/samagotchi/model_ref.rb +60 -0
- data/lib/samagotchi/prompt_history.rb +91 -0
- data/lib/samagotchi/recap_setup.rb +15 -3
- data/lib/samagotchi/self_report.rb +11 -6
- data/lib/samagotchi/send_command.rb +8 -8
- data/lib/samagotchi/session.rb +9 -2
- data/lib/samagotchi/session_commands.rb +10 -12
- data/lib/samagotchi/session_manager.rb +20 -3
- data/lib/samagotchi/sessions_command.rb +57 -51
- data/lib/samagotchi/steer.rb +23 -0
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +2 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +3 -6
- data/lib/samagotchi/terminal_ui/input_support.rb +78 -33
- data/lib/samagotchi/terminal_ui/reline_history_hook.rb +93 -0
- data/lib/samagotchi/terminal_ui.rb +9 -8
- data/lib/samagotchi/tool_response.rb +67 -0
- data/lib/samagotchi/tool_runner.rb +6 -1
- data/lib/samagotchi/tools/delegate.rb +4 -3
- data/lib/samagotchi/tools/memory.rb +8 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +28 -2
- data/lib/samagotchi/web/public/app.js +29 -2
- data/lib/samagotchi/web/public/data.js +9 -1
- data/lib/samagotchi/web/public/prompt_history.js +63 -0
- data/lib/samagotchi/worker.rb +2 -1
- metadata +6 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "config"
|
|
4
|
+
require_relative "model_ref"
|
|
4
5
|
require_relative "client"
|
|
5
6
|
require_relative "llm/openai_chat"
|
|
6
7
|
|
|
@@ -14,8 +15,9 @@ module Samagotchi
|
|
|
14
15
|
# /models), not on startup. Each host's list is cached (60s, 10 minutes
|
|
15
16
|
# for a remote host) with skip-on-error; lists are LLM::ModelInfo.
|
|
16
17
|
# - Routing: client_for_model resolves a (possibly qualified) model string
|
|
17
|
-
# to the appropriate Client instance
|
|
18
|
-
#
|
|
18
|
+
# to the appropriate Client instance: host:model, an alias, or an exact
|
|
19
|
+
# id in a cached list (default host first, then hosts: order); never by
|
|
20
|
+
# a substring.
|
|
19
21
|
class HostRegistry
|
|
20
22
|
CACHE_TTL_SECONDS = 60
|
|
21
23
|
REMOTE_CACHE_TTL_SECONDS = 600
|
|
@@ -45,8 +47,7 @@ module Samagotchi
|
|
|
45
47
|
def openai_base_url = url || "#{root_url}/v1"
|
|
46
48
|
|
|
47
49
|
# A provider on the network rather than a local server: it needs a key
|
|
48
|
-
# or speaks https. Its model list is cached longer
|
|
49
|
-
# picked by a substring of a model name.
|
|
50
|
+
# or speaks https. Its model list is cached longer.
|
|
50
51
|
def remote? = !api_key_env.to_s.empty? || scheme == "https"
|
|
51
52
|
|
|
52
53
|
def models_ttl = remote? ? REMOTE_CACHE_TTL_SECONDS : CACHE_TTL_SECONDS
|
|
@@ -106,7 +107,7 @@ module Samagotchi
|
|
|
106
107
|
@mutex = Mutex.new
|
|
107
108
|
@cache = nil
|
|
108
109
|
@cache_at = nil
|
|
109
|
-
@model_index = nil # downcased model_id => host_name
|
|
110
|
+
@model_index = nil # downcased model_id => [host_name] in hosts: order
|
|
110
111
|
end
|
|
111
112
|
|
|
112
113
|
def entries
|
|
@@ -142,79 +143,26 @@ module Samagotchi
|
|
|
142
143
|
# @param target [ModelTarget, nil] the resolved model's target (resolved when not given)
|
|
143
144
|
# @return [Array<String>]
|
|
144
145
|
def lookup_names(typed, resolved: nil, target: nil)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
[typed, aliased, resolved, parse_qualified_model(typed).last, target.bare_model]
|
|
146
|
+
target ||= resolve(resolved || typed)
|
|
147
|
+
[typed, model_ref(typed).ref, resolved, parse_qualified_model(typed).last, target.bare_model]
|
|
148
148
|
.map { |name| name.to_s.strip }.reject(&:empty?).uniq
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
-
#
|
|
152
|
-
#
|
|
153
|
-
#
|
|
151
|
+
# The host a model name, alias or host:model ref goes to and the model
|
|
152
|
+
# id it is sent as (ModelRef: one alias pass). A ref that names a host
|
|
153
|
+
# goes there. A bare id goes to the default host when its cached list
|
|
154
|
+
# has it, else to the first host in hosts: order whose list has it,
|
|
155
|
+
# else to the default host (before /models nothing is listed: no
|
|
156
|
+
# discovery here, the latency budget). Never by a substring.
|
|
154
157
|
def host_for_model(raw_model)
|
|
155
|
-
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
nil
|
|
164
|
-
end
|
|
165
|
-
# parse_qualified_model names a host only when it is one of ours
|
|
166
|
-
return [find_entry(host_ref), bare]
|
|
167
|
-
end
|
|
168
|
-
# Unqualified: also try alias resolution for discovery (small -> gemma-small or small -> small-box:gemma-small)
|
|
169
|
-
begin
|
|
170
|
-
aliases = ConfigFile.model_aliases
|
|
171
|
-
resolved = aliases.fetch(bare.to_s.strip.downcase, bare)
|
|
172
|
-
if resolved != bare
|
|
173
|
-
# If alias points to a qualified ref, re-parse it
|
|
174
|
-
q_host, q_bare = parse_qualified_model(resolved)
|
|
175
|
-
if q_host
|
|
176
|
-
entry = find_entry(q_host)
|
|
177
|
-
return [entry, q_bare] if entry
|
|
178
|
-
end
|
|
179
|
-
bare = resolved
|
|
180
|
-
end
|
|
181
|
-
rescue StandardError
|
|
182
|
-
nil
|
|
183
|
-
end
|
|
184
|
-
bare_down = bare.to_s.strip.downcase
|
|
185
|
-
# Try cached index (populated after list_all_models)
|
|
186
|
-
idx = @mutex.synchronize { @model_index }
|
|
187
|
-
if idx && idx.key?(bare_down)
|
|
188
|
-
host_name = idx[bare_down]
|
|
189
|
-
entry = find_entry(host_name)
|
|
190
|
-
return [entry, bare] if entry
|
|
191
|
-
end
|
|
192
|
-
# Fallback: try substring match in cached aggregated results if available
|
|
193
|
-
# (lightweight: scan cached model lists). Remote hosts match exactly only.
|
|
194
|
-
cached = @mutex.synchronize { @cache }
|
|
195
|
-
if cached
|
|
196
|
-
cached.each do |hname, data|
|
|
197
|
-
next unless data[:models]
|
|
198
|
-
data[:models].each do |m|
|
|
199
|
-
if m.id.downcase == bare_down
|
|
200
|
-
entry = find_entry(hname)
|
|
201
|
-
return [entry, bare] if entry
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
end
|
|
205
|
-
cached.each do |hname, data|
|
|
206
|
-
next unless data[:models]
|
|
207
|
-
next if find_entry(hname)&.remote?
|
|
208
|
-
|
|
209
|
-
data[:models].each do |m|
|
|
210
|
-
if m.id.downcase.include?(bare_down)
|
|
211
|
-
entry = find_entry(hname)
|
|
212
|
-
return [entry, bare] if entry
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
end
|
|
216
|
-
end
|
|
217
|
-
[default_entry, bare]
|
|
158
|
+
ref = model_ref(raw_model)
|
|
159
|
+
# ModelRef names a host only when it is one of ours
|
|
160
|
+
return [find_entry(ref.host_name), ref.id] if ref.host_name
|
|
161
|
+
|
|
162
|
+
listed = @mutex.synchronize { @model_index }&.fetch(ref.id.to_s.strip.downcase, nil) || []
|
|
163
|
+
default = default_entry
|
|
164
|
+
name = listed.include?(default.name) ? default.name : listed.first
|
|
165
|
+
[(name && find_entry(name)) || default, ref.id]
|
|
218
166
|
end
|
|
219
167
|
|
|
220
168
|
# The chat adapter for a host (one per host, so its cached model list
|
|
@@ -247,20 +195,30 @@ module Samagotchi
|
|
|
247
195
|
[client_for(host_entry), bare, host_entry]
|
|
248
196
|
end
|
|
249
197
|
|
|
250
|
-
# The single host/model resolution: alias
|
|
251
|
-
#
|
|
198
|
+
# The single host/model resolution: the alias applied, the host and
|
|
199
|
+
# the name sent to the server (host_for_model).
|
|
252
200
|
# @param raw_model [String] a model name, alias or host:model ref
|
|
253
201
|
# @return [ModelTarget]
|
|
254
202
|
def resolve(raw_model)
|
|
255
|
-
entry, = host_for_model(raw_model)
|
|
256
|
-
ModelTarget.new(model: raw_model, entry: entry, bare_model:
|
|
203
|
+
entry, bare = host_for_model(raw_model)
|
|
204
|
+
ModelTarget.new(model: raw_model, entry: entry, bare_model: bare, client: client_for(entry))
|
|
257
205
|
end
|
|
258
206
|
|
|
259
|
-
# The model
|
|
260
|
-
#
|
|
207
|
+
# The model id sent for +full_ref+: its alias applied, a known host
|
|
208
|
+
# prefix stripped ("box:gemma" → "gemma").
|
|
261
209
|
def bare_name(full_ref)
|
|
262
|
-
|
|
263
|
-
|
|
210
|
+
model_ref(full_ref).id
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# +raw+ parsed against these hosts and config.yml's aliases.
|
|
214
|
+
# @return [ModelRef]
|
|
215
|
+
def model_ref(raw)
|
|
216
|
+
aliases = begin
|
|
217
|
+
ConfigFile.model_aliases(env: @env)
|
|
218
|
+
rescue StandardError
|
|
219
|
+
{}
|
|
220
|
+
end
|
|
221
|
+
ModelRef.parse(raw, hosts: @entries, aliases: aliases)
|
|
264
222
|
end
|
|
265
223
|
|
|
266
224
|
def client_for(entry)
|
|
@@ -291,17 +249,19 @@ module Samagotchi
|
|
|
291
249
|
end
|
|
292
250
|
threads.each { |thread| thread.join if thread.is_a?(Thread) }
|
|
293
251
|
|
|
294
|
-
#
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
252
|
+
# Model index: model_id downcased -> the hosts listing it, in hosts:
|
|
253
|
+
# order (not the order the threads answered in).
|
|
254
|
+
index = Hash.new { |h, k| h[k] = [] }
|
|
255
|
+
@entries.each_key do |hname|
|
|
256
|
+
data = results[hname]
|
|
257
|
+
next if data.nil? || data[:error]
|
|
258
|
+
|
|
298
259
|
Array(data[:models]).each do |m|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
down = mid.downcase
|
|
302
|
-
index[down] = hname unless index.key?(down)
|
|
260
|
+
down = m.id.to_s.strip.downcase
|
|
261
|
+
index[down] << hname unless down.empty? || index[down].include?(hname)
|
|
303
262
|
end
|
|
304
263
|
end
|
|
264
|
+
index.default_proc = nil
|
|
305
265
|
|
|
306
266
|
@mutex.synchronize do
|
|
307
267
|
@cache = results
|