ironshell 1.5.1__py3-none-any.whl
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.
- ironshell/__init__.py +682 -0
- ironshell/cloud.py +967 -0
- ironshell/distributed.py +691 -0
- ironshell/langchain.py +1074 -0
- ironshell/metrics.py +603 -0
- ironshell/py.typed +0 -0
- ironshell/workflows.py +539 -0
- ironshell-1.5.1.dist-info/METADATA +349 -0
- ironshell-1.5.1.dist-info/RECORD +11 -0
- ironshell-1.5.1.dist-info/WHEEL +5 -0
- ironshell-1.5.1.dist-info/top_level.txt +1 -0
ironshell/__init__.py
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IronShell Python SDK
|
|
3
|
+
|
|
4
|
+
AI-powered typed shell with agent orchestration capabilities.
|
|
5
|
+
|
|
6
|
+
Modules:
|
|
7
|
+
- workflows: Workflow patterns (MapReduce, Saga, Pipeline, Fan-Out)
|
|
8
|
+
- metrics: Observability (Prometheus metrics, tracing, health checks)
|
|
9
|
+
- distributed: Distributed agents (service registry, leader election, routing)
|
|
10
|
+
- langchain: LangChain integration tools
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from importlib.metadata import PackageNotFoundError
|
|
18
|
+
from importlib.metadata import version as _distribution_version
|
|
19
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import os
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _read_version() -> str:
|
|
28
|
+
"""The SDK version, from the installed distribution or the source tree.
|
|
29
|
+
|
|
30
|
+
pyproject.toml is the one source. An installed distribution reports it
|
|
31
|
+
through importlib.metadata; a bare source checkout reads it from the
|
|
32
|
+
pyproject.toml two levels above this package.
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
return _distribution_version("ironshell")
|
|
36
|
+
except PackageNotFoundError:
|
|
37
|
+
pass
|
|
38
|
+
pyproject = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, "pyproject.toml")
|
|
39
|
+
try:
|
|
40
|
+
with open(pyproject, encoding="utf-8") as f:
|
|
41
|
+
match = re.search(r'^version\s*=\s*"([^"]+)"', f.read(), re.MULTILINE)
|
|
42
|
+
except OSError:
|
|
43
|
+
match = None
|
|
44
|
+
return match.group(1) if match else "0+unknown"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
__version__ = _read_version()
|
|
48
|
+
__all__ = [
|
|
49
|
+
"IronShellRuntime",
|
|
50
|
+
"Agent",
|
|
51
|
+
"AgentConfig",
|
|
52
|
+
"AgentResult",
|
|
53
|
+
"Swarm",
|
|
54
|
+
"SwarmConfig",
|
|
55
|
+
"SwarmResult",
|
|
56
|
+
"A2UIEvent",
|
|
57
|
+
"NotificationLevel",
|
|
58
|
+
"PipelineBuilder",
|
|
59
|
+
"evaluate",
|
|
60
|
+
"pipeline",
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class NotificationLevel(Enum):
|
|
65
|
+
"""A2UI notification levels"""
|
|
66
|
+
INFO = "info"
|
|
67
|
+
SUCCESS = "success"
|
|
68
|
+
WARNING = "warning"
|
|
69
|
+
ERROR = "error"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class A2UIEvent:
|
|
74
|
+
"""Event from A2UI protocol"""
|
|
75
|
+
id: str
|
|
76
|
+
timestamp: str
|
|
77
|
+
priority: str
|
|
78
|
+
event_type: str
|
|
79
|
+
data: Dict[str, Any]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass
|
|
83
|
+
class AgentConfig:
|
|
84
|
+
"""Configuration for an agent"""
|
|
85
|
+
name: str
|
|
86
|
+
model: str = "openai:gpt-4o-mini"
|
|
87
|
+
tools: List[str] = field(default_factory=list)
|
|
88
|
+
max_steps: int = 10
|
|
89
|
+
dry_run: bool = False
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass
|
|
93
|
+
class AgentResult:
|
|
94
|
+
"""Result from agent execution"""
|
|
95
|
+
success: bool
|
|
96
|
+
result: Any
|
|
97
|
+
trace: List[Dict[str, Any]]
|
|
98
|
+
steps_taken: int
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class SwarmConfig:
|
|
103
|
+
"""Configuration for a swarm"""
|
|
104
|
+
agents: List[Agent]
|
|
105
|
+
policy: str = "round_robin"
|
|
106
|
+
max_iterations: int = 10
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class SwarmResult:
|
|
111
|
+
"""Result from swarm execution"""
|
|
112
|
+
success: bool
|
|
113
|
+
result: Any
|
|
114
|
+
blackboard: Dict[str, Any]
|
|
115
|
+
iterations: int
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class Agent:
|
|
119
|
+
"""AI Agent with tool execution capabilities"""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
name: str,
|
|
124
|
+
model: str = "openai:gpt-4o-mini",
|
|
125
|
+
tools: Optional[List[str]] = None,
|
|
126
|
+
max_steps: int = 10,
|
|
127
|
+
runtime: Optional[IronShellRuntime] = None,
|
|
128
|
+
system_prompt: Optional[str] = None,
|
|
129
|
+
):
|
|
130
|
+
if system_prompt is not None:
|
|
131
|
+
# The agent builtin's config record has no system prompt key (its
|
|
132
|
+
# keys are goal, tools, max_steps, dry_run), and nothing else in the
|
|
133
|
+
# shell takes one, so the prompt could never reach the model.
|
|
134
|
+
raise ValueError(
|
|
135
|
+
"Agent system_prompt is not supported: the agent builtin has no "
|
|
136
|
+
"system prompt option, so the prompt cannot reach the model; "
|
|
137
|
+
"remove the argument and put the instructions in the goal"
|
|
138
|
+
)
|
|
139
|
+
self.name = name
|
|
140
|
+
self.model = model
|
|
141
|
+
self.tools = tools or []
|
|
142
|
+
self.max_steps = max_steps
|
|
143
|
+
self._runtime = runtime
|
|
144
|
+
self.trace: List[Dict[str, Any]] = []
|
|
145
|
+
|
|
146
|
+
async def run(self, goal: str) -> AgentResult:
|
|
147
|
+
"""Execute the agent with a goal.
|
|
148
|
+
|
|
149
|
+
Raises ValueError, before anything runs, for a model other than the
|
|
150
|
+
default or a max_steps outside 1..=1000 (see `_build_agent_code`).
|
|
151
|
+
"""
|
|
152
|
+
# Build the call first, so an unsupported option is refused without
|
|
153
|
+
# locating or starting the ish binary.
|
|
154
|
+
code = _build_agent_code(goal, self.model, self.tools, self.max_steps)
|
|
155
|
+
|
|
156
|
+
if self._runtime is None:
|
|
157
|
+
self._runtime = IronShellRuntime()
|
|
158
|
+
|
|
159
|
+
result = self._runtime.eval(code)
|
|
160
|
+
|
|
161
|
+
if isinstance(result, dict):
|
|
162
|
+
return AgentResult(
|
|
163
|
+
success=result.get("success", False),
|
|
164
|
+
result=result.get("result"),
|
|
165
|
+
trace=result.get("trace", []),
|
|
166
|
+
steps_taken=result.get("steps", 0),
|
|
167
|
+
)
|
|
168
|
+
# The builtin returns its final answer as a String and reports no
|
|
169
|
+
# trace or step count. A returned value means the run did not fail.
|
|
170
|
+
return AgentResult(success=True, result=result, trace=[], steps_taken=0)
|
|
171
|
+
|
|
172
|
+
def __repr__(self) -> str:
|
|
173
|
+
return f"Agent(name={self.name!r}, model={self.model!r}, tools={self.tools})"
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class Swarm:
|
|
177
|
+
"""Multi-agent swarm with coordination"""
|
|
178
|
+
|
|
179
|
+
def __init__(
|
|
180
|
+
self,
|
|
181
|
+
agents: List[Agent],
|
|
182
|
+
policy: str = "round_robin",
|
|
183
|
+
max_iterations: int = 10,
|
|
184
|
+
runtime: Optional[IronShellRuntime] = None,
|
|
185
|
+
):
|
|
186
|
+
self.agents = agents
|
|
187
|
+
self.policy = policy
|
|
188
|
+
self.max_iterations = max_iterations
|
|
189
|
+
self._runtime = runtime
|
|
190
|
+
|
|
191
|
+
async def run(self, goal: str) -> SwarmResult:
|
|
192
|
+
"""Execute the swarm with a goal.
|
|
193
|
+
|
|
194
|
+
Raises ValueError, before anything runs, for any option the `swarm`
|
|
195
|
+
builtin cannot honour (see `_build_swarm_code`).
|
|
196
|
+
"""
|
|
197
|
+
# Build the call first, so an unsupported option is refused without
|
|
198
|
+
# locating or starting the ish binary.
|
|
199
|
+
code = _build_swarm_code(self.agents, goal, self.policy, self.max_iterations)
|
|
200
|
+
|
|
201
|
+
if self._runtime is None:
|
|
202
|
+
self._runtime = IronShellRuntime()
|
|
203
|
+
|
|
204
|
+
result = self._runtime.eval(code)
|
|
205
|
+
|
|
206
|
+
if isinstance(result, dict):
|
|
207
|
+
return SwarmResult(
|
|
208
|
+
success=result.get("success", False),
|
|
209
|
+
result=result.get("result"),
|
|
210
|
+
blackboard=result.get("blackboard", {}),
|
|
211
|
+
iterations=result.get("iterations", 0),
|
|
212
|
+
)
|
|
213
|
+
# The builtin returns the combined answer as a String and reports no
|
|
214
|
+
# blackboard or iteration count. A returned value means the run did
|
|
215
|
+
# not fail, so success is True and the other fields are empty.
|
|
216
|
+
return SwarmResult(success=True, result=result, blackboard={}, iterations=0)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# Defaults that the SDK's Agent and Swarm classes start from. The `swarm`
|
|
220
|
+
# builtin has no model, policy or per-agent step option, so a non-default value
|
|
221
|
+
# for any of them is refused rather than silently dropped.
|
|
222
|
+
_DEFAULT_AGENT_MODEL = "openai:gpt-4o-mini"
|
|
223
|
+
_DEFAULT_AGENT_MAX_STEPS = 10
|
|
224
|
+
_DEFAULT_SWARM_POLICY = "round_robin"
|
|
225
|
+
# The builtin's upper bound on max_steps (MAX_AGENT_STEPS in src/builtins.rs).
|
|
226
|
+
_SWARM_MAX_STEPS = 1000
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _ironshell_string(value: str) -> str:
|
|
230
|
+
"""Render a Python str as an IronShell double-quoted string literal.
|
|
231
|
+
|
|
232
|
+
The lexer unescapes backslash, double quote, n, r and t, so those are
|
|
233
|
+
escaped here. Any other backslash sequence is not decoded (a backslash
|
|
234
|
+
followed by u is read as a plain u), so no unicode escape is ever emitted:
|
|
235
|
+
non-ASCII text is written as itself, which the lexer reads as characters.
|
|
236
|
+
|
|
237
|
+
Three inputs are refused rather than emitted wrong: "${" (the lexer has no
|
|
238
|
+
escape for it, and the evaluator interpolates ${...} in any string literal),
|
|
239
|
+
a control character other than tab, newline or carriage return (the lexer
|
|
240
|
+
has no escape for it, and NUL cannot be passed in a command line), and a
|
|
241
|
+
lone surrogate (it has no UTF-8 encoding).
|
|
242
|
+
"""
|
|
243
|
+
if not isinstance(value, str):
|
|
244
|
+
raise TypeError(f"expected a str for an IronShell string, got {type(value).__name__}")
|
|
245
|
+
if "${" in value:
|
|
246
|
+
raise ValueError(
|
|
247
|
+
"text containing '${' cannot be passed to IronShell: string literals "
|
|
248
|
+
"interpolate ${...}, and the language has no escape for it"
|
|
249
|
+
)
|
|
250
|
+
for ch in value:
|
|
251
|
+
code = ord(ch)
|
|
252
|
+
if code < 0x20 and ch not in "\t\n\r":
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"control character U+{code:04X} cannot be passed to IronShell: "
|
|
255
|
+
"the lexer has no escape for it"
|
|
256
|
+
)
|
|
257
|
+
if 0xD800 <= code <= 0xDFFF:
|
|
258
|
+
raise ValueError(
|
|
259
|
+
f"lone surrogate U+{code:04X} cannot be passed to IronShell: "
|
|
260
|
+
"it has no UTF-8 encoding"
|
|
261
|
+
)
|
|
262
|
+
escaped = (
|
|
263
|
+
value.replace("\\", "\\\\")
|
|
264
|
+
.replace('"', '\\"')
|
|
265
|
+
.replace("\n", "\\n")
|
|
266
|
+
.replace("\r", "\\r")
|
|
267
|
+
.replace("\t", "\\t")
|
|
268
|
+
)
|
|
269
|
+
return f'"{escaped}"'
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
_IDENTIFIER = re.compile(r"[A-Za-z_][A-Za-z0-9_]*\Z")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _ironshell_value(value: Any) -> str:
|
|
276
|
+
"""Render a Python value as an IronShell literal.
|
|
277
|
+
|
|
278
|
+
Strings go through _ironshell_string. Lists become arrays, and dicts become
|
|
279
|
+
records whose keys must be identifiers, since the parser reads record keys
|
|
280
|
+
as bare names. None, True and False become null, true and false.
|
|
281
|
+
"""
|
|
282
|
+
if value is None:
|
|
283
|
+
return "null"
|
|
284
|
+
if isinstance(value, bool):
|
|
285
|
+
return "true" if value else "false"
|
|
286
|
+
if isinstance(value, str):
|
|
287
|
+
return _ironshell_string(value)
|
|
288
|
+
if isinstance(value, int):
|
|
289
|
+
return str(value)
|
|
290
|
+
if isinstance(value, float):
|
|
291
|
+
if value != value or value in (float("inf"), float("-inf")):
|
|
292
|
+
raise ValueError(f"{value!r} has no IronShell literal")
|
|
293
|
+
return repr(value)
|
|
294
|
+
if isinstance(value, (list, tuple)):
|
|
295
|
+
return "[" + ", ".join(_ironshell_value(v) for v in value) + "]"
|
|
296
|
+
if isinstance(value, dict):
|
|
297
|
+
fields = []
|
|
298
|
+
for key, item in value.items():
|
|
299
|
+
if not isinstance(key, str) or not _IDENTIFIER.match(key):
|
|
300
|
+
raise ValueError(
|
|
301
|
+
f"record key {key!r} is not an identifier; IronShell record keys are bare names"
|
|
302
|
+
)
|
|
303
|
+
fields.append(f"{key}: {_ironshell_value(item)}")
|
|
304
|
+
return "{" + ", ".join(fields) + "}"
|
|
305
|
+
raise TypeError(f"{type(value).__name__} has no IronShell literal")
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _check_steps(value: int, owner: str) -> int:
|
|
309
|
+
"""Return value if it is a legal step count for the builtin, else raise ValueError."""
|
|
310
|
+
if not isinstance(value, int) or isinstance(value, bool) or not 1 <= value <= _SWARM_MAX_STEPS:
|
|
311
|
+
raise ValueError(
|
|
312
|
+
f"{owner} must be an int in 1..={_SWARM_MAX_STEPS} "
|
|
313
|
+
f"(the builtin's max_steps), got {value!r}"
|
|
314
|
+
)
|
|
315
|
+
return value
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _build_agent_code(goal: str, model: str, tools: List[str], max_steps: int) -> str:
|
|
319
|
+
"""Build the IronShell call that runs one agent.
|
|
320
|
+
|
|
321
|
+
Produces the record form of the builtin, `agent({goal, tools, max_steps})`.
|
|
322
|
+
Raises ValueError for a model other than the default, since the builtin has
|
|
323
|
+
no model option, and for a max_steps outside 1..=1000.
|
|
324
|
+
"""
|
|
325
|
+
if model != _DEFAULT_AGENT_MODEL:
|
|
326
|
+
raise ValueError(
|
|
327
|
+
f"Agent model {model!r} is not supported: the agent builtin has no model "
|
|
328
|
+
f"option; leave the model at {_DEFAULT_AGENT_MODEL!r}"
|
|
329
|
+
)
|
|
330
|
+
steps = _check_steps(max_steps, "max_steps")
|
|
331
|
+
tools_str = ", ".join(_ironshell_string(t) for t in tools)
|
|
332
|
+
return (
|
|
333
|
+
f"agent({{goal: {_ironshell_string(goal)}, tools: [{tools_str}], "
|
|
334
|
+
f"max_steps: {steps}}})"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _build_swarm_code(
|
|
339
|
+
agents: List[Agent],
|
|
340
|
+
goal: str,
|
|
341
|
+
policy: str,
|
|
342
|
+
max_iterations: int,
|
|
343
|
+
) -> str:
|
|
344
|
+
"""Build the IronShell call that runs a swarm.
|
|
345
|
+
|
|
346
|
+
Produces the record form of the builtin, `swarm({goal, tools, max_steps})`.
|
|
347
|
+
`tools` is the union of the agents' tools, in first-seen order. Raises
|
|
348
|
+
ValueError for an option the builtin cannot honour: a policy other than
|
|
349
|
+
round_robin, a per-agent model or max_steps other than the SDK defaults,
|
|
350
|
+
or a max_iterations outside 1..=1000.
|
|
351
|
+
"""
|
|
352
|
+
if policy != _DEFAULT_SWARM_POLICY:
|
|
353
|
+
raise ValueError(
|
|
354
|
+
f"Swarm policy {policy!r} is not supported: the swarm builtin has no "
|
|
355
|
+
f"policy option and always runs {_DEFAULT_SWARM_POLICY!r}"
|
|
356
|
+
)
|
|
357
|
+
if (
|
|
358
|
+
not isinstance(max_iterations, int)
|
|
359
|
+
or isinstance(max_iterations, bool)
|
|
360
|
+
or not 1 <= max_iterations <= _SWARM_MAX_STEPS
|
|
361
|
+
):
|
|
362
|
+
raise ValueError(
|
|
363
|
+
f"max_iterations must be an int in 1..={_SWARM_MAX_STEPS} "
|
|
364
|
+
f"(the swarm builtin's max_steps), got {max_iterations!r}"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
tools: List[str] = []
|
|
368
|
+
for agent in agents:
|
|
369
|
+
if agent.model != _DEFAULT_AGENT_MODEL:
|
|
370
|
+
raise ValueError(
|
|
371
|
+
f"Agent {agent.name!r} sets model {agent.model!r}, but the swarm builtin "
|
|
372
|
+
"has no per-agent model option; leave the model at its default"
|
|
373
|
+
)
|
|
374
|
+
if agent.max_steps != _DEFAULT_AGENT_MAX_STEPS:
|
|
375
|
+
raise ValueError(
|
|
376
|
+
f"Agent {agent.name!r} sets max_steps={agent.max_steps!r}, but the swarm "
|
|
377
|
+
"builtin has no per-agent step limit; use max_iterations on the swarm"
|
|
378
|
+
)
|
|
379
|
+
for tool in agent.tools:
|
|
380
|
+
if tool not in tools:
|
|
381
|
+
tools.append(tool)
|
|
382
|
+
|
|
383
|
+
tools_str = ", ".join(_ironshell_string(t) for t in tools)
|
|
384
|
+
return (
|
|
385
|
+
f"swarm({{goal: {_ironshell_string(goal)}, tools: [{tools_str}], "
|
|
386
|
+
f"max_steps: {max_iterations}}})"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
class IronShellRuntime:
|
|
391
|
+
"""Main runtime for IronShell execution"""
|
|
392
|
+
|
|
393
|
+
def __init__(self, ish_path: Optional[str] = None):
|
|
394
|
+
"""
|
|
395
|
+
Initialize the IronShell runtime.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
ish_path: Path to the ish binary. If None, searches PATH.
|
|
399
|
+
"""
|
|
400
|
+
self._ish_path = ish_path or self._find_ish()
|
|
401
|
+
self._event_listeners: List[Callable[[A2UIEvent], None]] = []
|
|
402
|
+
|
|
403
|
+
def _find_ish(self) -> str:
|
|
404
|
+
"""Find the ish binary in PATH or common locations"""
|
|
405
|
+
# Check PATH
|
|
406
|
+
for path in os.environ.get("PATH", "").split(os.pathsep):
|
|
407
|
+
ae = os.path.join(path, "ish.exe" if sys.platform == "win32" else "ish")
|
|
408
|
+
if os.path.isfile(ae):
|
|
409
|
+
return ae
|
|
410
|
+
|
|
411
|
+
# Check common locations
|
|
412
|
+
common_paths = [
|
|
413
|
+
"/usr/local/bin/ae",
|
|
414
|
+
"/usr/bin/ae",
|
|
415
|
+
os.path.expanduser("~/.cargo/bin/ae"),
|
|
416
|
+
os.path.expanduser("~/bin/ae"),
|
|
417
|
+
]
|
|
418
|
+
if sys.platform == "win32":
|
|
419
|
+
common_paths.extend([
|
|
420
|
+
r"C:\Program Files\IronShell\ish.exe",
|
|
421
|
+
os.path.expanduser(r"~\.cargo\bin\ish.exe"),
|
|
422
|
+
])
|
|
423
|
+
|
|
424
|
+
for path in common_paths:
|
|
425
|
+
if os.path.isfile(path):
|
|
426
|
+
return path
|
|
427
|
+
|
|
428
|
+
raise RuntimeError(
|
|
429
|
+
"Could not find 'ish' binary. Please install IronShell or "
|
|
430
|
+
"provide the path explicitly via ish_path parameter."
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
def eval(self, code: str) -> Any:
|
|
434
|
+
"""
|
|
435
|
+
Evaluate IronShell code and return the result.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
code: IronShell code to evaluate
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Parsed result (Python object)
|
|
442
|
+
"""
|
|
443
|
+
# `-c/--command`, not `-e`, and `--deterministic` for canonical JSON.
|
|
444
|
+
# The previous flags (`-e ... --json`) are not accepted by the binary:
|
|
445
|
+
# clap rejected `-e` outright, so every call raised RuntimeError. The
|
|
446
|
+
# SDK's core entry point could never have worked against a released
|
|
447
|
+
# build.
|
|
448
|
+
result = subprocess.run(
|
|
449
|
+
[self._ish_path, "-c", code, "--deterministic"],
|
|
450
|
+
capture_output=True,
|
|
451
|
+
text=True,
|
|
452
|
+
timeout=60,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
if result.returncode != 0:
|
|
456
|
+
raise RuntimeError(f"IronShell error: {result.stderr}")
|
|
457
|
+
|
|
458
|
+
try:
|
|
459
|
+
return json.loads(result.stdout)
|
|
460
|
+
except json.JSONDecodeError:
|
|
461
|
+
return result.stdout.strip()
|
|
462
|
+
|
|
463
|
+
def eval_file(self, path: str) -> Any:
|
|
464
|
+
"""
|
|
465
|
+
Evaluate an IronShell file.
|
|
466
|
+
|
|
467
|
+
Args:
|
|
468
|
+
path: Path to .ish file
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
Result of evaluation
|
|
472
|
+
"""
|
|
473
|
+
result = subprocess.run(
|
|
474
|
+
[self._ish_path, path, "--deterministic"],
|
|
475
|
+
capture_output=True,
|
|
476
|
+
text=True,
|
|
477
|
+
timeout=300,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
if result.returncode != 0:
|
|
481
|
+
raise RuntimeError(f"IronShell error: {result.stderr}")
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
return json.loads(result.stdout)
|
|
485
|
+
except json.JSONDecodeError:
|
|
486
|
+
return result.stdout.strip()
|
|
487
|
+
|
|
488
|
+
def create_agent(
|
|
489
|
+
self,
|
|
490
|
+
name: str,
|
|
491
|
+
model: str = "openai:gpt-4o-mini",
|
|
492
|
+
tools: Optional[List[str]] = None,
|
|
493
|
+
max_steps: int = 10,
|
|
494
|
+
) -> Agent:
|
|
495
|
+
"""
|
|
496
|
+
Create an AI agent.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
name: Agent identifier
|
|
500
|
+
model: Must be the default "openai:gpt-4o-mini". The agent builtin
|
|
501
|
+
has no model option, so any other value raises ValueError when
|
|
502
|
+
the agent runs.
|
|
503
|
+
tools: List of tool names the agent can use
|
|
504
|
+
max_steps: Maximum reasoning steps
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Agent instance
|
|
508
|
+
"""
|
|
509
|
+
return Agent(
|
|
510
|
+
name=name,
|
|
511
|
+
model=model,
|
|
512
|
+
tools=tools or [],
|
|
513
|
+
max_steps=max_steps,
|
|
514
|
+
runtime=self,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
def create_swarm(
|
|
518
|
+
self,
|
|
519
|
+
agents: List[Agent],
|
|
520
|
+
policy: str = "round_robin",
|
|
521
|
+
max_iterations: int = 10,
|
|
522
|
+
) -> Swarm:
|
|
523
|
+
"""
|
|
524
|
+
Create a multi-agent swarm.
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
agents: List of agents in the swarm
|
|
528
|
+
policy: Must be "round_robin". The swarm builtin has no policy
|
|
529
|
+
option, so any other value raises ValueError when the swarm runs.
|
|
530
|
+
max_iterations: Maximum coordination iterations, 1 to 1000
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
Swarm instance
|
|
534
|
+
"""
|
|
535
|
+
return Swarm(
|
|
536
|
+
agents=agents,
|
|
537
|
+
policy=policy,
|
|
538
|
+
max_iterations=max_iterations,
|
|
539
|
+
runtime=self,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
async def run_swarm(
|
|
543
|
+
self,
|
|
544
|
+
agents: List[Agent],
|
|
545
|
+
goal: str,
|
|
546
|
+
policy: str = "round_robin",
|
|
547
|
+
max_iterations: int = 10,
|
|
548
|
+
) -> SwarmResult:
|
|
549
|
+
"""
|
|
550
|
+
Run a multi-agent swarm with a goal.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
agents: List of agents
|
|
554
|
+
goal: Goal for the swarm
|
|
555
|
+
policy: Must be "round_robin" (see create_swarm)
|
|
556
|
+
max_iterations: Maximum iterations, 1 to 1000
|
|
557
|
+
|
|
558
|
+
Returns:
|
|
559
|
+
SwarmResult
|
|
560
|
+
"""
|
|
561
|
+
swarm = self.create_swarm(agents, policy, max_iterations)
|
|
562
|
+
return await swarm.run(goal)
|
|
563
|
+
|
|
564
|
+
def subscribe_a2ui(
|
|
565
|
+
self,
|
|
566
|
+
callback: Callable[[A2UIEvent], None],
|
|
567
|
+
) -> Callable[[], None]:
|
|
568
|
+
"""
|
|
569
|
+
Subscribe to A2UI events.
|
|
570
|
+
|
|
571
|
+
Args:
|
|
572
|
+
callback: Function called for each event
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
Unsubscribe function
|
|
576
|
+
"""
|
|
577
|
+
self._event_listeners.append(callback)
|
|
578
|
+
|
|
579
|
+
def unsubscribe():
|
|
580
|
+
self._event_listeners.remove(callback)
|
|
581
|
+
|
|
582
|
+
return unsubscribe
|
|
583
|
+
|
|
584
|
+
def _emit_event(self, event: A2UIEvent) -> None:
|
|
585
|
+
"""Emit an event to all listeners"""
|
|
586
|
+
for listener in self._event_listeners:
|
|
587
|
+
try:
|
|
588
|
+
listener(event)
|
|
589
|
+
except Exception as e:
|
|
590
|
+
print(f"A2UI event listener error: {e}", file=sys.stderr)
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def evaluate(code: str) -> Any:
|
|
594
|
+
"""
|
|
595
|
+
Convenience function to evaluate IronShell code.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
code: IronShell code to evaluate
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
Result
|
|
602
|
+
"""
|
|
603
|
+
runtime = IronShellRuntime()
|
|
604
|
+
return runtime.eval(code)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
class PipelineBuilder:
|
|
608
|
+
"""Fluent API for building pipelines"""
|
|
609
|
+
|
|
610
|
+
def __init__(self, data: Any):
|
|
611
|
+
self._data = data
|
|
612
|
+
self._ops: List[str] = []
|
|
613
|
+
|
|
614
|
+
def map(self, fn: str) -> PipelineBuilder:
|
|
615
|
+
self._ops.append(f"map({fn})")
|
|
616
|
+
return self
|
|
617
|
+
|
|
618
|
+
def filter(self, fn: str) -> PipelineBuilder:
|
|
619
|
+
# The shell calls this `where`; there is no `filter` builtin.
|
|
620
|
+
self._ops.append(f"where({fn})")
|
|
621
|
+
return self
|
|
622
|
+
|
|
623
|
+
def reduce(self, fn: str, initial: Any) -> PipelineBuilder:
|
|
624
|
+
self._ops.append(f"reduce({fn}, {_ironshell_value(initial)})")
|
|
625
|
+
return self
|
|
626
|
+
|
|
627
|
+
def sort(self, fn: Optional[str] = None) -> PipelineBuilder:
|
|
628
|
+
# `fn` is a key function, `fn(x) => key`: `sort` takes no argument, and
|
|
629
|
+
# `sort_by` orders by the key it returns.
|
|
630
|
+
self._ops.append(f"sort_by({fn})" if fn else "sort()")
|
|
631
|
+
return self
|
|
632
|
+
|
|
633
|
+
def reverse(self) -> PipelineBuilder:
|
|
634
|
+
self._ops.append("reverse()")
|
|
635
|
+
return self
|
|
636
|
+
|
|
637
|
+
def flatten(self) -> PipelineBuilder:
|
|
638
|
+
self._ops.append("flatten()")
|
|
639
|
+
return self
|
|
640
|
+
|
|
641
|
+
def unique(self) -> PipelineBuilder:
|
|
642
|
+
self._ops.append("unique()")
|
|
643
|
+
return self
|
|
644
|
+
|
|
645
|
+
def take(self, n: int) -> PipelineBuilder:
|
|
646
|
+
self._ops.append(f"slice(0, {n})")
|
|
647
|
+
return self
|
|
648
|
+
|
|
649
|
+
def skip(self, n: int) -> PipelineBuilder:
|
|
650
|
+
self._ops.append(f"slice({n})")
|
|
651
|
+
return self
|
|
652
|
+
|
|
653
|
+
def to_code(self) -> str:
|
|
654
|
+
"""Get the pipeline as IronShell code"""
|
|
655
|
+
return f"{_ironshell_value(self._data)} | {' | '.join(self._ops)}"
|
|
656
|
+
|
|
657
|
+
def run(self, runtime: Optional[IronShellRuntime] = None) -> Any:
|
|
658
|
+
"""Execute the pipeline"""
|
|
659
|
+
rt = runtime or IronShellRuntime()
|
|
660
|
+
return rt.eval(self.to_code())
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def pipeline(data: Any) -> PipelineBuilder:
|
|
664
|
+
"""
|
|
665
|
+
Create a pipeline builder for fluent data processing.
|
|
666
|
+
|
|
667
|
+
Args:
|
|
668
|
+
data: Initial data
|
|
669
|
+
|
|
670
|
+
Returns:
|
|
671
|
+
PipelineBuilder for chaining operations
|
|
672
|
+
|
|
673
|
+
Example:
|
|
674
|
+
result = (
|
|
675
|
+
pipeline([1, 2, 3, 4, 5])
|
|
676
|
+
.map("fn(x) => x * 2")
|
|
677
|
+
.filter("fn(x) => x > 4")
|
|
678
|
+
.run()
|
|
679
|
+
)
|
|
680
|
+
# [6, 8, 10]
|
|
681
|
+
"""
|
|
682
|
+
return PipelineBuilder(data)
|