scout-ai 0.2.0 → 1.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 +4 -4
- data/.vimproject +155 -9
- data/README.md +296 -0
- data/Rakefile +3 -0
- data/VERSION +1 -1
- data/bin/scout-ai +2 -0
- data/doc/Agent.md +279 -0
- data/doc/Chat.md +258 -0
- data/doc/LLM.md +446 -0
- data/doc/Model.md +513 -0
- data/doc/RAG.md +129 -0
- data/lib/scout/llm/agent/chat.rb +74 -0
- data/lib/scout/llm/agent/delegate.rb +39 -0
- data/lib/scout/llm/agent/iterate.rb +44 -0
- data/lib/scout/llm/agent.rb +51 -30
- data/lib/scout/llm/ask.rb +63 -21
- data/lib/scout/llm/backends/anthropic.rb +147 -0
- data/lib/scout/llm/backends/bedrock.rb +129 -0
- data/lib/scout/llm/backends/huggingface.rb +6 -21
- data/lib/scout/llm/backends/ollama.rb +62 -35
- data/lib/scout/llm/backends/openai.rb +77 -33
- data/lib/scout/llm/backends/openwebui.rb +1 -1
- data/lib/scout/llm/backends/relay.rb +3 -2
- data/lib/scout/llm/backends/responses.rb +320 -0
- data/lib/scout/llm/chat.rb +703 -0
- data/lib/scout/llm/embed.rb +4 -4
- data/lib/scout/llm/mcp.rb +28 -0
- data/lib/scout/llm/parse.rb +71 -13
- data/lib/scout/llm/rag.rb +9 -0
- data/lib/scout/llm/tools/call.rb +66 -0
- data/lib/scout/llm/tools/knowledge_base.rb +158 -0
- data/lib/scout/llm/tools/mcp.rb +59 -0
- data/lib/scout/llm/tools/workflow.rb +69 -0
- data/lib/scout/llm/tools.rb +112 -76
- data/lib/scout/llm/utils.rb +17 -10
- data/lib/scout/model/base.rb +19 -0
- data/lib/scout/model/python/base.rb +25 -0
- data/lib/scout/model/python/huggingface/causal/next_token.rb +23 -0
- data/lib/scout/model/python/huggingface/causal.rb +29 -0
- data/lib/scout/model/python/huggingface/classification +0 -0
- data/lib/scout/model/python/huggingface/classification.rb +50 -0
- data/lib/scout/model/python/huggingface.rb +112 -0
- data/lib/scout/model/python/torch/dataloader.rb +57 -0
- data/lib/scout/model/python/torch/helpers.rb +84 -0
- data/lib/scout/model/python/torch/introspection.rb +34 -0
- data/lib/scout/model/python/torch/load_and_save.rb +47 -0
- data/lib/scout/model/python/torch.rb +94 -0
- data/lib/scout/model/util/run.rb +181 -0
- data/lib/scout/model/util/save.rb +81 -0
- data/lib/scout-ai.rb +4 -1
- data/python/scout_ai/__init__.py +35 -0
- data/python/scout_ai/huggingface/data.py +48 -0
- data/python/scout_ai/huggingface/eval.py +60 -0
- data/python/scout_ai/huggingface/model.py +29 -0
- data/python/scout_ai/huggingface/rlhf.py +83 -0
- data/python/scout_ai/huggingface/train/__init__.py +34 -0
- data/python/scout_ai/huggingface/train/next_token.py +315 -0
- data/python/scout_ai/util.py +32 -0
- data/scout-ai.gemspec +143 -0
- data/scout_commands/agent/ask +89 -14
- data/scout_commands/agent/kb +15 -0
- data/scout_commands/documenter +148 -0
- data/scout_commands/llm/ask +71 -12
- data/scout_commands/llm/process +4 -2
- data/scout_commands/llm/server +319 -0
- data/share/server/chat.html +138 -0
- data/share/server/chat.js +468 -0
- data/test/data/cat.jpg +0 -0
- data/test/scout/llm/agent/test_chat.rb +14 -0
- data/test/scout/llm/backends/test_anthropic.rb +134 -0
- data/test/scout/llm/backends/test_bedrock.rb +60 -0
- data/test/scout/llm/backends/test_huggingface.rb +3 -3
- data/test/scout/llm/backends/test_ollama.rb +48 -10
- data/test/scout/llm/backends/test_openai.rb +134 -10
- data/test/scout/llm/backends/test_responses.rb +239 -0
- data/test/scout/llm/test_agent.rb +0 -70
- data/test/scout/llm/test_ask.rb +4 -1
- data/test/scout/llm/test_chat.rb +256 -0
- data/test/scout/llm/test_mcp.rb +29 -0
- data/test/scout/llm/test_parse.rb +81 -2
- data/test/scout/llm/tools/test_call.rb +0 -0
- data/test/scout/llm/tools/test_knowledge_base.rb +22 -0
- data/test/scout/llm/tools/test_mcp.rb +11 -0
- data/test/scout/llm/tools/test_workflow.rb +39 -0
- data/test/scout/model/python/huggingface/causal/test_next_token.rb +59 -0
- data/test/scout/model/python/huggingface/test_causal.rb +33 -0
- data/test/scout/model/python/huggingface/test_classification.rb +30 -0
- data/test/scout/model/python/test_base.rb +44 -0
- data/test/scout/model/python/test_huggingface.rb +9 -0
- data/test/scout/model/python/test_torch.rb +71 -0
- data/test/scout/model/python/torch/test_helpers.rb +14 -0
- data/test/scout/model/test_base.rb +117 -0
- data/test/scout/model/util/test_save.rb +31 -0
- metadata +113 -7
- data/README.rdoc +0 -18
- data/questions/coach +0 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require_relative 'helpers'
|
|
2
|
+
class TorchModel
|
|
3
|
+
def self.get_layer(state, layer = nil)
|
|
4
|
+
state = state.first if Array === state
|
|
5
|
+
if layer.nil?
|
|
6
|
+
state
|
|
7
|
+
else
|
|
8
|
+
layer.split(".").inject(state){|acc,l| PyCall.getattr(acc, l.to_sym) }
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
def get_layer(...); TorchModel.get_layer(state, ...); end
|
|
12
|
+
|
|
13
|
+
def self.get_weights(state, layer = nil)
|
|
14
|
+
Tensor.setup PyCall.getattr(get_layer(state, layer), :weight)
|
|
15
|
+
end
|
|
16
|
+
def get_weights(...); TorchModel.get_weights(state, ...); end
|
|
17
|
+
|
|
18
|
+
def self.freeze(layer, requires_grad=false)
|
|
19
|
+
begin
|
|
20
|
+
PyCall.getattr(layer, :weight).requires_grad = requires_grad
|
|
21
|
+
rescue
|
|
22
|
+
end
|
|
23
|
+
ScoutPython.iterate(layer.children) do |layer|
|
|
24
|
+
freeze(layer, requires_grad)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.freeze_layer(state, layer, requires_grad = false)
|
|
29
|
+
layer = get_layer(state, layer)
|
|
30
|
+
freeze(layer, requires_grad)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def freeze_layer(...); TorchModel.freeze_layer(state, ...); end
|
|
34
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
class TorchModel
|
|
2
|
+
def self.model_architecture(state_file)
|
|
3
|
+
state_file + '.architecture'
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
def self.save_state(state, state_file)
|
|
7
|
+
Log.debug "Saving model state into #{state_file}"
|
|
8
|
+
ScoutPython.torch.save(state.state_dict(), state_file)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.load_state(state, state_file)
|
|
12
|
+
return state unless Open.exists?(state_file)
|
|
13
|
+
Log.debug "Loading model state from #{state_file}"
|
|
14
|
+
state.load_state_dict(ScoutPython.torch.load(state_file))
|
|
15
|
+
state
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def self.save_architecture(state, state_file)
|
|
19
|
+
model_architecture = model_architecture(state_file)
|
|
20
|
+
Log.debug "Saving model architecture into #{model_architecture}"
|
|
21
|
+
ScoutPython.torch.save(state, model_architecture)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.load_architecture(state_file)
|
|
25
|
+
model_architecture = model_architecture(state_file)
|
|
26
|
+
return unless Open.exists?(model_architecture)
|
|
27
|
+
Log.debug "Loading model architecture from #{model_architecture}"
|
|
28
|
+
ScoutPython.torch.load(model_architecture, weights_only: false)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def reset_state
|
|
32
|
+
@trainer = @state = nil
|
|
33
|
+
Open.rm_rf state_file
|
|
34
|
+
Open.rm_rf TorchModel.model_architecture(state_file)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.save(state_file, state)
|
|
38
|
+
TorchModel.save_architecture(state, state_file)
|
|
39
|
+
TorchModel.save_state(state, state_file)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.load(state_file, state = nil)
|
|
43
|
+
state ||= TorchModel.load_architecture(state_file)
|
|
44
|
+
TorchModel.load_state(state, state_file)
|
|
45
|
+
state
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
require_relative 'base'
|
|
2
|
+
|
|
3
|
+
class TorchModel < PythonModel
|
|
4
|
+
attr_accessor :criterion, :optimizer, :device, :dtype
|
|
5
|
+
|
|
6
|
+
def fix_options
|
|
7
|
+
@options[:training_options] = @options.delete(:training_args) if @options.include?(:training_args)
|
|
8
|
+
training_args = IndiferentHash.pull_keys(@options, :training) || {}
|
|
9
|
+
@options[:training_args] = training_args
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(...)
|
|
13
|
+
|
|
14
|
+
super(...)
|
|
15
|
+
|
|
16
|
+
fix_options
|
|
17
|
+
|
|
18
|
+
load_state do |state_file|
|
|
19
|
+
@state = TorchModel.load(state_file, @state)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
save_state do |state_file,state|
|
|
23
|
+
TorchModel.save(state_file, state)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
train do |features,labels|
|
|
27
|
+
TorchModel.init_python
|
|
28
|
+
device ||= TorchModel.device(options)
|
|
29
|
+
dtype ||= TorchModel.dtype(options)
|
|
30
|
+
state.to(device)
|
|
31
|
+
@optimizer ||= TorchModel.optimizer(state, options[:training_args] || {})
|
|
32
|
+
@criterion ||= TorchModel.optimizer(state, options[:training_args] || {})
|
|
33
|
+
|
|
34
|
+
epochs = options[:training_args][:epochs] || 3
|
|
35
|
+
batch_size = options[:batch_size]
|
|
36
|
+
batch_size ||= options[:training_args][:batch_size]
|
|
37
|
+
batch_size ||= 1
|
|
38
|
+
|
|
39
|
+
inputs = TorchModel.tensor(features, device, dtype)
|
|
40
|
+
#target = TorchModel.tensor(labels.collect{|v| [v] }, @device, @dtype)
|
|
41
|
+
target = TorchModel.tensor(labels, device, dtype)
|
|
42
|
+
|
|
43
|
+
Log::ProgressBar.with_bar epochs, :desc => "Training" do |bar|
|
|
44
|
+
epochs.times do |i|
|
|
45
|
+
optimizer.zero_grad()
|
|
46
|
+
outputs = state.call(inputs)
|
|
47
|
+
outputs = outputs.squeeze() if target.dim() == 1
|
|
48
|
+
loss = criterion.call(outputs, target)
|
|
49
|
+
loss.backward()
|
|
50
|
+
optimizer.step
|
|
51
|
+
Log.debug "Epoch #{i}, loss #{loss}"
|
|
52
|
+
bar.tick
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
self.eval do |features,list|
|
|
58
|
+
TorchModel.init_python
|
|
59
|
+
device ||= TorchModel.device(options)
|
|
60
|
+
dtype ||= TorchModel.dtype(options)
|
|
61
|
+
state.to(device)
|
|
62
|
+
state.eval
|
|
63
|
+
|
|
64
|
+
list = [features] if features
|
|
65
|
+
|
|
66
|
+
batch_size = options[:batch_size]
|
|
67
|
+
batch_size ||= options[:training_args][:batch_size]
|
|
68
|
+
batch_size ||= 1
|
|
69
|
+
|
|
70
|
+
res = Misc.chunk(list, batch_size).inject(nil) do |acc,batch|
|
|
71
|
+
tensor = TorchModel.tensor(batch, device, dtype)
|
|
72
|
+
|
|
73
|
+
loss, chunk_res = state.call(tensor)
|
|
74
|
+
tensor.del
|
|
75
|
+
|
|
76
|
+
chunk_res = loss if chunk_res.nil?
|
|
77
|
+
|
|
78
|
+
TorchModel::Tensor.setup(chunk_res)
|
|
79
|
+
chunk_res = chunk_res.to_ruby!
|
|
80
|
+
|
|
81
|
+
acc = acc.nil? ? chunk_res : acc + chunk_res
|
|
82
|
+
|
|
83
|
+
acc
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
features ? res[0] : res
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
require_relative 'torch/helpers'
|
|
92
|
+
require_relative 'torch/dataloader'
|
|
93
|
+
require_relative 'torch/load_and_save'
|
|
94
|
+
require_relative 'torch/introspection'
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
class ScoutModel
|
|
2
|
+
def execute(method, *args)
|
|
3
|
+
case method
|
|
4
|
+
when Proc
|
|
5
|
+
instance_exec *args, &method
|
|
6
|
+
when nil
|
|
7
|
+
args.first
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def save_state(&block)
|
|
12
|
+
if block_given?
|
|
13
|
+
@save_state = block
|
|
14
|
+
else
|
|
15
|
+
return @state unless @save_state
|
|
16
|
+
execute @save_state, state_file, @state
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def load_state(&block)
|
|
21
|
+
if block_given?
|
|
22
|
+
@load_state = block
|
|
23
|
+
else
|
|
24
|
+
return @state unless @load_state
|
|
25
|
+
execute @load_state, state_file
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def init(&block)
|
|
30
|
+
return @state if @state
|
|
31
|
+
if block_given?
|
|
32
|
+
@init = block
|
|
33
|
+
else
|
|
34
|
+
@state = execute @init
|
|
35
|
+
load_state
|
|
36
|
+
@state
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def eval(sample = nil, &block)
|
|
41
|
+
if block_given?
|
|
42
|
+
@eval = block
|
|
43
|
+
else
|
|
44
|
+
features = extract_features sample
|
|
45
|
+
|
|
46
|
+
init unless @state
|
|
47
|
+
result = if @eval.arity == 2
|
|
48
|
+
|
|
49
|
+
execute @eval, features, nil
|
|
50
|
+
else
|
|
51
|
+
execute @eval, features
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
post_process result
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def eval_list(list = nil, &block)
|
|
59
|
+
if block_given?
|
|
60
|
+
@eval_list = block
|
|
61
|
+
else
|
|
62
|
+
list = extract_features_list list
|
|
63
|
+
|
|
64
|
+
init unless @state
|
|
65
|
+
result = if @eval_list
|
|
66
|
+
execute @eval_list, list
|
|
67
|
+
elsif @eval
|
|
68
|
+
|
|
69
|
+
if @eval.arity == 2
|
|
70
|
+
execute @eval, nil, list
|
|
71
|
+
else
|
|
72
|
+
list.collect{|features| execute @eval, features }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
post_process_list result
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def post_process(result = nil, &block)
|
|
81
|
+
if block_given?
|
|
82
|
+
@post_process = block
|
|
83
|
+
else
|
|
84
|
+
return result if @post_process.nil?
|
|
85
|
+
|
|
86
|
+
if @post_process.arity == 2
|
|
87
|
+
execute @post_process, result, nil
|
|
88
|
+
else
|
|
89
|
+
execute @post_process, result
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def post_process_list(list = nil, &block)
|
|
95
|
+
if block_given?
|
|
96
|
+
@post_process_list = block
|
|
97
|
+
else
|
|
98
|
+
|
|
99
|
+
if @post_process_list
|
|
100
|
+
execute @post_process_list, list
|
|
101
|
+
elsif @post_process
|
|
102
|
+
if @post_process.arity == 2
|
|
103
|
+
execute @post_process, nil, list
|
|
104
|
+
else
|
|
105
|
+
list.collect{|result| execute @post_process, result }
|
|
106
|
+
end
|
|
107
|
+
else
|
|
108
|
+
return list
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def train(&block)
|
|
114
|
+
if block_given?
|
|
115
|
+
@train = block
|
|
116
|
+
else
|
|
117
|
+
init unless @state
|
|
118
|
+
execute @train, @features, @labels
|
|
119
|
+
save_state
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def extract_features(sample = nil, &block)
|
|
124
|
+
if block_given?
|
|
125
|
+
@extract_features = block
|
|
126
|
+
else
|
|
127
|
+
return sample if @extract_features.nil?
|
|
128
|
+
|
|
129
|
+
if @extract_features.arity == 2
|
|
130
|
+
execute @extract_features, sample, nil
|
|
131
|
+
else
|
|
132
|
+
execute @extract_features, sample
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def extract_features_list(list = nil, &block)
|
|
138
|
+
if block_given?
|
|
139
|
+
@extract_features_list = block
|
|
140
|
+
else
|
|
141
|
+
return list if @extract_features.nil?
|
|
142
|
+
|
|
143
|
+
if @extract_features_list
|
|
144
|
+
execute @extract_features_list, list
|
|
145
|
+
elsif @extract_features
|
|
146
|
+
if @extract_features.arity == 2
|
|
147
|
+
execute @extract_features, nil, list
|
|
148
|
+
else
|
|
149
|
+
list.collect{|sample| execute @extract_features, sample }
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
return list
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def add(sample, label = nil)
|
|
158
|
+
features = extract_features sample
|
|
159
|
+
@features << features
|
|
160
|
+
@labels << label
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def add_list(list, labels = nil)
|
|
164
|
+
if Hash === list
|
|
165
|
+
list.each do |sample,label|
|
|
166
|
+
add sample, label
|
|
167
|
+
end
|
|
168
|
+
else
|
|
169
|
+
list = extract_features_list list
|
|
170
|
+
@features.concat list
|
|
171
|
+
|
|
172
|
+
if Hash === labels
|
|
173
|
+
list.each do |sample|
|
|
174
|
+
@labels << labels[sample]
|
|
175
|
+
end
|
|
176
|
+
elsif labels
|
|
177
|
+
@labels.concat labels
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
class ScoutModel
|
|
2
|
+
def state_file
|
|
3
|
+
return nil unless directory
|
|
4
|
+
directory.state
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def save_options
|
|
8
|
+
file = directory['options.json']
|
|
9
|
+
file.write(options.to_json)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def load_options
|
|
13
|
+
file = directory['options.json']
|
|
14
|
+
if file.exists?
|
|
15
|
+
IndiferentHash.setup(JSON.parse(file.read)).merge @options
|
|
16
|
+
else
|
|
17
|
+
@options
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def load_ruby_code(file)
|
|
22
|
+
Log.debug "Loading ruby file #{file}"
|
|
23
|
+
code = Open.read(file)
|
|
24
|
+
code.sub!(/.*(\sdo\b|{)/, 'Proc.new\1')
|
|
25
|
+
instance_eval code, file
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def load_method(name)
|
|
29
|
+
file = directory[name.to_s]
|
|
30
|
+
|
|
31
|
+
if file.exists?
|
|
32
|
+
file.read
|
|
33
|
+
elsif file.set_extension('rb').exists?
|
|
34
|
+
load_ruby_code file.set_extension('rb')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def save_method(name, value)
|
|
39
|
+
file = directory[name.to_s]
|
|
40
|
+
|
|
41
|
+
Log.debug "Saving #{file}"
|
|
42
|
+
case
|
|
43
|
+
when Proc === value
|
|
44
|
+
require 'method_source'
|
|
45
|
+
Open.write(file.set_extension('rb'), value.source)
|
|
46
|
+
when String === train_model
|
|
47
|
+
Open.write(file, @train_model)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def save
|
|
52
|
+
save_options if @options
|
|
53
|
+
|
|
54
|
+
save_method(:eval, @eval) if @eval
|
|
55
|
+
save_method(:eval_list, @eval_list) if @eval_list
|
|
56
|
+
save_method(:extract_features, @extract_features) if @extract_features
|
|
57
|
+
save_method(:extract_features_list, @extract_features_list) if @extract_features_list
|
|
58
|
+
save_method(:post_process, @post_process) if @post_process
|
|
59
|
+
save_method(:post_process_list, @post_process_list) if @post_process_list
|
|
60
|
+
save_method(:train, @train) if @train
|
|
61
|
+
save_method(:init, @init) if @init
|
|
62
|
+
save_method(:load_state, @load_state) if @load_state
|
|
63
|
+
save_method(:save_state, @save_state) if @save_state
|
|
64
|
+
|
|
65
|
+
save_state if @state
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def restore
|
|
69
|
+
@eval = load_method :eval
|
|
70
|
+
@eval_list = load_method :eval_list
|
|
71
|
+
@extract_features = load_method :extract_features
|
|
72
|
+
@extract_features_list = load_method :extract_features_list
|
|
73
|
+
@post_process = load_method :post_process
|
|
74
|
+
@post_process_list = load_method :post_process_list
|
|
75
|
+
@train = load_method :train
|
|
76
|
+
@init = load_method :init
|
|
77
|
+
@load_state = load_method :load_state
|
|
78
|
+
@save_state = load_method :save_state
|
|
79
|
+
@options = load_options
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/scout-ai.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
require 'scout'
|
|
2
2
|
require 'scout/path'
|
|
3
3
|
require 'scout/resource'
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
Path.add_path :scout_ai_lib, File.join(Path.caller_lib_dir(__FILE__), "{TOPLEVEL}/{SUBPATH}")
|
|
5
6
|
|
|
6
7
|
require 'scout/llm/ask'
|
|
8
|
+
require 'scout/llm/chat'
|
|
7
9
|
require 'scout/llm/embed'
|
|
10
|
+
require 'scout/llm/agent'
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import scout
|
|
2
|
+
import torch
|
|
3
|
+
from .util import *
|
|
4
|
+
|
|
5
|
+
class TSVDataset(torch.utils.data.Dataset):
|
|
6
|
+
def __init__(self, tsv):
|
|
7
|
+
self.tsv = tsv
|
|
8
|
+
|
|
9
|
+
def __getitem__(self, key):
|
|
10
|
+
if (type(key) == int):
|
|
11
|
+
row = self.tsv.iloc[key]
|
|
12
|
+
else:
|
|
13
|
+
row = self.tsv.loc[key]
|
|
14
|
+
|
|
15
|
+
row = row.to_numpy()
|
|
16
|
+
features = row[:-1]
|
|
17
|
+
label = row[-1]
|
|
18
|
+
|
|
19
|
+
return features, label
|
|
20
|
+
|
|
21
|
+
def __len__(self):
|
|
22
|
+
return len(self.tsv)
|
|
23
|
+
|
|
24
|
+
def tsv_dataset(filename, *args, **kwargs):
|
|
25
|
+
return TSVDataset(scout.tsv(filename, *args, **kwargs))
|
|
26
|
+
|
|
27
|
+
def tsv(*args, **kwargs):
|
|
28
|
+
return tsv_dataset(*args, **kwargs)
|
|
29
|
+
|
|
30
|
+
def tsv_loader(*args, **kwargs):
|
|
31
|
+
dataset = tsv(*args, kwargs)
|
|
32
|
+
return torch.utils.data.DataLoader(dataset, batch_size=2, shuffle=True)
|
|
33
|
+
|
|
34
|
+
def data_dir():
|
|
35
|
+
return scout.path('var/scout_dm/data')
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import scout
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import datasets
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
def load_tsv(tsv_file):
|
|
7
|
+
tsv = scout.tsv(tsv_file)
|
|
8
|
+
ds = datasets.Dataset.from_pandas(tsv)
|
|
9
|
+
d = datasets.DatasetDict()
|
|
10
|
+
d["train"] = ds
|
|
11
|
+
return d
|
|
12
|
+
|
|
13
|
+
def load_json(json_file):
|
|
14
|
+
return datasets.load_dataset('json', data_files=[json_file])
|
|
15
|
+
|
|
16
|
+
def tokenize_dataset(tokenizer, dataset, max_length=32):
|
|
17
|
+
def preprocess_function(examples):
|
|
18
|
+
return tokenizer(examples["text"], truncation=True, padding="max_length", max_length=max_length)
|
|
19
|
+
if isinstance(dataset, datasets.DatasetDict):
|
|
20
|
+
for split in dataset:
|
|
21
|
+
dataset[split] = dataset[split].map(preprocess_function, batched=True)
|
|
22
|
+
return dataset
|
|
23
|
+
else:
|
|
24
|
+
return dataset.map(preprocess_function, batched=True)
|
|
25
|
+
|
|
26
|
+
def tsv_dataset(tokenizer, tsv_file):
|
|
27
|
+
dataset = load_tsv(tsv_file)
|
|
28
|
+
return tokenize_dataset(tokenizer, dataset)
|
|
29
|
+
|
|
30
|
+
def json_dataset(tokenizer, json_file):
|
|
31
|
+
dataset = load_json(json_file)
|
|
32
|
+
return tokenize_dataset(tokenizer, dataset)
|
|
33
|
+
|
|
34
|
+
def list_dataset(tokenizer, texts, labels=None, max_length=32):
|
|
35
|
+
data_dict = {"text": texts}
|
|
36
|
+
if labels is not None:
|
|
37
|
+
data_dict["label"] = labels
|
|
38
|
+
ds = datasets.Dataset.from_dict(data_dict)
|
|
39
|
+
|
|
40
|
+
def preprocess_function(examples):
|
|
41
|
+
output = tokenizer(examples["text"], truncation=True, padding="max_length", max_length=max_length)
|
|
42
|
+
if "label" in examples:
|
|
43
|
+
output["label"] = examples["label"]
|
|
44
|
+
return output
|
|
45
|
+
|
|
46
|
+
tokenized_ds = ds.map(preprocess_function, batched=True)
|
|
47
|
+
return tokenized_ds
|
|
48
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
def forward(model, features):
|
|
2
|
+
return model(**features)
|
|
3
|
+
|
|
4
|
+
def get_logits(predictions):
|
|
5
|
+
logits = predictions["logits"]
|
|
6
|
+
return [v.detach().cpu().numpy() for v in logits]
|
|
7
|
+
|
|
8
|
+
def eval_model(model, tokenizer, texts, return_logits=True):
|
|
9
|
+
features = tokenizer(texts, return_tensors='pt', truncation=True).to(model.device)
|
|
10
|
+
model.eval()
|
|
11
|
+
predictions = forward(model, features)
|
|
12
|
+
if return_logits:
|
|
13
|
+
return get_logits(predictions)
|
|
14
|
+
return predictions
|
|
15
|
+
|
|
16
|
+
def eval_causal_lm_chat(
|
|
17
|
+
model, tokenizer, messages,
|
|
18
|
+
chat_template=None,
|
|
19
|
+
chat_template_kwargs=None,
|
|
20
|
+
generation_kwargs=None
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Evaluate a CausalLM model given chat messages. Uses tokenizer's chat template by default.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
model: Huggingface CausalLM
|
|
27
|
+
tokenizer: Huggingface tokenizer
|
|
28
|
+
messages: List[Dict[str, str]] (OpenAI API style, 'role' and 'content')
|
|
29
|
+
chat_template: (Optional) Override string for the chat template.
|
|
30
|
+
chat_template_kwargs: (Optional) Dict, kwargs for apply_chat_template (like tokenize, add_generation_prompt, etc).
|
|
31
|
+
generation_kwargs: (Optional) Dict for model.generate
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Generated text (or list, depending on settings).
|
|
35
|
+
"""
|
|
36
|
+
chat_template_kwargs = chat_template_kwargs or {}
|
|
37
|
+
generation_kwargs = generation_kwargs or {}
|
|
38
|
+
|
|
39
|
+
# If the tokenizer has a chat template (HF 4.34+)
|
|
40
|
+
if hasattr(tokenizer, "___apply_chat_template"):
|
|
41
|
+
kwargs = dict(add_generation_prompt=True, tokenize=False)
|
|
42
|
+
kwargs.update(chat_template_kwargs)
|
|
43
|
+
if chat_template is not None:
|
|
44
|
+
# Override the template (may require tokenizer._chat_template)
|
|
45
|
+
tokenizer._chat_template = chat_template
|
|
46
|
+
prompt = tokenizer.apply_chat_template(messages, **kwargs)
|
|
47
|
+
else:
|
|
48
|
+
# Fallback: simple concatenation
|
|
49
|
+
prompt = "\n".join([msg['content'] for msg in messages])
|
|
50
|
+
|
|
51
|
+
# Tokenize as usual
|
|
52
|
+
inputs = tokenizer(prompt, return_tensors='pt').to(model.device)
|
|
53
|
+
model.eval()
|
|
54
|
+
# Use generate
|
|
55
|
+
output_ids = model.generate(**inputs, **generation_kwargs)
|
|
56
|
+
# Decode only the newly generated tokens (not the prompt)
|
|
57
|
+
output_text = tokenizer.decode(
|
|
58
|
+
output_ids[0, inputs["input_ids"].shape[1]:], skip_special_tokens=True
|
|
59
|
+
)
|
|
60
|
+
return output_text
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# huggingface_model.py
|
|
2
|
+
import importlib
|
|
3
|
+
from typing import Optional, Any
|
|
4
|
+
|
|
5
|
+
def import_module_class(module: str, class_name: str) -> Any:
|
|
6
|
+
"""Dynamically import a class from a module."""
|
|
7
|
+
mod = importlib.import_module(module)
|
|
8
|
+
return getattr(mod, class_name)
|
|
9
|
+
|
|
10
|
+
def load_model(task: Optional[str], checkpoint: str, **kwargs) -> Any:
|
|
11
|
+
"""Load a Huggingface model by task and checkpoint"""
|
|
12
|
+
if task is None or task.lower() == 'embedding':
|
|
13
|
+
model_class = import_module_class('transformers', 'AutoModel')
|
|
14
|
+
elif ":" in task:
|
|
15
|
+
module, class_name = task.split(":")
|
|
16
|
+
model_class = import_module_class(module, class_name)
|
|
17
|
+
else:
|
|
18
|
+
model_class = import_module_class('transformers', f'AutoModelFor{task}')
|
|
19
|
+
return model_class.from_pretrained(checkpoint, **kwargs)
|
|
20
|
+
|
|
21
|
+
def load_tokenizer(checkpoint: str, **kwargs) -> Any:
|
|
22
|
+
"""Load a Huggingface tokenizer"""
|
|
23
|
+
tokenizer_class = import_module_class('transformers', 'AutoTokenizer')
|
|
24
|
+
return tokenizer_class.from_pretrained(checkpoint, **kwargs)
|
|
25
|
+
|
|
26
|
+
def load_model_and_tokenizer(task: Optional[str], checkpoint: str, **kwargs):
|
|
27
|
+
model = load_model(task, checkpoint, **kwargs)
|
|
28
|
+
tokenizer = load_tokenizer(checkpoint, **kwargs)
|
|
29
|
+
return model, tokenizer
|