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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.vimproject +155 -9
  3. data/README.md +296 -0
  4. data/Rakefile +3 -0
  5. data/VERSION +1 -1
  6. data/bin/scout-ai +2 -0
  7. data/doc/Agent.md +279 -0
  8. data/doc/Chat.md +258 -0
  9. data/doc/LLM.md +446 -0
  10. data/doc/Model.md +513 -0
  11. data/doc/RAG.md +129 -0
  12. data/lib/scout/llm/agent/chat.rb +74 -0
  13. data/lib/scout/llm/agent/delegate.rb +39 -0
  14. data/lib/scout/llm/agent/iterate.rb +44 -0
  15. data/lib/scout/llm/agent.rb +51 -30
  16. data/lib/scout/llm/ask.rb +63 -21
  17. data/lib/scout/llm/backends/anthropic.rb +147 -0
  18. data/lib/scout/llm/backends/bedrock.rb +129 -0
  19. data/lib/scout/llm/backends/huggingface.rb +6 -21
  20. data/lib/scout/llm/backends/ollama.rb +62 -35
  21. data/lib/scout/llm/backends/openai.rb +77 -33
  22. data/lib/scout/llm/backends/openwebui.rb +1 -1
  23. data/lib/scout/llm/backends/relay.rb +3 -2
  24. data/lib/scout/llm/backends/responses.rb +320 -0
  25. data/lib/scout/llm/chat.rb +703 -0
  26. data/lib/scout/llm/embed.rb +4 -4
  27. data/lib/scout/llm/mcp.rb +28 -0
  28. data/lib/scout/llm/parse.rb +71 -13
  29. data/lib/scout/llm/rag.rb +9 -0
  30. data/lib/scout/llm/tools/call.rb +66 -0
  31. data/lib/scout/llm/tools/knowledge_base.rb +158 -0
  32. data/lib/scout/llm/tools/mcp.rb +59 -0
  33. data/lib/scout/llm/tools/workflow.rb +69 -0
  34. data/lib/scout/llm/tools.rb +112 -76
  35. data/lib/scout/llm/utils.rb +17 -10
  36. data/lib/scout/model/base.rb +19 -0
  37. data/lib/scout/model/python/base.rb +25 -0
  38. data/lib/scout/model/python/huggingface/causal/next_token.rb +23 -0
  39. data/lib/scout/model/python/huggingface/causal.rb +29 -0
  40. data/lib/scout/model/python/huggingface/classification +0 -0
  41. data/lib/scout/model/python/huggingface/classification.rb +50 -0
  42. data/lib/scout/model/python/huggingface.rb +112 -0
  43. data/lib/scout/model/python/torch/dataloader.rb +57 -0
  44. data/lib/scout/model/python/torch/helpers.rb +84 -0
  45. data/lib/scout/model/python/torch/introspection.rb +34 -0
  46. data/lib/scout/model/python/torch/load_and_save.rb +47 -0
  47. data/lib/scout/model/python/torch.rb +94 -0
  48. data/lib/scout/model/util/run.rb +181 -0
  49. data/lib/scout/model/util/save.rb +81 -0
  50. data/lib/scout-ai.rb +4 -1
  51. data/python/scout_ai/__init__.py +35 -0
  52. data/python/scout_ai/huggingface/data.py +48 -0
  53. data/python/scout_ai/huggingface/eval.py +60 -0
  54. data/python/scout_ai/huggingface/model.py +29 -0
  55. data/python/scout_ai/huggingface/rlhf.py +83 -0
  56. data/python/scout_ai/huggingface/train/__init__.py +34 -0
  57. data/python/scout_ai/huggingface/train/next_token.py +315 -0
  58. data/python/scout_ai/util.py +32 -0
  59. data/scout-ai.gemspec +143 -0
  60. data/scout_commands/agent/ask +89 -14
  61. data/scout_commands/agent/kb +15 -0
  62. data/scout_commands/documenter +148 -0
  63. data/scout_commands/llm/ask +71 -12
  64. data/scout_commands/llm/process +4 -2
  65. data/scout_commands/llm/server +319 -0
  66. data/share/server/chat.html +138 -0
  67. data/share/server/chat.js +468 -0
  68. data/test/data/cat.jpg +0 -0
  69. data/test/scout/llm/agent/test_chat.rb +14 -0
  70. data/test/scout/llm/backends/test_anthropic.rb +134 -0
  71. data/test/scout/llm/backends/test_bedrock.rb +60 -0
  72. data/test/scout/llm/backends/test_huggingface.rb +3 -3
  73. data/test/scout/llm/backends/test_ollama.rb +48 -10
  74. data/test/scout/llm/backends/test_openai.rb +134 -10
  75. data/test/scout/llm/backends/test_responses.rb +239 -0
  76. data/test/scout/llm/test_agent.rb +0 -70
  77. data/test/scout/llm/test_ask.rb +4 -1
  78. data/test/scout/llm/test_chat.rb +256 -0
  79. data/test/scout/llm/test_mcp.rb +29 -0
  80. data/test/scout/llm/test_parse.rb +81 -2
  81. data/test/scout/llm/tools/test_call.rb +0 -0
  82. data/test/scout/llm/tools/test_knowledge_base.rb +22 -0
  83. data/test/scout/llm/tools/test_mcp.rb +11 -0
  84. data/test/scout/llm/tools/test_workflow.rb +39 -0
  85. data/test/scout/model/python/huggingface/causal/test_next_token.rb +59 -0
  86. data/test/scout/model/python/huggingface/test_causal.rb +33 -0
  87. data/test/scout/model/python/huggingface/test_classification.rb +30 -0
  88. data/test/scout/model/python/test_base.rb +44 -0
  89. data/test/scout/model/python/test_huggingface.rb +9 -0
  90. data/test/scout/model/python/test_torch.rb +71 -0
  91. data/test/scout/model/python/torch/test_helpers.rb +14 -0
  92. data/test/scout/model/test_base.rb +117 -0
  93. data/test/scout/model/util/test_save.rb +31 -0
  94. metadata +113 -7
  95. data/README.rdoc +0 -18
  96. data/questions/coach +0 -2
