scout-ai 1.0.1 → 1.1.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 +20 -2
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/bin/scout-ai +46 -0
- data/lib/scout/llm/agent/chat.rb +4 -7
- data/lib/scout/llm/agent/delegate.rb +12 -0
- data/lib/scout/llm/agent.rb +2 -2
- data/lib/scout/llm/ask.rb +18 -2
- data/lib/scout/llm/backends/huggingface.rb +0 -2
- data/lib/scout/llm/backends/ollama.rb +6 -6
- data/lib/scout/llm/backends/openai.rb +7 -4
- data/lib/scout/llm/backends/openwebui.rb +1 -4
- data/lib/scout/llm/backends/relay.rb +1 -3
- data/lib/scout/llm/backends/responses.rb +34 -18
- data/lib/scout/llm/chat/annotation.rb +195 -0
- data/lib/scout/llm/chat/parse.rb +139 -0
- data/lib/scout/llm/chat/process/clear.rb +29 -0
- data/lib/scout/llm/chat/process/files.rb +96 -0
- data/lib/scout/llm/chat/process/options.rb +52 -0
- data/lib/scout/llm/chat/process/tools.rb +173 -0
- data/lib/scout/llm/chat/process.rb +16 -0
- data/lib/scout/llm/chat.rb +26 -662
- data/lib/scout/llm/mcp.rb +1 -1
- data/lib/scout/llm/tools/call.rb +22 -1
- data/lib/scout/llm/tools/knowledge_base.rb +15 -14
- data/lib/scout/llm/tools/mcp.rb +4 -0
- data/lib/scout/llm/tools/workflow.rb +54 -15
- data/lib/scout/llm/tools.rb +42 -0
- data/lib/scout/llm/utils.rb +2 -17
- data/scout-ai.gemspec +13 -4
- data/scout_commands/agent/ask +36 -12
- data/scout_commands/llm/ask +17 -7
- data/scout_commands/llm/process +1 -1
- data/test/scout/llm/backends/test_anthropic.rb +2 -2
- data/test/scout/llm/backends/test_ollama.rb +1 -1
- data/test/scout/llm/backends/test_responses.rb +9 -9
- data/test/scout/llm/chat/test_parse.rb +126 -0
- data/test/scout/llm/chat/test_process.rb +123 -0
- data/test/scout/llm/test_agent.rb +2 -25
- data/test/scout/llm/test_chat.rb +2 -178
- metadata +25 -3
- data/lib/scout/llm/parse.rb +0 -91
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
|
2
|
+
require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
|
|
3
|
+
|
|
4
|
+
class TestParse < Test::Unit::TestCase
|
|
5
|
+
def test_parse_simple_text
|
|
6
|
+
text = "Hello\nWorld"
|
|
7
|
+
msgs = Chat.parse(text)
|
|
8
|
+
assert_equal 1, msgs.size
|
|
9
|
+
assert_equal 'user', msgs[0][:role]
|
|
10
|
+
assert_equal "Hello\nWorld", msgs[0][:content]
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_parse_block_and_inline_headers
|
|
14
|
+
text = <<~TXT
|
|
15
|
+
assistant:
|
|
16
|
+
This is a block
|
|
17
|
+
with lines
|
|
18
|
+
user: inline reply
|
|
19
|
+
another line
|
|
20
|
+
TXT
|
|
21
|
+
|
|
22
|
+
msgs = Chat.parse(text)
|
|
23
|
+
|
|
24
|
+
# Expect a few messages: initial empty user, assistant block, inline user, and final user block
|
|
25
|
+
assert_equal 'user', msgs[0][:role]
|
|
26
|
+
assert_equal '', msgs[0][:content]
|
|
27
|
+
|
|
28
|
+
assert_equal 'assistant', msgs[1][:role]
|
|
29
|
+
assert_equal "This is a block\nwith lines", msgs[1][:content]
|
|
30
|
+
|
|
31
|
+
assert_equal 'user', msgs[2][:role]
|
|
32
|
+
assert_equal 'inline reply', msgs[2][:content]
|
|
33
|
+
|
|
34
|
+
assert_equal 'assistant', msgs[3][:role]
|
|
35
|
+
assert_equal 'another line', msgs[3][:content]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_parse_code_fence_protection
|
|
39
|
+
text = <<~TXT
|
|
40
|
+
assistant:
|
|
41
|
+
Here is code:
|
|
42
|
+
```
|
|
43
|
+
def foo
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
Done
|
|
47
|
+
TXT
|
|
48
|
+
|
|
49
|
+
msgs = Chat.parse(text)
|
|
50
|
+
assert_equal 2, msgs.size # initial empty + assistant
|
|
51
|
+
|
|
52
|
+
assistant_msg = msgs[1]
|
|
53
|
+
assert_equal 'assistant', assistant_msg[:role]
|
|
54
|
+
|
|
55
|
+
expected = "Here is code:\n```\ndef foo\nend\n```\nDone"
|
|
56
|
+
assert_equal expected, assistant_msg[:content]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_parse_xml_protection
|
|
60
|
+
text = <<~TXT
|
|
61
|
+
assistant:
|
|
62
|
+
Before xml
|
|
63
|
+
<note>
|
|
64
|
+
This is protected
|
|
65
|
+
</note>
|
|
66
|
+
After
|
|
67
|
+
TXT
|
|
68
|
+
|
|
69
|
+
msgs = Chat.parse(text)
|
|
70
|
+
assistant_msg = msgs.find { |m| m[:role] == 'assistant' }
|
|
71
|
+
assert assistant_msg
|
|
72
|
+
assert_equal "Before xml\n<note>\nThis is protected\n</note>\nAfter", assistant_msg[:content]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_parse_square_brackets_protection
|
|
76
|
+
text = <<~TXT
|
|
77
|
+
assistant:
|
|
78
|
+
Start
|
|
79
|
+
[[This: has colon
|
|
80
|
+
and lines]]
|
|
81
|
+
End
|
|
82
|
+
TXT
|
|
83
|
+
|
|
84
|
+
msgs = Chat.parse(text)
|
|
85
|
+
assistant_msg = msgs.find { |m| m[:role] == 'assistant' }
|
|
86
|
+
assert assistant_msg
|
|
87
|
+
assert_equal "Start\nThis: has colon\nand lines\nEnd", assistant_msg[:content]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def test_parse_cmd_output_protection
|
|
91
|
+
text = <<~TXT
|
|
92
|
+
assistant:
|
|
93
|
+
Before
|
|
94
|
+
shell:-- ls {{{
|
|
95
|
+
file1
|
|
96
|
+
shell:-- ls }}}
|
|
97
|
+
After
|
|
98
|
+
TXT
|
|
99
|
+
|
|
100
|
+
msgs = Chat.parse(text)
|
|
101
|
+
assistant_msg = msgs.find { |m| m[:role] == 'assistant' }
|
|
102
|
+
assert assistant_msg
|
|
103
|
+
|
|
104
|
+
expected = "Before\n<cmd_output cmd=\"ls\">\nfile1\n</cmd_output>\nAfter"
|
|
105
|
+
assert_equal expected, assistant_msg[:content]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_previous_response_id_behavior
|
|
109
|
+
text = <<~TXT
|
|
110
|
+
previous_response_id:abc123
|
|
111
|
+
Some block
|
|
112
|
+
assistant: Got it
|
|
113
|
+
TXT
|
|
114
|
+
|
|
115
|
+
msgs = Chat.parse(text)
|
|
116
|
+
|
|
117
|
+
# Find the previous_response_id message
|
|
118
|
+
idx = msgs.index { |m| m[:role] == 'previous_response_id' }
|
|
119
|
+
assert idx, 'previous_response_id message not found'
|
|
120
|
+
assert_equal 'abc123', msgs[idx][:content]
|
|
121
|
+
|
|
122
|
+
# The message after previous_response_id should be a user block containing "Some block"
|
|
123
|
+
assert_equal 'user', msgs[idx + 1][:role]
|
|
124
|
+
assert_equal 'Some block', msgs[idx + 1][:content]
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
|
2
|
+
require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
|
|
3
|
+
|
|
4
|
+
require 'scout/llm/chat'
|
|
5
|
+
class TestProcess < Test::Unit::TestCase
|
|
6
|
+
def setup
|
|
7
|
+
super
|
|
8
|
+
@tmp = tmpdir
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def _test_imports_basic_and_continue_last
|
|
12
|
+
TmpFile.with_file do |file|
|
|
13
|
+
Open.write(file, "assistant: hello\nuser: from_import\n")
|
|
14
|
+
|
|
15
|
+
messages = [{role: 'import', content: file}]
|
|
16
|
+
out = Chat.imports(messages)
|
|
17
|
+
|
|
18
|
+
# Should have replaced import with the messages from the file
|
|
19
|
+
roles = out.collect{|m| m[:role]}
|
|
20
|
+
assert_includes roles, 'assistant'
|
|
21
|
+
assert_includes roles, 'user'
|
|
22
|
+
|
|
23
|
+
# Test continue: only last non-empty message
|
|
24
|
+
messages = [{role: 'continue', content: file}]
|
|
25
|
+
out = Chat.imports(messages)
|
|
26
|
+
assert_equal 1, out.size
|
|
27
|
+
assert_equal 'user', out[0][:role]
|
|
28
|
+
assert_equal 'from_import', out[0][:content].strip
|
|
29
|
+
|
|
30
|
+
# Test last: should behave similarly but using purge
|
|
31
|
+
messages = [{role: 'last', content: file}]
|
|
32
|
+
out = Chat.imports(messages)
|
|
33
|
+
assert_equal 1, out.size
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def _test_files_file_reads_and_tags_content
|
|
38
|
+
TmpFile.with_file do |tmp|
|
|
39
|
+
file = File.join(tmp, 'afile.txt')
|
|
40
|
+
Open.write(file, "SOME_UNIQUE_CONTENT_12345")
|
|
41
|
+
|
|
42
|
+
messages = [{role: 'file', content: file}]
|
|
43
|
+
out = Chat.files(messages)
|
|
44
|
+
|
|
45
|
+
assert_equal 1, out.size
|
|
46
|
+
msg = out[0]
|
|
47
|
+
assert_equal 'user', msg[:role]
|
|
48
|
+
# content should include the file content and the filename
|
|
49
|
+
assert_match /SOME_UNIQUE_CONTENT_12345/, msg[:content]
|
|
50
|
+
assert_match /afile.txt/, msg[:content]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def _test_options_extracts_and_resets
|
|
55
|
+
chat = [
|
|
56
|
+
{role: 'endpoint', content: 'http://api.example'},
|
|
57
|
+
{role: 'option', content: 'k1 v1'},
|
|
58
|
+
{role: 'sticky_option', content: 'sk sv'},
|
|
59
|
+
{role: 'assistant', content: 'ok'},
|
|
60
|
+
{role: 'option', content: 'k2 v2'},
|
|
61
|
+
{role: 'user', content: 'do something'}
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
opts = Chat.options(chat)
|
|
65
|
+
|
|
66
|
+
# endpoint should be sticky
|
|
67
|
+
assert_equal 'http://api.example', opts['endpoint']
|
|
68
|
+
# sticky_option should be present
|
|
69
|
+
assert_equal 'sv', opts['sk']
|
|
70
|
+
# first option k1 should have been cleared after assistant
|
|
71
|
+
assert_nil opts['k1']
|
|
72
|
+
# second option should remain
|
|
73
|
+
assert_equal 'v2', opts['k2']
|
|
74
|
+
|
|
75
|
+
# chat should have been replaced and should not include option messages
|
|
76
|
+
roles = chat.collect{|m| m[:role]}
|
|
77
|
+
assert_includes roles, 'assistant'
|
|
78
|
+
assert_includes roles, 'user'
|
|
79
|
+
assert_not_includes roles, 'option'
|
|
80
|
+
assert_not_includes roles, 'sticky_option'
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_tasks_creates_jobs_and_calls_workflow_produce
|
|
84
|
+
# define a minimal workflow class to be resolved by Kernel.const_get
|
|
85
|
+
klass = Class.new do
|
|
86
|
+
def self.job(task_name, jobname=nil, options={})
|
|
87
|
+
# return a simple object with a path that responds to find
|
|
88
|
+
path = Struct.new(:p) do
|
|
89
|
+
def find; p; end
|
|
90
|
+
end
|
|
91
|
+
job = Struct.new(:path).new(path.new("/tmp/fake_job_#{task_name}"))
|
|
92
|
+
job
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Object.const_set('TestWorkflow', klass)
|
|
97
|
+
|
|
98
|
+
produced = nil
|
|
99
|
+
# stub Workflow.produce to capture
|
|
100
|
+
orig = Workflow.method(:produce)
|
|
101
|
+
Workflow.define_singleton_method(:produce) do |jobs|
|
|
102
|
+
produced = jobs
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
begin
|
|
106
|
+
messages = [ {role: 'task', content: 'TestWorkflow mytask jobname=jn param=1'} ]
|
|
107
|
+
out = Chat.tasks(messages)
|
|
108
|
+
|
|
109
|
+
# Should have returned a job message pointing to our fake path
|
|
110
|
+
assert_equal 1, out.size
|
|
111
|
+
assert_equal 'job', out[0][:role]
|
|
112
|
+
assert_match /fake_job_mytask/, out[0][:content]
|
|
113
|
+
|
|
114
|
+
# produce should have been called with the job
|
|
115
|
+
assert_not_nil produced
|
|
116
|
+
assert_equal 1, produced.size
|
|
117
|
+
ensure
|
|
118
|
+
# restore original
|
|
119
|
+
Workflow.define_singleton_method(:produce, orig)
|
|
120
|
+
Object.send(:remove_const, 'TestWorkflow') rescue nil
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
require File.expand_path(__FILE__).sub(%r(/test/.*), '/test/test_helper.rb')
|
|
2
2
|
require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1')
|
|
3
3
|
|
|
4
|
-
require '
|
|
4
|
+
require 'scout/knowledge_base'
|
|
5
5
|
class TestLLMAgent < Test::Unit::TestCase
|
|
6
|
-
def
|
|
6
|
+
def test_system
|
|
7
7
|
TmpFile.with_dir do |dir|
|
|
8
8
|
kb = KnowledgeBase.new dir
|
|
9
9
|
kb.format = {"Person" => "Alias"}
|
|
@@ -13,32 +13,9 @@ class TestLLMAgent < Test::Unit::TestCase
|
|
|
13
13
|
|
|
14
14
|
agent = LLM::Agent.new knowledge_base: kb
|
|
15
15
|
|
|
16
|
-
agent.system = ""
|
|
17
|
-
|
|
18
16
|
sss 0
|
|
19
17
|
ppp agent.ask "Who is Miguel's brother-in-law. Brother in law is your spouses sibling or your sibling's spouse"
|
|
20
|
-
#ppp agent.ask "Who is Guille's brother-in-law. Brother in law is your spouses sibling or your sibling's spouse"
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def _test_system_gepeto
|
|
25
|
-
TmpFile.with_dir do |dir|
|
|
26
|
-
Scout::Config.set(:backend, :ollama, :ask)
|
|
27
|
-
kb = KnowledgeBase.new dir
|
|
28
|
-
kb.format = {"Person" => "Alias"}
|
|
29
|
-
kb.register :brothers, datafile_test(:person).brothers, undirected: true
|
|
30
|
-
kb.register :marriages, datafile_test(:person).marriages, undirected: true, source: "=>Alias", target: "=>Alias"
|
|
31
|
-
kb.register :parents, datafile_test(:person).parents
|
|
32
|
-
|
|
33
|
-
agent = LLM::Agent.new knowledge_base: kb, model: 'mistral', url: "https://gepeto.bsc.es/"
|
|
34
|
-
|
|
35
|
-
agent.system = ""
|
|
36
|
-
|
|
37
|
-
sss 0
|
|
38
|
-
ppp agent.ask "Who is Guille's brother-in-law. Brother in law is your spouses sibling or your sibling's spouse"
|
|
39
18
|
end
|
|
40
19
|
end
|
|
41
|
-
|
|
42
|
-
|
|
43
20
|
end
|
|
44
21
|
|
data/test/scout/llm/test_chat.rb
CHANGED
|
@@ -3,143 +3,9 @@ require File.expand_path(__FILE__).sub(%r(.*/test/), '').sub(/test_(.*)\.rb/,'\1
|
|
|
3
3
|
|
|
4
4
|
class TestMessages < Test::Unit::TestCase
|
|
5
5
|
|
|
6
|
-
def test_short
|
|
7
|
-
|
|
8
|
-
question =<<-EOF
|
|
9
|
-
Hi
|
|
10
|
-
EOF
|
|
11
|
-
|
|
12
|
-
iii LLM.chat(question)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def test_inline
|
|
16
|
-
question =<<-EOF
|
|
17
|
-
system:
|
|
18
|
-
|
|
19
|
-
you are a terse assistant that only write in short sentences
|
|
20
|
-
|
|
21
|
-
assistant:
|
|
22
|
-
|
|
23
|
-
Here is some stuff
|
|
24
|
-
|
|
25
|
-
user: feedback
|
|
26
|
-
|
|
27
|
-
that continues here
|
|
28
|
-
EOF
|
|
29
|
-
|
|
30
|
-
iii LLM.chat(question)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def test_messages
|
|
34
|
-
question =<<-EOF
|
|
35
|
-
system:
|
|
36
|
-
|
|
37
|
-
you are a terse assistant that only write in short sentences
|
|
38
|
-
|
|
39
|
-
user:
|
|
40
|
-
|
|
41
|
-
What is the capital of France
|
|
42
|
-
|
|
43
|
-
assistant:
|
|
44
|
-
|
|
45
|
-
Paris
|
|
46
|
-
|
|
47
|
-
user:
|
|
48
|
-
|
|
49
|
-
is this the national anthem
|
|
50
|
-
|
|
51
|
-
[[
|
|
52
|
-
corous: Viva Espagna
|
|
53
|
-
]]
|
|
54
|
-
|
|
55
|
-
assistant:
|
|
56
|
-
|
|
57
|
-
no
|
|
58
|
-
|
|
59
|
-
user:
|
|
60
|
-
|
|
61
|
-
import: math.system
|
|
62
|
-
|
|
63
|
-
consider this file
|
|
64
|
-
|
|
65
|
-
<file name=foo_bar>
|
|
66
|
-
foo: bar
|
|
67
|
-
</file>
|
|
68
|
-
|
|
69
|
-
how many characters does it hold
|
|
70
|
-
|
|
71
|
-
assistant:
|
|
72
|
-
|
|
73
|
-
8
|
|
74
|
-
EOF
|
|
75
|
-
|
|
76
|
-
messages = LLM.messages question
|
|
77
|
-
refute messages.collect{|i| i[:role] }.include?("corous")
|
|
78
|
-
assert messages.collect{|i| i[:role] }.include?("import")
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def test_chat_import
|
|
82
|
-
file1 =<<-EOF
|
|
83
|
-
system: You are an assistant
|
|
84
|
-
EOF
|
|
85
|
-
|
|
86
|
-
file2 =<<-EOF
|
|
87
|
-
import: header
|
|
88
|
-
user: say something
|
|
89
|
-
EOF
|
|
90
|
-
|
|
91
|
-
TmpFile.with_path do |tmpdir|
|
|
92
|
-
tmpdir.header.write file1
|
|
93
|
-
tmpdir.chat.write file2
|
|
94
|
-
|
|
95
|
-
chat = LLM.chat tmpdir.chat
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
def test_clear
|
|
100
|
-
question =<<-EOF
|
|
101
|
-
system:
|
|
102
|
-
|
|
103
|
-
you are a terse assistant that only write in short sentences
|
|
104
|
-
|
|
105
|
-
clear:
|
|
106
|
-
|
|
107
|
-
user:
|
|
108
|
-
|
|
109
|
-
What is the capital of France
|
|
110
|
-
EOF
|
|
111
|
-
|
|
112
|
-
TmpFile.with_file question do |file|
|
|
113
|
-
messages = LLM.chat file
|
|
114
|
-
refute messages.collect{|m| m[:role] }.include?('system')
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def __test_job
|
|
119
|
-
question =<<-EOF
|
|
120
|
-
system:
|
|
121
|
-
|
|
122
|
-
you are a terse assistant that only write in short sentences
|
|
123
|
-
|
|
124
|
-
job: Baking/bake_muffin_tray/Default_08a1812eca3a18dce2232509dabc9b41
|
|
125
|
-
|
|
126
|
-
How are muffins made
|
|
127
|
-
|
|
128
|
-
EOF
|
|
129
|
-
|
|
130
|
-
TmpFile.with_file question do |file|
|
|
131
|
-
messages = LLM.chat file
|
|
132
|
-
ppp LLM.print messages
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
|
|
136
6
|
|
|
137
7
|
def test_task
|
|
138
8
|
question =<<-EOF
|
|
139
|
-
system:
|
|
140
|
-
|
|
141
|
-
you are a terse assistant that only write in short sentences
|
|
142
|
-
|
|
143
9
|
user:
|
|
144
10
|
|
|
145
11
|
task: Baking bake_muffin_tray blueberries=true title="This is a title" list=one,two,"and three"
|
|
@@ -150,26 +16,8 @@ How are muffins made?
|
|
|
150
16
|
|
|
151
17
|
TmpFile.with_file question do |file|
|
|
152
18
|
messages = LLM.chat file
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
def test_structure
|
|
158
|
-
require 'scout/llm/ask'
|
|
159
|
-
sss 0
|
|
160
|
-
question =<<-EOF
|
|
161
|
-
system:
|
|
162
|
-
|
|
163
|
-
Respond in json format with a hash of strings as keys and string arrays as values, at most three in length
|
|
164
|
-
|
|
165
|
-
endpoint: sambanova
|
|
166
|
-
|
|
167
|
-
What other movies have the protagonists of the original gost busters played on, just the top.
|
|
168
|
-
|
|
169
|
-
EOF
|
|
170
|
-
|
|
171
|
-
TmpFile.with_file question do |file|
|
|
172
|
-
ppp LLM.ask file
|
|
19
|
+
assert_include messages.collect{|m| m[:role] }, 'function_call'
|
|
20
|
+
assert_include messages.find{|m| m[:role] == 'function_call_output' }[:content], 'Baking'
|
|
173
21
|
end
|
|
174
22
|
end
|
|
175
23
|
|
|
@@ -228,29 +76,5 @@ association: marriages #{datafile_test(:person).marriages} undirected=true sourc
|
|
|
228
76
|
ppp LLM.ask file
|
|
229
77
|
end
|
|
230
78
|
end
|
|
231
|
-
|
|
232
|
-
def test_previous_response
|
|
233
|
-
require 'scout/llm/ask'
|
|
234
|
-
sss 0
|
|
235
|
-
question =<<-EOF
|
|
236
|
-
user:
|
|
237
|
-
|
|
238
|
-
Say hi
|
|
239
|
-
|
|
240
|
-
assistant:
|
|
241
|
-
|
|
242
|
-
Hi
|
|
243
|
-
|
|
244
|
-
previous_response_id: asdfasdfasdfasdf
|
|
245
|
-
|
|
246
|
-
Bye
|
|
247
|
-
|
|
248
|
-
EOF
|
|
249
|
-
|
|
250
|
-
messages = LLM.messages question
|
|
251
|
-
|
|
252
|
-
iii messages
|
|
253
|
-
|
|
254
|
-
end
|
|
255
79
|
end
|
|
256
80
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: scout-ai
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Miguel Vazquez
|
|
@@ -37,6 +37,20 @@ dependencies:
|
|
|
37
37
|
- - ">="
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: ollama-ai
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
55
|
name: ruby-mcp-client
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -89,9 +103,15 @@ files:
|
|
|
89
103
|
- lib/scout/llm/backends/relay.rb
|
|
90
104
|
- lib/scout/llm/backends/responses.rb
|
|
91
105
|
- lib/scout/llm/chat.rb
|
|
106
|
+
- lib/scout/llm/chat/annotation.rb
|
|
107
|
+
- lib/scout/llm/chat/parse.rb
|
|
108
|
+
- lib/scout/llm/chat/process.rb
|
|
109
|
+
- lib/scout/llm/chat/process/clear.rb
|
|
110
|
+
- lib/scout/llm/chat/process/files.rb
|
|
111
|
+
- lib/scout/llm/chat/process/options.rb
|
|
112
|
+
- lib/scout/llm/chat/process/tools.rb
|
|
92
113
|
- lib/scout/llm/embed.rb
|
|
93
114
|
- lib/scout/llm/mcp.rb
|
|
94
|
-
- lib/scout/llm/parse.rb
|
|
95
115
|
- lib/scout/llm/rag.rb
|
|
96
116
|
- lib/scout/llm/tools.rb
|
|
97
117
|
- lib/scout/llm/tools/call.rb
|
|
@@ -145,6 +165,8 @@ files:
|
|
|
145
165
|
- test/scout/llm/backends/test_openwebui.rb
|
|
146
166
|
- test/scout/llm/backends/test_relay.rb
|
|
147
167
|
- test/scout/llm/backends/test_responses.rb
|
|
168
|
+
- test/scout/llm/chat/test_parse.rb
|
|
169
|
+
- test/scout/llm/chat/test_process.rb
|
|
148
170
|
- test/scout/llm/test_agent.rb
|
|
149
171
|
- test/scout/llm/test_ask.rb
|
|
150
172
|
- test/scout/llm/test_chat.rb
|
|
@@ -186,7 +208,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
186
208
|
- !ruby/object:Gem::Version
|
|
187
209
|
version: '0'
|
|
188
210
|
requirements: []
|
|
189
|
-
rubygems_version: 3.7.
|
|
211
|
+
rubygems_version: 3.7.2
|
|
190
212
|
specification_version: 4
|
|
191
213
|
summary: AI gear for scouts
|
|
192
214
|
test_files: []
|
data/lib/scout/llm/parse.rb
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
require 'scout/llm/utils'
|
|
2
|
-
module LLM
|
|
3
|
-
def self.process_inside(inside)
|
|
4
|
-
header, content = inside.match(/([^\n]*)\n(.*)/).values_at 1, 2
|
|
5
|
-
if header.empty?
|
|
6
|
-
content
|
|
7
|
-
else
|
|
8
|
-
action, _sep, rest = header.partition /\s/
|
|
9
|
-
case action
|
|
10
|
-
when 'import'
|
|
11
|
-
when 'cmd'
|
|
12
|
-
title = rest.strip.empty? ? content : rest
|
|
13
|
-
tag('file', title, CMD.cmd(content).read)
|
|
14
|
-
when 'file'
|
|
15
|
-
file = content
|
|
16
|
-
title = rest.strip.empty? ? file : rest
|
|
17
|
-
tag(action, title, Open.read(file))
|
|
18
|
-
when 'directory'
|
|
19
|
-
directory = content
|
|
20
|
-
title = rest.strip.empty? ? directory : rest
|
|
21
|
-
directory_content = Dir.glob(File.join(directory, '**/*')).collect do |file|
|
|
22
|
-
file_title = Misc.path_relative_to(directory, file)
|
|
23
|
-
tag('file', file_title, Open.read(file) )
|
|
24
|
-
end * "\n"
|
|
25
|
-
tag(action, title, directory_content )
|
|
26
|
-
else
|
|
27
|
-
tag(action, rest, content)
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def self.parse(question, role = nil)
|
|
33
|
-
role = :user if role.nil?
|
|
34
|
-
|
|
35
|
-
if Array === question
|
|
36
|
-
question.collect do |q|
|
|
37
|
-
Hash === q ? q : {role: role, content: q}
|
|
38
|
-
end
|
|
39
|
-
else
|
|
40
|
-
if m = question.match(/(.*?)\[\[(.*?)\]\](.*)/m)
|
|
41
|
-
pre = m[1]
|
|
42
|
-
inside = m[2]
|
|
43
|
-
post = m[3]
|
|
44
|
-
messages = parse(pre, role)
|
|
45
|
-
|
|
46
|
-
messages = [{role: role, content: ''}] if messages.empty?
|
|
47
|
-
messages.last[:content] += process_inside inside
|
|
48
|
-
|
|
49
|
-
last = parse(post, messages.last[:role])
|
|
50
|
-
|
|
51
|
-
messages.concat last
|
|
52
|
-
|
|
53
|
-
messages
|
|
54
|
-
elsif m = question.match(/(.*?)(```.*?```)(.*)/m)
|
|
55
|
-
pre = m[1]
|
|
56
|
-
inside = m[2]
|
|
57
|
-
post = m[3]
|
|
58
|
-
messages = parse(pre, role)
|
|
59
|
-
|
|
60
|
-
messages = [{role: role, content: ''}] if messages.empty?
|
|
61
|
-
messages.last[:content] += inside
|
|
62
|
-
|
|
63
|
-
last = parse(post, messages.last[:role])
|
|
64
|
-
|
|
65
|
-
if last.first[:role] == messages.last[:role]
|
|
66
|
-
m = last.shift
|
|
67
|
-
messages.last[:content] += m[:content]
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
messages.concat last
|
|
71
|
-
|
|
72
|
-
messages
|
|
73
|
-
else
|
|
74
|
-
chunks = question.scan(/(.*?)^(\w+):(.*?)(?=^\w+:|\z)/m)
|
|
75
|
-
|
|
76
|
-
if chunks.any?
|
|
77
|
-
messages = []
|
|
78
|
-
messages << {role: role, content: chunks.first.first} if chunks.first and not chunks.first.first.empty?
|
|
79
|
-
chunks.collect do |pre,role,text|
|
|
80
|
-
messages << {role: role, content: text.strip}
|
|
81
|
-
end
|
|
82
|
-
messages
|
|
83
|
-
elsif question.strip.empty?
|
|
84
|
-
[]
|
|
85
|
-
else
|
|
86
|
-
[{role: role, content: question}]
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|