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/langchain.py ADDED
@@ -0,0 +1,1074 @@
1
+ """
2
+ IronShell LangChain Integration
3
+
4
+ Enhanced LangChain tools for IronShell:
5
+ - Core execution tools (code, pipelines, agents)
6
+ - Workflow orchestration tools (MapReduce, Saga, Fan-Out)
7
+ - Metrics and observability tools
8
+ - Distributed agent tools
9
+ - Service registry tools
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import asyncio
15
+ import json
16
+ import urllib.request
17
+ import urllib.error
18
+ from typing import Any, Dict, List, Optional, Type, Union
19
+
20
+ try:
21
+ from langchain.tools import BaseTool
22
+ from langchain.callbacks.manager import CallbackManagerForToolRun
23
+ from pydantic import BaseModel, Field
24
+ LANGCHAIN_AVAILABLE = True
25
+ except ImportError:
26
+ LANGCHAIN_AVAILABLE = False
27
+ BaseTool = object
28
+ BaseModel = object
29
+ Field = lambda **kwargs: None
30
+ CallbackManagerForToolRun = None
31
+
32
+ from . import IronShellRuntime, Agent as IronShellAgent, _ironshell_value
33
+ from .workflows import (
34
+ MapReduceWorkflow,
35
+ SagaWorkflow,
36
+ SagaStep,
37
+ FanOutWorkflow,
38
+ PipelineWorkflow,
39
+ CircuitBreaker,
40
+ WorkflowResult,
41
+ )
42
+ from .metrics import (
43
+ MetricsCollector,
44
+ get_metrics_collector,
45
+ Tracer,
46
+ Timer,
47
+ )
48
+ from .distributed import (
49
+ ServiceRegistry,
50
+ LeaderElection,
51
+ AgentRouter,
52
+ LoadBalancer,
53
+ DistributedSwarm,
54
+ )
55
+
56
+ __all__ = [
57
+ # Original tools
58
+ "IronShellTool",
59
+ "IronShellPipelineTool",
60
+ "IronShellAgentTool",
61
+ # New enhanced tools
62
+ "IronShellWorkflowTool",
63
+ "IronShellMapReduceTool",
64
+ "IronShellSagaTool",
65
+ "IronShellMetricsTool",
66
+ "IronShellTracingTool",
67
+ "IronShellDistributedAgentTool",
68
+ "IronShellServiceRegistryTool",
69
+ # Factory functions
70
+ "get_ironshell_tools",
71
+ "get_all_ironshell_tools",
72
+ "create_workflow_agent",
73
+ # HTTP Agent API tools
74
+ "AgentAPIClient",
75
+ "IronShellAgentAPITool",
76
+ "IronShellAgentAPIEvalTool",
77
+ "IronShellAgentAPIBuiltinsTool",
78
+ "get_agent_api_tools",
79
+ ]
80
+
81
+
82
+ def _check_langchain():
83
+ if not LANGCHAIN_AVAILABLE:
84
+ raise ImportError(
85
+ "langchain is required for LangChain integration. "
86
+ "Install with: pip install ironshell[langchain]"
87
+ )
88
+
89
+
90
+
91
+ # ============================================================================
92
+ # HTTP Agent API Client
93
+ # ============================================================================
94
+
95
+
96
+ class AgentAPIClient:
97
+ """
98
+ HTTP client for the IronShell Agent API server.
99
+
100
+ Connects to a running ``ish agent serve`` instance (default port 3002)
101
+ and sends structured JSON requests for code evaluation, builtin listing,
102
+ and schema retrieval.
103
+
104
+ Usage::
105
+
106
+ client = AgentAPIClient()
107
+ client = AgentAPIClient("http://myhost:3002")
108
+
109
+ result = client.eval('[1,2,3] | map(fn(x) => x * 2)')
110
+ builtins = client.list_builtins()
111
+ """
112
+
113
+ def __init__(self, base_url: str = "http://127.0.0.1:3002", timeout: int = 30):
114
+ self.base_url = base_url.rstrip("/")
115
+ self.timeout = timeout
116
+
117
+ def _post(self, payload: dict) -> dict:
118
+ """Send a JSON POST to the Agent API /request endpoint."""
119
+ data = json.dumps(payload).encode("utf-8")
120
+ req = urllib.request.Request(
121
+ f"{self.base_url}/request",
122
+ data=data,
123
+ headers={"Content-Type": "application/json"},
124
+ method="POST",
125
+ )
126
+ try:
127
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
128
+ return json.loads(resp.read().decode("utf-8"))
129
+ except urllib.error.URLError as exc:
130
+ raise ConnectionError(
131
+ f"Cannot reach Agent API at {self.base_url}. "
132
+ f"Start the server with: ish agent serve -- {exc}"
133
+ ) from exc
134
+
135
+ def eval(self, code: str) -> Any:
136
+ """Evaluate IronShell code via the Agent API."""
137
+ resp = self._post({"action": "eval", "code": code})
138
+ if resp.get("success"):
139
+ return resp.get("result")
140
+ raise RuntimeError(resp.get("error", "Unknown Agent API error"))
141
+
142
+ def list_builtins(self, category: Optional[str] = None) -> List[Dict[str, Any]]:
143
+ """List available IronShell builtins, optionally filtered by category."""
144
+ payload: Dict[str, Any] = {"action": "list_builtins"}
145
+ if category:
146
+ payload["category"] = category
147
+ resp = self._post(payload)
148
+ if resp.get("success"):
149
+ result = resp.get("result", {})
150
+ return result.get("builtins", [])
151
+ raise RuntimeError(resp.get("error", "Unknown Agent API error"))
152
+
153
+ def get_schema(self, fmt: str = "compact") -> str:
154
+ """Get the IronShell language schema for AI agents."""
155
+ resp = self._post({"action": "schema", "format": fmt})
156
+ if resp.get("success"):
157
+ return json.dumps(resp.get("result", {}), indent=2)
158
+ raise RuntimeError(resp.get("error", "Unknown Agent API error"))
159
+
160
+ def ping(self) -> bool:
161
+ """Check if the Agent API server is reachable."""
162
+ try:
163
+ self._post({"action": "list_builtins"})
164
+ return True
165
+ except (ConnectionError, Exception):
166
+ return False
167
+
168
+ # ============================================================================
169
+ # Core Tools (Enhanced)
170
+ # ============================================================================
171
+
172
+ if LANGCHAIN_AVAILABLE:
173
+
174
+ class IronShellInput(BaseModel):
175
+ """Input schema for IronShell tool"""
176
+ code: str = Field(description="IronShell code to evaluate")
177
+
178
+
179
+ class IronShellTool(BaseTool):
180
+ """
181
+ LangChain tool for executing IronShell code.
182
+
183
+ IronShell is a typed shell with AI capabilities. It supports:
184
+ - Typed data pipelines: `[1,2,3] | map(fn(x) => x * 2)`
185
+ - Records and tables: `{name: "John", age: 30}`
186
+ - File operations: `ls "." | where(fn(r) => r.size > 1000)`
187
+ - HTTP requests: `http_get("https://api.example.com")`
188
+ - JSON processing: `parse_json(data) | get("items")`
189
+ """
190
+
191
+ name: str = "ironshell"
192
+ description: str = (
193
+ "Execute IronShell code for data processing, file operations, "
194
+ "and system tasks. IronShell uses typed pipelines for data "
195
+ "transformation. Example: `[1,2,3] | map(fn(x) => x * 2)` returns [2,4,6]"
196
+ )
197
+ args_schema: Type[BaseModel] = IronShellInput
198
+
199
+ runtime: IronShellRuntime = None
200
+
201
+ def __init__(self, runtime: Optional[IronShellRuntime] = None, **kwargs):
202
+ super().__init__(**kwargs)
203
+ self.runtime = runtime or IronShellRuntime()
204
+
205
+ def _run(
206
+ self,
207
+ code: str,
208
+ run_manager: Optional[CallbackManagerForToolRun] = None,
209
+ ) -> str:
210
+ """Execute IronShell code"""
211
+ try:
212
+ result = self.runtime.eval(code)
213
+ if isinstance(result, (dict, list)):
214
+ return json.dumps(result, indent=2)
215
+ return str(result)
216
+ except Exception as e:
217
+ return f"Error: {e}"
218
+
219
+ async def _arun(
220
+ self,
221
+ code: str,
222
+ run_manager: Optional[CallbackManagerForToolRun] = None,
223
+ ) -> str:
224
+ """Execute IronShell code asynchronously"""
225
+ result = await asyncio.to_thread(self.runtime.eval, code)
226
+ return json.dumps(result, default=str) if isinstance(result, (dict, list)) else str(result)
227
+
228
+
229
+ class IronShellPipelineInput(BaseModel):
230
+ """Input schema for pipeline tool"""
231
+ data: str = Field(description="JSON data to process")
232
+ operations: str = Field(
233
+ description="Pipeline operations (e.g., 'map(fn(x) => x * 2) | filter(fn(x) => x > 0)')"
234
+ )
235
+
236
+
237
+ class IronShellPipelineTool(BaseTool):
238
+ """
239
+ LangChain tool for executing IronShell pipelines on data.
240
+
241
+ Pipelines transform data through a series of operations:
242
+ - map: Transform each element
243
+ - filter: Keep elements matching condition
244
+ - reduce: Aggregate elements
245
+ - sort, reverse, unique, flatten
246
+ - select, where: Table/record operations
247
+ """
248
+
249
+ name: str = "ironshell_pipeline"
250
+ description: str = (
251
+ "Process data through IronShell pipelines. "
252
+ "Provide JSON data and pipeline operations. "
253
+ "Example: data='[1,2,3]', operations='map(fn(x) => x * 2)' returns [2,4,6]"
254
+ )
255
+ args_schema: Type[BaseModel] = IronShellPipelineInput
256
+
257
+ runtime: IronShellRuntime = None
258
+
259
+ def __init__(self, runtime: Optional[IronShellRuntime] = None, **kwargs):
260
+ super().__init__(**kwargs)
261
+ self.runtime = runtime or IronShellRuntime()
262
+
263
+ def _run(
264
+ self,
265
+ data: str,
266
+ operations: str,
267
+ run_manager: Optional[CallbackManagerForToolRun] = None,
268
+ ) -> str:
269
+ """Execute pipeline on data"""
270
+ try:
271
+ code = f"{data} | {operations}"
272
+ result = self.runtime.eval(code)
273
+ if isinstance(result, (dict, list)):
274
+ return json.dumps(result, indent=2)
275
+ return str(result)
276
+ except Exception as e:
277
+ return f"Error: {e}"
278
+
279
+ async def _arun(
280
+ self,
281
+ data: str,
282
+ operations: str,
283
+ run_manager: Optional[CallbackManagerForToolRun] = None,
284
+ ) -> str:
285
+ """Execute pipeline asynchronously"""
286
+ code = f"{data} | {operations}"
287
+ result = await asyncio.to_thread(self.runtime.eval, code)
288
+ return json.dumps(result, default=str) if isinstance(result, (dict, list)) else str(result)
289
+
290
+
291
+ class IronShellAgentInput(BaseModel):
292
+ """Input schema for agent tool"""
293
+ goal: str = Field(description="Goal for the agent to accomplish")
294
+ tools: List[str] = Field(
295
+ default=[],
296
+ description="Tools the agent can use (e.g., ['http_get', 'read_text'])"
297
+ )
298
+
299
+
300
+ class IronShellAgentTool(BaseTool):
301
+ """
302
+ LangChain tool for running IronShell AI agents.
303
+
304
+ Creates an AI agent that can use tools to accomplish goals.
305
+ The agent reasons about the goal, plans steps, and executes
306
+ tools to achieve the objective.
307
+ """
308
+
309
+ name: str = "ironshell_agent"
310
+ description: str = (
311
+ "Run an AI agent with tools to accomplish a goal. "
312
+ "The agent will reason and use tools like http_get, read_text, write_file. "
313
+ "Example: goal='Find Python docs homepage', tools=['http_get']"
314
+ )
315
+ args_schema: Type[BaseModel] = IronShellAgentInput
316
+
317
+ runtime: IronShellRuntime = None
318
+ model: str = "openai:gpt-4o-mini"
319
+ max_steps: int = 10
320
+
321
+ def __init__(self, runtime: Optional[IronShellRuntime] = None, **kwargs):
322
+ super().__init__(**kwargs)
323
+ self.runtime = runtime or IronShellRuntime()
324
+
325
+ def _run(
326
+ self,
327
+ goal: str,
328
+ tools: List[str] = None,
329
+ run_manager: Optional[CallbackManagerForToolRun] = None,
330
+ ) -> str:
331
+ """Run agent with goal"""
332
+ async def run_agent():
333
+ agent = self.runtime.create_agent(
334
+ name="langchain_agent",
335
+ model=self.model,
336
+ tools=tools or [],
337
+ max_steps=self.max_steps,
338
+ )
339
+ result = await agent.run(goal)
340
+ return result
341
+
342
+ try:
343
+ result = asyncio.run(run_agent())
344
+ if result.success:
345
+ return str(result.result)
346
+ else:
347
+ return f"Agent failed: {result.result}"
348
+ except Exception as e:
349
+ return f"Error: {e}"
350
+
351
+ async def _arun(
352
+ self,
353
+ goal: str,
354
+ tools: List[str] = None,
355
+ run_manager: Optional[CallbackManagerForToolRun] = None,
356
+ ) -> str:
357
+ """Run agent asynchronously"""
358
+ agent = self.runtime.create_agent(
359
+ name="langchain_agent",
360
+ model=self.model,
361
+ tools=tools or [],
362
+ max_steps=self.max_steps,
363
+ )
364
+ result = await agent.run(goal)
365
+ return str(result.result) if result.success else f"Agent failed: {result.result}"
366
+
367
+
368
+ # ============================================================================
369
+ # Workflow Tools
370
+ # ============================================================================
371
+
372
+ class WorkflowInput(BaseModel):
373
+ """Input for workflow execution"""
374
+ input_data: str = Field(description="JSON input data for the workflow")
375
+ workflow_type: str = Field(
376
+ default="pipeline",
377
+ description="Workflow type: 'pipeline', 'map_reduce', 'saga', 'fan_out'"
378
+ )
379
+ steps: str = Field(
380
+ default="[]",
381
+ description="JSON array of step configurations"
382
+ )
383
+
384
+
385
+ class IronShellWorkflowTool(BaseTool):
386
+ """
387
+ Execute IronShell workflows.
388
+
389
+ Supports multiple workflow patterns for distributed processing:
390
+ - pipeline: Sequential processing stages
391
+ - map_reduce: Parallel map, then reduce
392
+ - saga: Steps with compensation on failure
393
+ - fan_out: Parallel workers with result gathering
394
+ """
395
+ name: str = "ironshell_workflow"
396
+ description: str = """Execute an IronShell workflow.
397
+ Workflow types:
398
+ - 'pipeline': Sequential processing stages
399
+ - 'map_reduce': Parallel map, then reduce
400
+ - 'saga': Steps with compensation on failure
401
+ - 'fan_out': Parallel workers with result gathering
402
+
403
+ Example: input_data='[1,2,3]' workflow_type='pipeline' steps='[{"code": "x * 2"}]'
404
+ """
405
+ args_schema: Type[BaseModel] = WorkflowInput
406
+
407
+ runtime: IronShellRuntime = None
408
+
409
+ def __init__(self, runtime: Optional[IronShellRuntime] = None, **kwargs):
410
+ super().__init__(**kwargs)
411
+ self.runtime = runtime or IronShellRuntime()
412
+
413
+ def _run(
414
+ self,
415
+ input_data: str,
416
+ workflow_type: str = "pipeline",
417
+ steps: str = "[]",
418
+ run_manager: Optional[CallbackManagerForToolRun] = None,
419
+ ) -> str:
420
+ """Execute workflow synchronously"""
421
+ data = json.loads(input_data)
422
+ step_configs = json.loads(steps)
423
+
424
+ result = asyncio.run(self._execute_workflow(data, workflow_type, step_configs))
425
+ return json.dumps({
426
+ "success": result.success,
427
+ "result": result.result,
428
+ "steps_completed": result.steps_completed,
429
+ "total_steps": result.total_steps,
430
+ "duration_ms": result.duration_ms,
431
+ "errors": result.errors,
432
+ }, default=str)
433
+
434
+ async def _arun(
435
+ self,
436
+ input_data: str,
437
+ workflow_type: str = "pipeline",
438
+ steps: str = "[]",
439
+ run_manager: Optional[CallbackManagerForToolRun] = None,
440
+ ) -> str:
441
+ """Execute workflow asynchronously"""
442
+ data = json.loads(input_data)
443
+ step_configs = json.loads(steps)
444
+
445
+ result = await self._execute_workflow(data, workflow_type, step_configs)
446
+ return json.dumps({
447
+ "success": result.success,
448
+ "result": result.result,
449
+ "steps_completed": result.steps_completed,
450
+ "total_steps": result.total_steps,
451
+ "duration_ms": result.duration_ms,
452
+ "errors": result.errors,
453
+ }, default=str)
454
+
455
+ async def _execute_workflow(
456
+ self,
457
+ data: Any,
458
+ workflow_type: str,
459
+ step_configs: List[Dict],
460
+ ) -> WorkflowResult:
461
+ """Execute the specified workflow type"""
462
+ if workflow_type == "pipeline":
463
+ stages = [
464
+ lambda d, code=s.get("code", "x"): self.runtime.eval(
465
+ f'let x = {_ironshell_value(d)}; {code}'
466
+ )
467
+ for s in step_configs
468
+ ]
469
+ workflow = PipelineWorkflow("langchain_pipeline", stages=stages)
470
+ return await workflow.run(data)
471
+
472
+ elif workflow_type == "map_reduce":
473
+ map_code = step_configs[0].get("map", "x") if step_configs else "x"
474
+ reduce_code = step_configs[0].get("reduce", "a + b") if step_configs else "a + b"
475
+
476
+ def map_fn(item):
477
+ return self.runtime.eval(f'let x = {_ironshell_value(item)}; {map_code}')
478
+
479
+ def reduce_fn(a, b):
480
+ return self.runtime.eval(
481
+ f'let a = {_ironshell_value(a)}; let b = {_ironshell_value(b)}; {reduce_code}'
482
+ )
483
+
484
+ workflow = MapReduceWorkflow("langchain_mapreduce", map_fn=map_fn, reduce_fn=reduce_fn)
485
+ return await workflow.run(data)
486
+
487
+ else:
488
+ workflow = PipelineWorkflow("langchain_default")
489
+ return await workflow.run(data)
490
+
491
+
492
+ class MapReduceInput(BaseModel):
493
+ """Input for MapReduce operations"""
494
+ data: str = Field(description="JSON array of data to process")
495
+ map_expr: str = Field(description="IronShell expression for map (use 'x' for item)")
496
+ reduce_expr: str = Field(description="IronShell expression for reduce (use 'a', 'b')")
497
+ chunk_size: int = Field(default=100, description="Size of chunks for parallel processing")
498
+
499
+
500
+ class IronShellMapReduceTool(BaseTool):
501
+ """
502
+ Execute MapReduce operations on data.
503
+
504
+ Splits data into chunks, processes in parallel, then combines.
505
+ """
506
+ name: str = "ironshell_mapreduce"
507
+ description: str = """Execute a MapReduce operation.
508
+ Example: data='[1,2,3,4,5]' map_expr='x * 2' reduce_expr='a + b'
509
+ Result: Sums doubled values = 30
510
+ """
511
+ args_schema: Type[BaseModel] = MapReduceInput
512
+ runtime: IronShellRuntime = None
513
+
514
+ def __init__(self, runtime: Optional[IronShellRuntime] = None, **kwargs):
515
+ super().__init__(**kwargs)
516
+ self.runtime = runtime or IronShellRuntime()
517
+
518
+ def _run(
519
+ self,
520
+ data: str,
521
+ map_expr: str,
522
+ reduce_expr: str,
523
+ chunk_size: int = 100,
524
+ run_manager: Optional[CallbackManagerForToolRun] = None,
525
+ ) -> str:
526
+ """Execute MapReduce synchronously"""
527
+ result = asyncio.run(self._mapreduce(data, map_expr, reduce_expr, chunk_size))
528
+ return json.dumps(result, default=str)
529
+
530
+ async def _arun(
531
+ self,
532
+ data: str,
533
+ map_expr: str,
534
+ reduce_expr: str,
535
+ chunk_size: int = 100,
536
+ run_manager: Optional[CallbackManagerForToolRun] = None,
537
+ ) -> str:
538
+ """Execute MapReduce asynchronously"""
539
+ result = await self._mapreduce(data, map_expr, reduce_expr, chunk_size)
540
+ return json.dumps(result, default=str)
541
+
542
+ async def _mapreduce(self, data: str, map_expr: str, reduce_expr: str, chunk_size: int) -> Dict[str, Any]:
543
+ """Perform MapReduce"""
544
+ items = json.loads(data)
545
+
546
+ def map_fn(item):
547
+ return self.runtime.eval(f'let x = {_ironshell_value(item)}; {map_expr}')
548
+
549
+ def reduce_fn(a, b):
550
+ return self.runtime.eval(f'let a = {_ironshell_value(a)}; let b = {_ironshell_value(b)}; {reduce_expr}')
551
+
552
+ workflow = MapReduceWorkflow("langchain_mapreduce", map_fn=map_fn, reduce_fn=reduce_fn, chunk_size=chunk_size)
553
+ result = await workflow.run(items)
554
+ return {"success": result.success, "result": result.result, "duration_ms": result.duration_ms}
555
+
556
+
557
+ class SagaInput(BaseModel):
558
+ """Input for Saga workflow"""
559
+ steps: str = Field(description="JSON array of saga steps with 'action' and 'compensate' code")
560
+ input_data: str = Field(description="JSON input data")
561
+
562
+
563
+ class IronShellSagaTool(BaseTool):
564
+ """
565
+ Execute Saga workflow pattern.
566
+
567
+ Runs steps in sequence; on failure, executes compensation in reverse.
568
+ """
569
+ name: str = "ironshell_saga"
570
+ description: str = """Execute a Saga workflow with compensation.
571
+ Steps run sequentially. On failure, compensations run in reverse.
572
+ Example: steps='[{"action": "x + 1", "compensate": "x - 1"}]'
573
+ """
574
+ args_schema: Type[BaseModel] = SagaInput
575
+ runtime: IronShellRuntime = None
576
+
577
+ def __init__(self, runtime: Optional[IronShellRuntime] = None, **kwargs):
578
+ super().__init__(**kwargs)
579
+ self.runtime = runtime or IronShellRuntime()
580
+
581
+ def _run(
582
+ self,
583
+ steps: str,
584
+ input_data: str,
585
+ run_manager: Optional[CallbackManagerForToolRun] = None,
586
+ ) -> str:
587
+ """Execute Saga synchronously"""
588
+ result = asyncio.run(self._saga(steps, input_data))
589
+ return json.dumps(result, default=str)
590
+
591
+ async def _saga(self, steps: str, input_data: str) -> Dict[str, Any]:
592
+ """Perform Saga"""
593
+ step_configs = json.loads(steps)
594
+ data = json.loads(input_data)
595
+
596
+ saga = SagaWorkflow("langchain_saga")
597
+ for i, cfg in enumerate(step_configs):
598
+ action_code = cfg.get("action", "x")
599
+ compensate_code = cfg.get("compensate", "x")
600
+
601
+ def make_action(code):
602
+ return lambda d: self.runtime.eval(f'let x = {_ironshell_value(d)}; {code}')
603
+
604
+ saga.add_saga_step(f"step_{i}", make_action(action_code), make_action(compensate_code))
605
+
606
+ result = await saga.run(data)
607
+ return {"success": result.success, "result": result.result, "steps_completed": result.steps_completed}
608
+
609
+
610
+ # ============================================================================
611
+ # Metrics Tools
612
+ # ============================================================================
613
+
614
+ class MetricsInput(BaseModel):
615
+ """Input for metrics operations"""
616
+ operation: str = Field(description="Operation: 'get', 'increment', 'set', 'record', 'export'")
617
+ metric_name: str = Field(description="Name of the metric")
618
+ value: Optional[float] = Field(default=None, description="Value for set/record operations")
619
+ metric_type: str = Field(default="counter", description="Type: 'counter', 'gauge', 'histogram'")
620
+
621
+
622
+ class IronShellMetricsTool(BaseTool):
623
+ """
624
+ Access IronShell metrics for monitoring.
625
+
626
+ Monitor and observe agent/workflow performance.
627
+ """
628
+ name: str = "ironshell_metrics"
629
+ description: str = """Access metrics for monitoring.
630
+ Operations:
631
+ - 'get': Get current metric value
632
+ - 'increment': Increment a counter
633
+ - 'set': Set a gauge value
634
+ - 'record': Record histogram observation
635
+ - 'export': Export all metrics (Prometheus format)
636
+ """
637
+ args_schema: Type[BaseModel] = MetricsInput
638
+ collector: MetricsCollector = None
639
+
640
+ def __init__(self, collector: Optional[MetricsCollector] = None, **kwargs):
641
+ super().__init__(**kwargs)
642
+ self.collector = collector or get_metrics_collector()
643
+
644
+ def _run(
645
+ self,
646
+ operation: str,
647
+ metric_name: str,
648
+ value: Optional[float] = None,
649
+ metric_type: str = "counter",
650
+ run_manager: Optional[CallbackManagerForToolRun] = None,
651
+ ) -> str:
652
+ """Execute metrics operation"""
653
+ if operation == "get":
654
+ if metric_type == "counter":
655
+ counter = self.collector.counter(metric_name)
656
+ return json.dumps({"name": metric_name, "value": counter.value})
657
+ elif metric_type == "gauge":
658
+ gauge = self.collector.gauge(metric_name)
659
+ return json.dumps({"name": metric_name, "value": gauge.value})
660
+ elif metric_type == "histogram":
661
+ hist = self.collector.histogram(metric_name)
662
+ return json.dumps({"name": metric_name, "count": hist.count, "sum": hist.sum, "mean": hist.mean})
663
+
664
+ elif operation == "increment":
665
+ counter = self.collector.counter(metric_name)
666
+ counter.inc(value or 1.0)
667
+ return json.dumps({"name": metric_name, "value": counter.value})
668
+
669
+ elif operation == "set":
670
+ gauge = self.collector.gauge(metric_name)
671
+ gauge.set(value or 0.0)
672
+ return json.dumps({"name": metric_name, "value": gauge.value})
673
+
674
+ elif operation == "record":
675
+ hist = self.collector.histogram(metric_name)
676
+ hist.observe(value or 0.0)
677
+ return json.dumps({"name": metric_name, "recorded": value})
678
+
679
+ elif operation == "export":
680
+ return self.collector.to_prometheus()
681
+
682
+ return json.dumps({"error": f"Unknown operation: {operation}"})
683
+
684
+
685
+ class TracingInput(BaseModel):
686
+ """Input for tracing operations"""
687
+ operation: str = Field(description="Operation: 'get_trace', 'clear'")
688
+ trace_id: Optional[str] = Field(default=None, description="Trace ID to query")
689
+
690
+
691
+ class IronShellTracingTool(BaseTool):
692
+ """
693
+ Distributed tracing for debugging.
694
+ """
695
+ name: str = "ironshell_tracing"
696
+ description: str = """Manage distributed traces.
697
+ Operations:
698
+ - 'get_trace': Get all spans (JSON format)
699
+ - 'clear': Clear recorded spans
700
+ """
701
+ args_schema: Type[BaseModel] = TracingInput
702
+ tracer: Tracer = None
703
+
704
+ def __init__(self, tracer: Optional[Tracer] = None, **kwargs):
705
+ super().__init__(**kwargs)
706
+ self.tracer = tracer or get_metrics_collector().tracer()
707
+
708
+ def _run(
709
+ self,
710
+ operation: str,
711
+ trace_id: Optional[str] = None,
712
+ run_manager: Optional[CallbackManagerForToolRun] = None,
713
+ ) -> str:
714
+ """Execute tracing operation"""
715
+ if operation == "get_trace":
716
+ spans = self.tracer.get_spans()
717
+ if trace_id:
718
+ spans = [s for s in spans if s.trace_id == trace_id]
719
+ return json.dumps([s.to_dict() for s in spans], default=str)
720
+ elif operation == "clear":
721
+ self.tracer.clear_spans()
722
+ return json.dumps({"status": "cleared"})
723
+ return json.dumps({"error": f"Unknown operation: {operation}"})
724
+
725
+
726
+ # ============================================================================
727
+ # Distributed Agent Tools
728
+ # ============================================================================
729
+
730
+ class DistributedAgentInput(BaseModel):
731
+ """Input for distributed agent operations"""
732
+ goal: str = Field(description="Goal for the agent(s)")
733
+ agent_name: Optional[str] = Field(default=None, description="Specific agent to use")
734
+ capability: Optional[str] = Field(default=None, description="Required capability")
735
+ broadcast: bool = Field(default=False, description="Broadcast to all agents")
736
+
737
+
738
+ class IronShellDistributedAgentTool(BaseTool):
739
+ """
740
+ Run distributed AI agents.
741
+
742
+ Routes tasks to agents across a cluster.
743
+ """
744
+ name: str = "ironshell_distributed_agent"
745
+ description: str = """Run a task on distributed agents.
746
+ Can route by:
747
+ - agent_name: Specific agent
748
+ - capability: Any agent with that capability
749
+ - broadcast=True: All agents
750
+ """
751
+ args_schema: Type[BaseModel] = DistributedAgentInput
752
+ swarm: DistributedSwarm = None
753
+
754
+ def __init__(self, swarm: Optional[DistributedSwarm] = None, registry: Optional[ServiceRegistry] = None, **kwargs):
755
+ super().__init__(**kwargs)
756
+ if swarm:
757
+ self.swarm = swarm
758
+ else:
759
+ registry = registry or ServiceRegistry()
760
+ self.swarm = DistributedSwarm("langchain_swarm", registry)
761
+
762
+ def _run(
763
+ self,
764
+ goal: str,
765
+ agent_name: Optional[str] = None,
766
+ capability: Optional[str] = None,
767
+ broadcast: bool = False,
768
+ run_manager: Optional[CallbackManagerForToolRun] = None,
769
+ ) -> str:
770
+ """Execute distributed agent task"""
771
+ result = asyncio.run(self._dispatch(goal, agent_name, capability, broadcast))
772
+ return json.dumps(result, default=str)
773
+
774
+ async def _arun(
775
+ self,
776
+ goal: str,
777
+ agent_name: Optional[str] = None,
778
+ capability: Optional[str] = None,
779
+ broadcast: bool = False,
780
+ run_manager: Optional[CallbackManagerForToolRun] = None,
781
+ ) -> str:
782
+ """Execute distributed agent task asynchronously"""
783
+ result = await self._dispatch(goal, agent_name, capability, broadcast)
784
+ return json.dumps(result, default=str)
785
+
786
+ async def _dispatch(self, goal: str, agent_name: Optional[str], capability: Optional[str], broadcast: bool) -> Any:
787
+ """Dispatch task to agents"""
788
+ if broadcast:
789
+ return await self.swarm.broadcast(goal)
790
+ else:
791
+ return await self.swarm.dispatch(goal, agent_name=agent_name, capability=capability)
792
+
793
+
794
+ class ServiceRegistryInput(BaseModel):
795
+ """Input for service registry operations"""
796
+ operation: str = Field(description="Operation: 'list', 'register', 'deregister', 'health'")
797
+ service_name: Optional[str] = Field(default=None, description="Service name")
798
+ host: Optional[str] = Field(default=None, description="Service host")
799
+ port: Optional[int] = Field(default=None, description="Service port")
800
+
801
+
802
+ class IronShellServiceRegistryTool(BaseTool):
803
+ """
804
+ Manage the service registry.
805
+ """
806
+ name: str = "ironshell_service_registry"
807
+ description: str = """Manage agent service registry.
808
+ Operations:
809
+ - 'list': List all registered services
810
+ - 'register': Register a new service
811
+ - 'deregister': Remove a service
812
+ - 'health': Get service health status
813
+ """
814
+ args_schema: Type[BaseModel] = ServiceRegistryInput
815
+ registry: ServiceRegistry = None
816
+
817
+ def __init__(self, registry: Optional[ServiceRegistry] = None, **kwargs):
818
+ super().__init__(**kwargs)
819
+ self.registry = registry or ServiceRegistry()
820
+
821
+ def _run(
822
+ self,
823
+ operation: str,
824
+ service_name: Optional[str] = None,
825
+ host: Optional[str] = None,
826
+ port: Optional[int] = None,
827
+ run_manager: Optional[CallbackManagerForToolRun] = None,
828
+ ) -> str:
829
+ """Execute registry operation"""
830
+ if operation == "list":
831
+ services = self.registry.get_all_services(healthy_only=False)
832
+ return json.dumps([s.to_dict() for s in services], default=str)
833
+
834
+ elif operation == "register":
835
+ if not all([service_name, host, port]):
836
+ return json.dumps({"error": "name, host, and port required"})
837
+ service = self.registry.register(service_name, host, port)
838
+ return json.dumps(service.to_dict(), default=str)
839
+
840
+ elif operation == "deregister":
841
+ if not service_name:
842
+ return json.dumps({"error": "service_name required"})
843
+ services = self.registry.get_services_by_name(service_name, healthy_only=False)
844
+ removed = sum(1 for s in services if self.registry.deregister(s.service_id))
845
+ return json.dumps({"removed": removed})
846
+
847
+ elif operation == "health":
848
+ services = self.registry.get_all_services(healthy_only=False)
849
+ return json.dumps({
850
+ "total": len(services),
851
+ "healthy": sum(1 for s in services if s.is_healthy),
852
+ "unhealthy": sum(1 for s in services if not s.is_healthy),
853
+ })
854
+
855
+ return json.dumps({"error": f"Unknown operation: {operation}"})
856
+
857
+
858
+
859
+ # ============================================================================
860
+ # HTTP Agent API LangChain Tools
861
+ # ============================================================================
862
+
863
+ if LANGCHAIN_AVAILABLE:
864
+
865
+ class IronShellAgentAPIEvalTool(BaseTool):
866
+ """LangChain tool for evaluating IronShell code via the HTTP Agent API."""
867
+
868
+ name: str = "ironshell_agent_api_eval"
869
+ description: str = (
870
+ "Evaluate IronShell code on a remote Agent API server. "
871
+ "Use this to run shell commands, data pipelines, or AI queries "
872
+ "via a running ish agent serve instance."
873
+ )
874
+ client: Any = None
875
+
876
+ def __init__(self, client: Optional[AgentAPIClient] = None, **kwargs):
877
+ super().__init__(**kwargs)
878
+ self.client = client or AgentAPIClient()
879
+
880
+ def _run(self, code: str) -> str:
881
+ try:
882
+ result = self.client.eval(code)
883
+ return json.dumps(result) if not isinstance(result, str) else result
884
+ except Exception as e:
885
+ return f"Error: {e}"
886
+
887
+ async def _arun(self, code: str) -> str:
888
+ return self._run(code)
889
+
890
+
891
+ class IronShellAgentAPITool(BaseTool):
892
+ """LangChain tool for interacting with the IronShell Agent API.
893
+
894
+ Supports eval, list_builtins, and schema actions.
895
+ """
896
+
897
+ name: str = "ironshell_agent_api"
898
+ description: str = (
899
+ "Interact with an IronShell Agent API server. "
900
+ "Actions: eval (run code), list_builtins (show available builtins), "
901
+ 'schema (get language schema). Input is JSON: {"action": "eval", "code": "..."}'
902
+ )
903
+ client: Any = None
904
+
905
+ def __init__(self, client: Optional[AgentAPIClient] = None, **kwargs):
906
+ super().__init__(**kwargs)
907
+ self.client = client or AgentAPIClient()
908
+
909
+ def _run(self, input_str: str) -> str:
910
+ try:
911
+ params = json.loads(input_str) if isinstance(input_str, str) else input_str
912
+ action = params.get("action", "eval")
913
+ if action == "eval":
914
+ result = self.client.eval(params.get("code", ""))
915
+ return json.dumps(result) if not isinstance(result, str) else result
916
+ elif action == "list_builtins":
917
+ builtins = self.client.list_builtins(params.get("category"))
918
+ return json.dumps(builtins)
919
+ elif action == "schema":
920
+ return self.client.get_schema(params.get("format", "compact"))
921
+ else:
922
+ return json.dumps({"error": f"Unknown action: {action}"})
923
+ except Exception as e:
924
+ return f"Error: {e}"
925
+
926
+ async def _arun(self, input_str: str) -> str:
927
+ return self._run(input_str)
928
+
929
+
930
+ class IronShellAgentAPIBuiltinsTool(BaseTool):
931
+ """LangChain tool for listing IronShell builtins via the Agent API."""
932
+
933
+ name: str = "ironshell_agent_api_builtins"
934
+ description: str = (
935
+ "List available IronShell builtins from the Agent API server. "
936
+ "Optionally filter by category (e.g., 'io', 'data', 'ai')."
937
+ )
938
+ client: Any = None
939
+
940
+ def __init__(self, client: Optional[AgentAPIClient] = None, **kwargs):
941
+ super().__init__(**kwargs)
942
+ self.client = client or AgentAPIClient()
943
+
944
+ def _run(self, category: str = "") -> str:
945
+ try:
946
+ cat = category.strip() if category else None
947
+ builtins = self.client.list_builtins(cat)
948
+ return json.dumps(builtins)
949
+ except Exception as e:
950
+ return f"Error: {e}"
951
+
952
+ async def _arun(self, category: str = "") -> str:
953
+ return self._run(category)
954
+
955
+ # ============================================================================
956
+ # Factory Functions
957
+ # ============================================================================
958
+
959
+ def get_ironshell_tools(runtime: Optional[IronShellRuntime] = None) -> List:
960
+ """
961
+ Get the basic set of IronShell LangChain tools.
962
+
963
+ Args:
964
+ runtime: Optional IronShellRuntime instance
965
+
966
+ Returns:
967
+ List of LangChain tools
968
+ """
969
+ _check_langchain()
970
+ runtime = runtime or IronShellRuntime()
971
+ return [
972
+ IronShellTool(runtime=runtime),
973
+ IronShellPipelineTool(runtime=runtime),
974
+ IronShellAgentTool(runtime=runtime),
975
+ ]
976
+
977
+
978
+ def get_all_ironshell_tools(
979
+ runtime: Optional[IronShellRuntime] = None,
980
+ metrics_collector: Optional[MetricsCollector] = None,
981
+ service_registry: Optional[ServiceRegistry] = None,
982
+ ) -> List:
983
+ """
984
+ Get all IronShell LangChain tools including advanced features.
985
+
986
+ Args:
987
+ runtime: Optional IronShellRuntime instance
988
+ metrics_collector: Optional MetricsCollector
989
+ service_registry: Optional ServiceRegistry
990
+
991
+ Returns:
992
+ List of all available LangChain tools
993
+ """
994
+ _check_langchain()
995
+ runtime = runtime or IronShellRuntime()
996
+ metrics = metrics_collector or get_metrics_collector()
997
+ registry = service_registry or ServiceRegistry()
998
+
999
+ return [
1000
+ # Basic tools
1001
+ IronShellTool(runtime=runtime),
1002
+ IronShellPipelineTool(runtime=runtime),
1003
+ IronShellAgentTool(runtime=runtime),
1004
+ # Workflow tools
1005
+ IronShellWorkflowTool(runtime=runtime),
1006
+ IronShellMapReduceTool(runtime=runtime),
1007
+ IronShellSagaTool(runtime=runtime),
1008
+ # Metrics tools
1009
+ IronShellMetricsTool(collector=metrics),
1010
+ IronShellTracingTool(tracer=metrics.tracer()),
1011
+ # Distributed tools
1012
+ IronShellDistributedAgentTool(registry=registry),
1013
+ IronShellServiceRegistryTool(registry=registry),
1014
+ ]
1015
+
1016
+
1017
+ def create_workflow_agent(model: str = "openai:gpt-4o-mini", runtime: Optional[IronShellRuntime] = None) -> IronShellAgent:
1018
+ """
1019
+ Create an agent configured for workflow orchestration.
1020
+
1021
+ The orchestration instructions are passed as a system prompt, which the
1022
+ agent builtin cannot take, so this raises ValueError until the shell gains
1023
+ a system prompt option.
1024
+
1025
+ Args:
1026
+ model: Must be the default "openai:gpt-4o-mini" (see Agent)
1027
+ runtime: Optional IronShellRuntime
1028
+
1029
+ Returns:
1030
+ Configured Agent instance
1031
+
1032
+ Raises:
1033
+ ValueError: always, because of the system prompt (see Agent).
1034
+ """
1035
+ from . import Agent
1036
+ runtime = runtime or IronShellRuntime()
1037
+
1038
+ return Agent(
1039
+ name="workflow_orchestrator",
1040
+ model=model,
1041
+ system_prompt="""You are a workflow orchestration agent for IronShell.
1042
+ You can create and execute data pipelines, MapReduce operations, and distributed agent tasks.
1043
+ Prefer pipelines for sequential transformations, MapReduce for large parallel processing.""",
1044
+ runtime=runtime,
1045
+ )
1046
+
1047
+
1048
+ def get_agent_api_tools(base_url: str = "http://127.0.0.1:3002") -> List:
1049
+ """
1050
+ Get LangChain tools that connect to a running IronShell Agent API server.
1051
+
1052
+ These tools use HTTP to communicate with ``ish agent serve`` and do not
1053
+ require a local IronShell binary or Python subprocess.
1054
+
1055
+ Args:
1056
+ base_url: Agent API server URL (default: http://127.0.0.1:3002)
1057
+
1058
+ Returns:
1059
+ List of LangChain tools for the Agent API
1060
+
1061
+ Example::
1062
+
1063
+ from ironshell.langchain import get_agent_api_tools
1064
+ tools = get_agent_api_tools("http://localhost:3002")
1065
+ # Use with a LangChain agent
1066
+ agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION)
1067
+ """
1068
+ _check_langchain()
1069
+ client = AgentAPIClient(base_url=base_url)
1070
+ return [
1071
+ IronShellAgentAPIEvalTool(client=client),
1072
+ IronShellAgentAPITool(client=client),
1073
+ IronShellAgentAPIBuiltinsTool(client=client),
1074
+ ]