@@ -19,8 +19,12 @@ Use STDIN to add context to the question
19
19
  -h--help Print this help
20
20
  -l--log* Log level
21
21
  -t--template* Use a template
22
+ -c--chat* Follow a conversation
22
23
  -m--model* Model to use
23
- -f--file* Incorporate file at the start
24
+ -e--endpoint* Endpoint to use
25
+ -f--file* Incorporate file
26
+ -wt--workflow_tasks* Export these tasks to the agent
27
+ -i--imports* Chat files to import, separated by comma
24
28
  EOF
25
29
  if options[:help]
26
30
  if defined? scout_usage
@@ -33,38 +37,109 @@ end
33
37
 
34
38
  Log.severity = options.delete(:log).to_i if options.include? :log
35
39
 
36
- file = options.delete(:file)
40
+ agent_name, *question_parts = ARGV
37
41
 
38
- agent, *question_parts = ARGV
42
+ question = question_parts * " "
39
43
 
44
+ file, chat, inline, template, dry_run, imports = IndiferentHash.process_options options, :file, :chat, :inline, :template, :dry_run, :imports
40
45
 
41
- workflow = begin
42
- Workflow.require_workflow agent
43
- rescue
44
- end
46
+ file = Path.setup(file) if file
45
47
 
46
- knowledge_base = begin workflow.knowledge_base rescue nil end || KnowledgeBase.new(Scout.var.Agent[agent])
48
+ imports = imports.split(/,\s*/) if imports
47
49
 
48
- agent = LLM::Agent.new workflow: workflow, knowledge_base: knowledge_base
49
50
 
