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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 648a917e45f537ed44b068e1e4762a8a2846dbef5280cb9c37a0c2351e9e95b1
4
- data.tar.gz: 25bf5fdbac1f713e6746a692f4aecefa594b69d6df17b526e4658182529a0b3a
3
+ metadata.gz: 8549d8d309d5c6fa5b4b259db26ac7001cafa2fd10b99fe552e6f733a8a8d72e
4
+ data.tar.gz: 52f728fa8fd19b5c6bc6a4f2ec8245241345946ffe682a8d54ba89dd4a61ca14
5
5
  SHA512:
6
- metadata.gz: 6a46450a6d7e0b49b66cb2c0fe94efaa4ba8d4e030a607f8fd2da8a610af0143ae3272f04a470019e72d6b62e6510c7c291a75f1b70871ae3befeed87138237a
7
- data.tar.gz: 2fe91febb6d59bca6fa3b942a4bd1fbade3f6001df35be85497e0020979de9c7642185bc8bb221a2f0edbe2872c41fbc24851805f47801425125d320a575e2c8
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.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
- if message[:role] == 'mcp'
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 message[:role] == 'tool'
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 message[:role] == 'introduce'
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 message[:role] == 'kb'
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 message[:role] == 'clear_tools'
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
- if message[:role] == 'association'
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 message[:role] == 'clear_associations'
213
+ elsif role == 'clear_associations'
209
214
  tool_definitions = {}
210
215
  else
211
216
  message
data/python/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.pyd
5
+ *.egg-info/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
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
@@ -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 Any, Iterable, List, Optional, Sequence
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__(self, command: Optional[Sequence[str] | str] = None):
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
- def _run(self, *args: str) -> str:
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
- def _write_json(self, path: Path, messages: Iterable[dict]) -> None:
50
- path.write_text(json.dumps(list(messages), ensure_ascii=False, indent=2), encoding="utf-8")
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.2 ruby lib
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.2".freeze
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.0.dev".freeze
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.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.0.dev
246
+ rubygems_version: 3.7.2
243
247
  specification_version: 4
244
248
  summary: AI gear for scouts
245
249
  test_files: []