scout-ai 1.2.2 → 1.2.3
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 +7 -0
- data/VERSION +1 -1
- data/lib/scout/llm/chat/process/tools.rb +12 -7
- data/python/.gitignore +8 -0
- data/python/README.md +91 -0
- data/python/pyproject.toml +50 -0
- data/python/scout_ai/runner.py +52 -11
- data/python/tests/test_runner.py +27 -0
- data/scout-ai.gemspec +7 -3
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8549d8d309d5c6fa5b4b259db26ac7001cafa2fd10b99fe552e6f733a8a8d72e
|
|
4
|
+
data.tar.gz: 52f728fa8fd19b5c6bc6a4f2ec8245241345946ffe682a8d54ba89dd4a61ca14
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 884c4612317b94f3511118c62a03d5a665f42107ebdeb233d13618bd22e23b97aa290025d054c5b11683334570a870fbdab6a75f662dca3d5639d54d383fb7d6
|
|
7
|
+
data.tar.gz: f7c59e09e5e0f6052204033d23de97f1202eb762ee548aee5caac63081d69f8f9fcd5c5170a99e76ad73c8d2b99578c5ddda7ee8897ced9dc6f09f7fafac6e00
|
data/.vimproject
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.txt *.conf" {
|
|
2
2
|
README.md
|
|
3
|
+
Rakefile
|
|
3
4
|
chats=chats filter="*"{
|
|
4
5
|
system=system{
|
|
5
6
|
scout-ai
|
|
@@ -50,6 +51,12 @@ scout-ai=$PWD filter="*.rb *.rake Rakefile *.rdoc *.R *.sh *.js *.haml *.sass *.
|
|
|
50
51
|
test_prompt.rb
|
|
51
52
|
|
|
52
53
|
from_python
|
|
54
|
+
|
|
55
|
+
python_delegation
|
|
56
|
+
|
|
57
|
+
python_package
|
|
58
|
+
|
|
59
|
+
stderr_python
|
|
53
60
|
}
|
|
54
61
|
lib=lib {
|
|
55
62
|
scout-ai.rb
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.2.
|
|
1
|
+
1.2.3
|
|
@@ -109,8 +109,10 @@ module Chat
|
|
|
109
109
|
|
|
110
110
|
def self.tools(messages)
|
|
111
111
|
tool_definitions = IndiferentHash.setup({})
|
|
112
|
+
introduced_workflows = []
|
|
112
113
|
new = messages.collect do |message|
|
|
113
|
-
|
|
114
|
+
role = message[:role]
|
|
115
|
+
if role == 'mcp'
|
|
114
116
|
url, *tools = content_tokens(message)
|
|
115
117
|
|
|
116
118
|
if url == 'stdio'
|
|
@@ -128,7 +130,7 @@ module Chat
|
|
|
128
130
|
tool_definitions.merge!(mcp_tool_definitions)
|
|
129
131
|
end
|
|
130
132
|
next
|
|
131
|
-
elsif
|
|
133
|
+
elsif role == 'tool'
|
|
132
134
|
workflow_name, task_name, *inputs = content_tokens(message)
|
|
133
135
|
inputs = nil if inputs.empty?
|
|
134
136
|
inputs = [] if inputs == ['none'] || inputs == ['noinputs']
|
|
@@ -148,8 +150,10 @@ module Chat
|
|
|
148
150
|
tool_definitions.merge!(LLM.workflow_tools(workflow))
|
|
149
151
|
end
|
|
150
152
|
next
|
|
151
|
-
elsif
|
|
153
|
+
elsif role == 'introduce'
|
|
152
154
|
workflow_name = message[:content]
|
|
155
|
+
next if introduced_workflows.include? workflow_name
|
|
156
|
+
introduced_workflows << workflow_name
|
|
153
157
|
workflow = begin
|
|
154
158
|
Kernel.const_get workflow_name
|
|
155
159
|
rescue
|
|
@@ -176,7 +180,7 @@ Below is the documentation of the workflow:
|
|
|
176
180
|
EOF
|
|
177
181
|
|
|
178
182
|
{role: :user, content: content}
|
|
179
|
-
elsif
|
|
183
|
+
elsif role == 'kb'
|
|
180
184
|
knowledge_base_name, *databases = content_tokens(message)
|
|
181
185
|
databases = nil if databases.empty?
|
|
182
186
|
knowledge_base = KnowledgeBase.load knowledge_base_name
|
|
@@ -184,7 +188,7 @@ Below is the documentation of the workflow:
|
|
|
184
188
|
knowledge_base_definition = LLM.knowledge_base_tool_definition(knowledge_base, databases)
|
|
185
189
|
tool_definitions.merge!(knowledge_base_definition)
|
|
186
190
|
next
|
|
187
|
-
elsif
|
|
191
|
+
elsif role == 'clear_tools'
|
|
188
192
|
tool_definitions = {}
|
|
189
193
|
else
|
|
190
194
|
message
|
|
@@ -197,7 +201,8 @@ Below is the documentation of the workflow:
|
|
|
197
201
|
def self.associations(messages, kb = nil)
|
|
198
202
|
tool_definitions = {}
|
|
199
203
|
new = messages.collect do |message|
|
|
200
|
-
|
|
204
|
+
role = message[:role]
|
|
205
|
+
if role == 'association'
|
|
201
206
|
name, path, *options = content_tokens(message)
|
|
202
207
|
|
|
203
208
|
kb ||= KnowledgeBase.new Scout.var.Agent.Chat.knowledge_base
|
|
@@ -205,7 +210,7 @@ Below is the documentation of the workflow:
|
|
|
205
210
|
|
|
206
211
|
tool_definitions.merge!(LLM.knowledge_base_tool_definition( kb, [name]))
|
|
207
212
|
next
|
|
208
|
-
elsif
|
|
213
|
+
elsif role == 'clear_associations'
|
|
209
214
|
tool_definitions = {}
|
|
210
215
|
else
|
|
211
216
|
message
|
data/python/.gitignore
ADDED
data/python/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# scout-ai Python package
|
|
2
|
+
|
|
3
|
+
This directory contains the Python package for Scout-AI.
|
|
4
|
+
|
|
5
|
+
The package provides a lightweight Python interface to Scout-AI chats and agents while keeping Ruby as the source of truth for:
|
|
6
|
+
|
|
7
|
+
- chat parsing and printing
|
|
8
|
+
- LLM execution
|
|
9
|
+
- agent execution
|
|
10
|
+
- workflow and tool execution
|
|
11
|
+
|
|
12
|
+
In practice, the Python layer builds chat objects, serializes them through the Scout-AI CLI, and delegates execution back to Scout.
|
|
13
|
+
|
|
14
|
+
## Requirements
|
|
15
|
+
|
|
16
|
+
The Python package expects the `scout-ai` command to be available.
|
|
17
|
+
|
|
18
|
+
Usually this means you should already have the Ruby Scout-AI package installed and configured, together with the Scout stack it depends on.
|
|
19
|
+
|
|
20
|
+
If the command is not on your `PATH`, you can point the Python package to it with:
|
|
21
|
+
|
|
22
|
+
export SCOUT_AI_COMMAND=/full/path/to/scout-ai
|
|
23
|
+
|
|
24
|
+
## Install from GitHub with pip
|
|
25
|
+
|
|
26
|
+
Because this repository is primarily a Ruby project and the Python package lives under `python/`, install it with the `subdirectory` fragment:
|
|
27
|
+
|
|
28
|
+
pip install "scout-ai @ git+https://github.com/mikisvaz/scout-ai.git@main#subdirectory=python"
|
|
29
|
+
|
|
30
|
+
You can also omit the explicit package name:
|
|
31
|
+
|
|
32
|
+
pip install "git+https://github.com/mikisvaz/scout-ai.git@main#subdirectory=python"
|
|
33
|
+
|
|
34
|
+
For editable local development from a clone of the repository:
|
|
35
|
+
|
|
36
|
+
pip install -e python
|
|
37
|
+
|
|
38
|
+
## Optional extras
|
|
39
|
+
|
|
40
|
+
If you also want the machine-learning helpers, you can install optional extras:
|
|
41
|
+
|
|
42
|
+
pip install "scout-ai[ml] @ git+https://github.com/mikisvaz/scout-ai.git@main#subdirectory=python"
|
|
43
|
+
|
|
44
|
+
For Hugging Face / RLHF helpers:
|
|
45
|
+
|
|
46
|
+
pip install "scout-ai[huggingface] @ git+https://github.com/mikisvaz/scout-ai.git@main#subdirectory=python"
|
|
47
|
+
|
|
48
|
+
## Quick example
|
|
49
|
+
|
|
50
|
+
from scout_ai import load_agent
|
|
51
|
+
|
|
52
|
+
agent = load_agent("Planner", endpoint="nano")
|
|
53
|
+
agent.file("README.md")
|
|
54
|
+
agent.user("Summarize this repository")
|
|
55
|
+
|
|
56
|
+
message = agent.chat()
|
|
57
|
+
print(message.content)
|
|
58
|
+
|
|
59
|
+
## Chat example
|
|
60
|
+
|
|
61
|
+
from scout_ai import Chat
|
|
62
|
+
|
|
63
|
+
chat = Chat()
|
|
64
|
+
chat.system("You are concise")
|
|
65
|
+
chat.user("Say hello")
|
|
66
|
+
|
|
67
|
+
delta = chat.ask()
|
|
68
|
+
print(delta.to_json())
|
|
69
|
+
|
|
70
|
+
## What pip installs
|
|
71
|
+
|
|
72
|
+
The pip package installs only the Python interface contained in this directory.
|
|
73
|
+
It does not install the Ruby Scout-AI gem or the rest of the Scout stack.
|
|
74
|
+
|
|
75
|
+
That separation is intentional:
|
|
76
|
+
|
|
77
|
+
- Ruby remains responsible for the actual Scout-AI runtime
|
|
78
|
+
- Python provides an ergonomic interface to that runtime
|
|
79
|
+
|
|
80
|
+
## Package layout
|
|
81
|
+
|
|
82
|
+
- `scout_ai.chat.Chat` — chat builder and execution wrapper
|
|
83
|
+
- `scout_ai.agent.Agent` — thin wrapper over Scout agents with eager `current_chat`
|
|
84
|
+
- `scout_ai.runner.ScoutRunner` — CLI bridge used internally
|
|
85
|
+
- `scout_ai.message.Message` — message wrapper returned by `chat()`
|
|
86
|
+
|
|
87
|
+
## Running tests
|
|
88
|
+
|
|
89
|
+
From the repository root:
|
|
90
|
+
|
|
91
|
+
PYTHONPATH=python python -m unittest discover python/tests
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=69", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scout-ai"
|
|
7
|
+
version = "1.2.2"
|
|
8
|
+
description = "Python interface for Scout-AI chats, agents, and model helpers"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Miki S. Vazquez" }
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
24
|
+
"Topic :: Software Development :: Libraries :: Python Modules"
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
ml = [
|
|
30
|
+
"numpy",
|
|
31
|
+
"pandas",
|
|
32
|
+
"torch"
|
|
33
|
+
]
|
|
34
|
+
huggingface = [
|
|
35
|
+
"datasets",
|
|
36
|
+
"transformers",
|
|
37
|
+
"trl"
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/mikisvaz/scout-ai"
|
|
42
|
+
Repository = "https://github.com/mikisvaz/scout-ai"
|
|
43
|
+
Issues = "https://github.com/mikisvaz/scout-ai/issues"
|
|
44
|
+
|
|
45
|
+
[tool.setuptools.packages.find]
|
|
46
|
+
where = ["."]
|
|
47
|
+
include = ["scout_ai*"]
|
|
48
|
+
|
|
49
|
+
[tool.setuptools]
|
|
50
|
+
include-package-data = true
|
data/python/scout_ai/runner.py
CHANGED
|
@@ -4,9 +4,11 @@ import json
|
|
|
4
4
|
import os
|
|
5
5
|
import shlex
|
|
6
6
|
import subprocess
|
|
7
|
+
import sys
|
|
7
8
|
import tempfile
|
|
9
|
+
import threading
|
|
8
10
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
11
|
+
from typing import Iterable, List, Optional, Sequence, TextIO
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
class CommandError(RuntimeError):
|
|
@@ -27,7 +29,12 @@ class ScoutRunner:
|
|
|
27
29
|
commands.
|
|
28
30
|
"""
|
|
29
31
|
|
|
30
|
-
def __init__(
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
command: Optional[Sequence[str] | str] = None,
|
|
35
|
+
show_stderr: bool = True,
|
|
36
|
+
stderr: Optional[TextIO] = None,
|
|
37
|
+
):
|
|
31
38
|
if command is None:
|
|
32
39
|
command = os.environ.get("SCOUT_AI_COMMAND", "scout-ai")
|
|
33
40
|
|
|
@@ -39,15 +46,49 @@ class ScoutRunner:
|
|
|
39
46
|
if not self.command:
|
|
40
47
|
raise ValueError("command can not be empty")
|
|
41
48
|
|
|
42
|
-
|
|
49
|
+
self.show_stderr = show_stderr
|
|
50
|
+
self.stderr = stderr
|
|
51
|
+
|
|
52
|
+
def _run(self, *args: str, stream_stderr: bool = False) -> str:
|
|
43
53
|
cmd = self.command + [str(arg) for arg in args]
|
|
44
|
-
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
45
|
-
if proc.returncode != 0:
|
|
46
|
-
raise CommandError(cmd, proc.stdout, proc.stderr, proc.returncode)
|
|
47
|
-
return proc.stdout
|
|
48
54
|
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
proc = subprocess.Popen(
|
|
56
|
+
cmd,
|
|
57
|
+
stdout=subprocess.PIPE,
|
|
58
|
+
stderr=subprocess.PIPE,
|
|
59
|
+
text=True,
|
|
60
|
+
bufsize=1,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
stdout_chunks: List[str] = []
|
|
64
|
+
should_stream = stream_stderr and self.show_stderr
|
|
65
|
+
stderr_target = self.stderr if self.stderr is not None else sys.stderr
|
|
66
|
+
|
|
67
|
+
def forward_stderr() -> None:
|
|
68
|
+
assert proc.stderr is not None
|
|
69
|
+
while True:
|
|
70
|
+
chunk = proc.stderr.readline()
|
|
71
|
+
if chunk == "":
|
|
72
|
+
break
|
|
73
|
+
if should_stream:
|
|
74
|
+
stderr_target.write(chunk)
|
|
75
|
+
stderr_target.flush()
|
|
76
|
+
|
|
77
|
+
stderr_thread = threading.Thread(target=forward_stderr, daemon=True)
|
|
78
|
+
stderr_thread.start()
|
|
79
|
+
|
|
80
|
+
assert proc.stdout is not None
|
|
81
|
+
stdout_chunks.append(proc.stdout.read())
|
|
82
|
+
proc.stdout.close()
|
|
83
|
+
|
|
84
|
+
stderr_thread.join()
|
|
85
|
+
returncode = proc.wait()
|
|
86
|
+
|
|
87
|
+
stdout = "".join(stdout_chunks)
|
|
88
|
+
|
|
89
|
+
if returncode != 0:
|
|
90
|
+
raise CommandError(cmd, stdout, "", returncode)
|
|
91
|
+
return stdout
|
|
51
92
|
|
|
52
93
|
def json_to_chat_file(self, messages: Iterable[dict], chat_file: Path) -> Path:
|
|
53
94
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False, encoding="utf-8") as handle:
|
|
@@ -115,9 +156,9 @@ class ScoutRunner:
|
|
|
115
156
|
try:
|
|
116
157
|
self.json_to_chat_file(messages, chat_file)
|
|
117
158
|
if agent_name:
|
|
118
|
-
self._run("agent", "ask", str(agent_name), "--chat", str(chat_file))
|
|
159
|
+
self._run("agent", "ask", str(agent_name), "--chat", str(chat_file), stream_stderr=True)
|
|
119
160
|
else:
|
|
120
|
-
self._run("llm", "ask", "--chat", str(chat_file))
|
|
161
|
+
self._run("llm", "ask", "--chat", str(chat_file), stream_stderr=True)
|
|
121
162
|
return self.chat_file_to_messages(chat_file)
|
|
122
163
|
finally:
|
|
123
164
|
chat_file.unlink(missing_ok=True)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import sys
|
|
3
|
+
import unittest
|
|
4
|
+
from contextlib import redirect_stderr
|
|
5
|
+
|
|
6
|
+
from scout_ai.runner import ScoutRunner
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RunnerStreamingTest(unittest.TestCase):
|
|
10
|
+
def test_run_streams_stderr_when_requested(self):
|
|
11
|
+
code = (
|
|
12
|
+
"import sys; "
|
|
13
|
+
"sys.stderr.write('waiting...\\n'); sys.stderr.flush(); "
|
|
14
|
+
"print('done')"
|
|
15
|
+
)
|
|
16
|
+
runner = ScoutRunner(command=[sys.executable, "-c", code])
|
|
17
|
+
|
|
18
|
+
stderr = io.StringIO()
|
|
19
|
+
with redirect_stderr(stderr):
|
|
20
|
+
output = runner._run("ignored", stream_stderr=True)
|
|
21
|
+
|
|
22
|
+
self.assertEqual(output.strip(), "done")
|
|
23
|
+
self.assertIn("waiting...", stderr.getvalue())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
unittest.main()
|
data/scout-ai.gemspec
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
|
3
3
|
# Instead, edit Juwelier::Tasks in Rakefile, and run 'rake gemspec'
|
|
4
4
|
# -*- encoding: utf-8 -*-
|
|
5
|
-
# stub: scout-ai 1.2.
|
|
5
|
+
# stub: scout-ai 1.2.3 ruby lib
|
|
6
6
|
|
|
7
7
|
Gem::Specification.new do |s|
|
|
8
8
|
s.name = "scout-ai".freeze
|
|
9
|
-
s.version = "1.2.
|
|
9
|
+
s.version = "1.2.3".freeze
|
|
10
10
|
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
|
|
12
12
|
s.require_paths = ["lib".freeze]
|
|
@@ -85,6 +85,9 @@ Gem::Specification.new do |s|
|
|
|
85
85
|
"lib/scout/network/entity.rb",
|
|
86
86
|
"lib/scout/network/knowledge_base.rb",
|
|
87
87
|
"lib/scout/network/paths.rb",
|
|
88
|
+
"python/.gitignore",
|
|
89
|
+
"python/README.md",
|
|
90
|
+
"python/pyproject.toml",
|
|
88
91
|
"python/scout_ai/__init__.py",
|
|
89
92
|
"python/scout_ai/agent.py",
|
|
90
93
|
"python/scout_ai/chat.py",
|
|
@@ -99,6 +102,7 @@ Gem::Specification.new do |s|
|
|
|
99
102
|
"python/scout_ai/runner.py",
|
|
100
103
|
"python/scout_ai/util.py",
|
|
101
104
|
"python/tests/test_chat_agent.py",
|
|
105
|
+
"python/tests/test_runner.py",
|
|
102
106
|
"scout-ai.gemspec",
|
|
103
107
|
"scout_commands/agent/ask",
|
|
104
108
|
"scout_commands/agent/find",
|
|
@@ -156,7 +160,7 @@ Gem::Specification.new do |s|
|
|
|
156
160
|
]
|
|
157
161
|
s.homepage = "http://github.com/mikisvaz/scout-ai".freeze
|
|
158
162
|
s.licenses = ["MIT".freeze]
|
|
159
|
-
s.rubygems_version = "3.7.
|
|
163
|
+
s.rubygems_version = "3.7.2".freeze
|
|
160
164
|
s.summary = "AI gear for scouts".freeze
|
|
161
165
|
|
|
162
166
|
s.specification_version = 4
|
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.2.
|
|
4
|
+
version: 1.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Miguel Vazquez
|
|
@@ -153,6 +153,9 @@ files:
|
|
|
153
153
|
- lib/scout/network/entity.rb
|
|
154
154
|
- lib/scout/network/knowledge_base.rb
|
|
155
155
|
- lib/scout/network/paths.rb
|
|
156
|
+
- python/.gitignore
|
|
157
|
+
- python/README.md
|
|
158
|
+
- python/pyproject.toml
|
|
156
159
|
- python/scout_ai/__init__.py
|
|
157
160
|
- python/scout_ai/agent.py
|
|
158
161
|
- python/scout_ai/chat.py
|
|
@@ -167,6 +170,7 @@ files:
|
|
|
167
170
|
- python/scout_ai/runner.py
|
|
168
171
|
- python/scout_ai/util.py
|
|
169
172
|
- python/tests/test_chat_agent.py
|
|
173
|
+
- python/tests/test_runner.py
|
|
170
174
|
- scout-ai.gemspec
|
|
171
175
|
- scout_commands/agent/ask
|
|
172
176
|
- scout_commands/agent/find
|
|
@@ -239,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
239
243
|
- !ruby/object:Gem::Version
|
|
240
244
|
version: '0'
|
|
241
245
|
requirements: []
|
|
242
|
-
rubygems_version: 3.7.
|
|
246
|
+
rubygems_version: 3.7.2
|
|
243
247
|
specification_version: 4
|
|
244
248
|
summary: AI gear for scouts
|
|
245
249
|
test_files: []
|