50
- question = question_parts * " "
51
+ agent_name ||= 'default'
51
52
 
52
- if template = options.delete(:template)
53
+ agent_file = Scout.workflows[agent_name]
54
+
55
+ agent_file = Scout.chats[agent_name] unless agent_file.exists?
56
+
57
+ agent_file = agent_file.find_with_extension('rb') unless agent_file.exists?
58
+
59
+
60
+ if agent_file.exists?
61
+ if agent_file.directory?
62
+ if agent_file.agent.find_with_extension('rb').exists?
63
+ agent = load agent_file.agent.find_with_extension('rb')
64
+ else
65
+ agent = LLM::Agent.load_from_path agent_file
66
+ end
67
+ else
68
+ agent = load agent_file
69
+ end
70
+ else
71
+ #raise ParameterException agent_file
72
+ end
73
+
74
+ if template
53
75
  if Open.exists?(template)
54
76
  template_question = Open.read(template)
55
- else
77
+ elsif Scout.questions[template].exists?
56
78
  template_question = Scout.questions[template].read
79
+ elsif Scout.chats.system[template].exists?
80
+ template_question = Scout.chats.system[template].read
81
+ elsif Scout.chats[template].exists?
82
+ template_question = Scout.chats[template].read
57
83
  end
58
84
  if template_question.include?('???')
59
85
  question = template_question.sub('???', question)
86
+ elsif not question.empty?
87
+ question = template_question + "\nuser: #{question}"
60
88
  else
61
89
  question = template_question
62
90
  end
63
91
  end
64
92
 
65
93
  if question.include?('...')
66
- context = file ? Open.read(file) : STDIN.read
94
+ context = file ? Open.read(file) : STDIN.read
67
95
  question = question.sub('...', context)
96
+ elsif file
97
+ question = "<file basename=#{File.basename file}>\n" + Open.read(file) + "\n</file>\n\n" + question
68
98
  end
69
99
 
70
- puts LLM.ask(question, options)
100
+ if chat
101
+ conversation = Open.exist?(chat)? LLM.chat(chat) : []
102
+ convo_options = LLM.options conversation
103
+ conversation = question.empty? ? conversation : conversation + LLM.chat(question)
104
+
105
+ if dry_run
106
+ ppp LLM.print conversation
107
+ exit 0
108
+ end
109
+ new = agent.ask(conversation, convo_options.merge(options.merge(return_messages: true)))
110
+ conversation = Open.read(chat) + LLM.print(new)
111
+ Open.write(chat, conversation)
112
+ elsif inline
113
+
114
+ file = Open.read inline
115
+
116
+ new_file = ""
117
+ while true
118
+ pre, question, post =
119
+ file.partition(/^\s*#\s*ask:(?:.*?)(?=^\s*[^\s#])/smu)
120
+
121
+ break if post.empty?
122
+
123
+ new_file << pre
124
+ new_file << question
125
+ clean_question = question.gsub('#', '').gsub(/\s+/,' ').sub(/.*ask:\s*/,'').strip
126
+ chat = [
127
+ {role: :system, content: "Write a succint reply with no commentary and no formatting."},
128
+ {role: :user, content: "Find the following question as a comment in the file give a response to be placed inline: #{question}"},
129
+ LLM.tag('file', file, inline)
130
+ ]
131
+ response = LLM.ask(LLM.chat(chat))
132
+ new_file << <<-EOF
133
+ # Response start
134
+ #{response}
135
+ # Response end
136
+ EOF
137
+ file = post
138
+ end
139
+ new_file << file
140
+ Open.write(inline, new_file)
141
+ else
142
+ conversation = Chat.setup(LLM.chat question)
143
+ imports.each{|import| conversation.import import } if imports
144
+ puts agent.ask(conversation, nil, options)
145
+ end
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ agent = ARGV.shift
4
+
5
+ agent_dir = Scout.var.Agent[agent]
6
+
7
+ if ARGV.any?
8
+ ARGV.push "--knowledge_base"
9
+ ARGV.push agent_dir.knowledge_base
10
+ ARGV.push "--log"
11
+ ARGV.push Log.severity.to_s
12
+ end
13
+ ARGV.unshift 'kb'
14
+
15
+ load Scout.bin.scout.find
@@ -0,0 +1,148 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'scout'
4
+
5
+ $0 = "scout-ai #{$previous_commands.any? ? $previous_commands*" " + " " : "" }#{ File.basename(__FILE__) }" if $previous_commands
6
+
7
+ options = SOPT.setup <<EOF
8
+ Scout documenter tool
9
+
10
+ $ #{$0} [<options>] <topic>
11
+
12
+ Generates technical, example-driven documentation for a given Scout topic by
13
+ analyzing Ruby source and corresponding test files. For a specified topic, it
14
+ locates main and subtopic files, invokes an LLM agent to synthesize markdown
15
+ documentation using real test behavior and examples, then outputs comprehensive
16
+ topic and subtopic documentation files.
17
+
18
+ -h--help Print this help
19
+ EOF
20
+ if options[:help]
21
+ if defined? scout_usage
22
+ scout_usage
23
+ else
24
+ puts SOPT.doc
25
+ end
26
+ exit 0
27
+ end
28
+
29
+ require 'scout-ai'
30
+
31
+ topic = ARGV.first
32
+
33
+ raise MissingParameterException if topic.nil?
34
+
35
+ src_files = Scout.lib.scout.glob("#{topic}*/**/*") + Scout.lib.scout.glob("#{topic}.rb")
36
+ src_files.collect! do |file|
37
+ file.sub(Dir.pwd, '.')
38
+ end
39
+
40
+ bin_files = Scout.scout_commands.glob("**/*").select{|file| file.include?(topic) && ! file.directory? }
41
+
42
+ main = src_files.select{|f| f.split("/").length == 4}.first
43
+ subtopics = src_files.select{|f| f.split("/").length == 5}.collect{|f| File.basename(f).sub(".rb",'') }.uniq
44
+
45
+ def source_to_test(file)
46
+ file.sub(%r{\A./lib/}, './test/').sub(%r{([^/]+)\.rb\z}, 'test_\1.rb')
47
+ end
48
+
49
+ documenter = LLM::Agent.new
50
+
51
+ documenter.start_chat.system <<-EOF
52
+ You are a world-class Ruby documentation author. For each (source, test) file
53
+ pair given, produce technically precise module- and file-level documentation,
54
+ incorporating specific code usage and behavior from the test file as worked
55
+ examples, code idioms, and edge-case handling.
56
+
57
+ Never insert your own example code: always use live content from the tests as examples.
58
+
59
+ Integrate documentation and test-derived examples smoothly.
60
+
61
+ You will be given first the main topic documentation for the main file and
62
+ test_file, then you will be asked to produce documentation for a subtopic.
63
+
64
+ Finally you will be ask to aggregate all the documentation portions into
65
+ a final topic documentation file
66
+
67
+ User markdown
68
+
69
+ Avoid initial and final comments like: Certainly! I'll do this and that
70
+ EOF
71
+
72
+ documenter.start_chat.file main
73
+ documenter.start_chat.file source_to_test(main)
74
+
75
+ documenter.start_chat.user <<-EOF
76
+ This is the basic topic file. Write the markdown documentation for it.
77
+ EOF
78
+
79
+ docs = {}
80
+ subtopics.each do |subtopic|
81
+ src = src_files.select{|f| f.include? subtopic}.select{|f| f.end_with?(".rb") }
82
+ test = src.collect{|f| source_to_test(f) }.select{|f| Open.exists? f }.select{|f| f.end_with?(".rb")}
83
+
84
+ documenter.start
85
+ (src + test + bin_files).each do |file|
86
+ documenter.file file
87
+ end
88
+
89
+ documenter.start_chat.user <<-EOF
90
+ Write documentation for topic #{topic} subtopic #{subtopic}
91
+ EOF
92
+ docs[subtopic] = documenter.respond
93
+ end
94
+
95
+ documenter.start
96
+
97
+ docs.each do |subtopic, documentation|
98
+ documenter.user <<-EOF
99
+ Please construct a comprehensive documentation on topic #{topic}. For each
100
+ subtopic reproduce all the most important from the original documentation
101
+ files. The subtopic documentation files will not be available anymore, so
102
+ don't leave anything imporant out
103
+
104
+ <file subtopic=#{subtopic}>
105
+ #{documentation}
106
+ <file>
107
+ EOF
108
+ end
109
+
110
+ main_documentation = documenter.chat
111
+
112
+ documenter.start_chat.user <<-EOF
113
+ This is the revise documentation for the topic:
114
+
115
+ ---
116
+
117
+ #{main_documentation}
118
+
119
+ ---
120
+ EOF
121
+
122
+ revised_subtopics = {}
123
+ docs.each do |subtopic, documentation|
124
+ documenter.start
125
+
126
+ src = src_files.select{|f| f.include? subtopic}.select{|f| f.end_with?(".rb") }
127
+ test = src.collect{|f| source_to_test(f) }.select{|f| Open.exists? f }.select{|f| f.end_with?(".rb")}
128
+
129
+ documenter.start
130
+ (src + test + bin_files).each do |file|
131
+ documenter.file file
132
+ end
133
+
134
+ documenter.user <<-EOF
135
+ Please revise the subtopic documentation in light of the revised main_documentation
136
+
137
+ <file subtopic=#{subtopic}>
138
+ #{documentation}
139
+ <file>
140
+ EOF
141
+ revised_subtopics[subtopic] = documenter.respond
142
+ end
143
+
144
+
145
+ Open.write Scout.doc.lib.scout[topic + '.md'].find(:current), main_documentation
146
+ revised_subtopics.each do |subtopic,documentation|
147
+ Open.write Scout.doc.lib.scout[topic][subtopic + '.md'].find(:current), documentation
148
+ end
@@ -3,22 +3,30 @@
3
3
  require 'scout'
4
4
  require 'scout-ai'
5
5
 
6
- $0 = "scout #{$previous_commands.any? ? $previous_commands*" " + " " : "" }#{ File.basename(__FILE__) }" if $previous_commands
6
+ $0 = "scout-ai #{$previous_commands.any? ? $previous_commands*" " + " " : "" }#{ File.basename(__FILE__) }" if $previous_commands
7
7
 
8
8
  options = SOPT.setup <<EOF
9
+ Ask an LLM model
9
10
 
10
- Ask GPT
11
+ $ #{$0} [<options>] [<question>]
11
12
 
12
- $ #{$0} [<options>] [question]
13
-
14
- Use STDIN to add context to the question
13
+ Use STDIN to add context to the question. The context can be referenced using
14
+ three dots '...'. The model will be prompted with the question, unless the
15
+ inline option is used. If the chat option is used, the response will be added
16
+ to the end of the file. If the file option is used the file contents will be
17
+ prepended before the question. With the template option, the file will be read
18
+ as if it were the question, and the actual question will be placed under the
19
+ characters '???', if they are present.
15
20
 
16
21
  -h--help Print this help
17
- -l--log* Log level
18
22
  -t--template* Use a template
23
+ -c--chat* Follow a conversation
24
+ -i--inline* Ask inline questions about a file
25
+ -f--file* Incorporate file at the start
19
26
  -m--model* Model to use
20
27
  -e--endpoint* Endpoint to use
21
- -f--file* Incorporate file at the start
28
+ -b--backend* Backend to use
29
+ -d--dry_run Dry run, don't ask
22
30
  EOF
23
31
  if options[:help]
24
32
  if defined? scout_usage
@@ -31,26 +39,77 @@ end
31
39
 
32
40
  Log.severity = options.delete(:log).to_i if options.include? :log
33
41
 
34
- file = options.delete(:file)
42
+ file, chat, inline, template, dry_run = IndiferentHash.process_options options, :file, :chat, :inline, :template, :dry_run
35
43
 
36
44
  question = ARGV * " "
37
45
 
38
- if template = options.delete(:template)
46
+ if template
39
47
  if Open.exists?(template)
40
48
  template_question = Open.read(template)
41
- else
49
+ elsif Scout.questions[template].exists?
42
50
  template_question = Scout.questions[template].read
51
+ elsif Scout.chats.system[template].exists?
52
+ template_question = Scout.chats.system[template].read
53
+ elsif Scout.chats[template].exists?
54
+ template_question = Scout.chats[template].read
43
55
  end
44
56
  if template_question.include?('???')
45
57
  question = template_question.sub('???', question)
58
+ elsif not question.empty?
59
+ question = template_question + "\nuser: #{question}"
46
60
  else
47
61
  question = template_question
48
62
  end
49
63
  end
50
64
 
51
65
  if question.include?('...')
52
- context = file ? Open.read(file) : STDIN.read
66
+ context = file ? Open.read(file) : STDIN.read
53
67
  question = question.sub('...', context)
68
+ elsif file
69
+ question = "<file basename=#{File.basename file}>[[[\n" + Open.read(file) + "\n]]]</file>"
54
70
  end
55
71
 
56
- puts LLM.ask(question, options)
72
+ if chat
73
+ conversation = Open.exist?(chat)? LLM.chat(chat) : []
74
+ convo_options = LLM.options conversation
75
+ conversation = question.empty? ? conversation : conversation + LLM.chat(question)
76
+
77
+ if dry_run
78
+ ppp LLM.print conversation
79
+ exit 0
80
+ end
81
+ new = LLM.ask(conversation, convo_options.merge(options.merge(return_messages: true)))
82
+ conversation = Open.read(chat) + LLM.print(new)
83
+ Open.write(chat, conversation)
84
+ elsif inline
85
+
86
+ file = Open.read inline
87
+
88
+ new_file = ""
89
+ while true
90
+ pre, question, post =
91
+ file.partition(/^\s*#\s*ask:(?:.*?)(?=^\s*[^\s#]|\z)/smu)
92
+
93
+ break if question.empty?
94
+
95
+ new_file << pre
96
+ new_file << question
97
+ clean_question = question.gsub('#', '').gsub(/\s+/,' ').sub(/.*ask:\s*/,'').strip
98
+ chat = [
99
+ {role: :system, content: "Write a succint reply with no commentary and no formatting."},
100
+ {role: :user, content: "Find the following question as a comment in the file give a response to be placed inline: #{question}"},
101
+ LLM.tag('file', file, inline)
102
+ ]
103
+ response = LLM.ask(LLM.chat(chat))
104
+ new_file << <<-EOF
105
+ # Response start
106
+ #{response}
107
+ # Response end
108
+ EOF
109
+ file = post
110
+ end
111
+ new_file << file
112
+ Open.write(inline, new_file)
113
+ else
114
+ puts LLM.ask(question, options)
115
+ end
@@ -32,7 +32,9 @@ directory = ARGV.first || Scout.var.ask.find
32
32
  directory = Path.setup directory
33
33
 
34
34
  while true
35
- directory.glob('*.json').each do |file|
35
+ files = directory.glob('*.json')
36
+
37
+ files.each do |file|
36
38
  target = directory.reply[id + '.json']
37
39
 
38
40
  if ! File.exist?(target)
@@ -46,5 +48,5 @@ while true
46
48
  Open.rm(file)
47
49
  end
48
50
 
49
- sleep 1
51
+ sleep 1 if files.empty?
50
52
  end