soorma-core 0.3.0__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.
- soorma/__init__.py +138 -0
- soorma/agents/__init__.py +17 -0
- soorma/agents/base.py +523 -0
- soorma/agents/planner.py +391 -0
- soorma/agents/tool.py +373 -0
- soorma/agents/worker.py +385 -0
- soorma/ai/event_toolkit.py +281 -0
- soorma/ai/tools.py +280 -0
- soorma/cli/__init__.py +7 -0
- soorma/cli/commands/__init__.py +3 -0
- soorma/cli/commands/dev.py +780 -0
- soorma/cli/commands/init.py +717 -0
- soorma/cli/main.py +52 -0
- soorma/context.py +832 -0
- soorma/events.py +496 -0
- soorma/models.py +24 -0
- soorma/registry/client.py +186 -0
- soorma/utils/schema_utils.py +209 -0
- soorma_core-0.3.0.dist-info/METADATA +454 -0
- soorma_core-0.3.0.dist-info/RECORD +23 -0
- soorma_core-0.3.0.dist-info/WHEEL +4 -0
- soorma_core-0.3.0.dist-info/entry_points.txt +3 -0
- soorma_core-0.3.0.dist-info/licenses/LICENSE.txt +21 -0
soorma/agents/tool.py
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool Service - Atomic, stateless capability.
|
|
3
|
+
|
|
4
|
+
Tools are specialized micro-services that perform atomic, stateless operations.
|
|
5
|
+
They:
|
|
6
|
+
- Expose specific capabilities (e.g., calculator, API search, file parser)
|
|
7
|
+
- Handle synchronous request/response operations
|
|
8
|
+
- Are stateless - no memory of previous calls
|
|
9
|
+
- Often wrap external APIs or perform deterministic computations
|
|
10
|
+
|
|
11
|
+
Unlike Workers (which are cognitive), Tools are rules-based and deterministic.
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
from soorma.agents import Tool
|
|
15
|
+
|
|
16
|
+
tool = Tool(
|
|
17
|
+
name="calculator-tool",
|
|
18
|
+
description="Performs mathematical calculations",
|
|
19
|
+
capabilities=["arithmetic", "unit_conversion"],
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
@tool.on_invoke("calculate")
|
|
23
|
+
async def calculate(request, context):
|
|
24
|
+
expression = request.data["expression"]
|
|
25
|
+
result = eval(expression) # (use safe_eval in production!)
|
|
26
|
+
return {"result": result, "expression": expression}
|
|
27
|
+
|
|
28
|
+
@tool.on_invoke("convert_units")
|
|
29
|
+
async def convert_units(request, context):
|
|
30
|
+
value = request.data["value"]
|
|
31
|
+
from_unit = request.data["from"]
|
|
32
|
+
to_unit = request.data["to"]
|
|
33
|
+
converted = perform_conversion(value, from_unit, to_unit)
|
|
34
|
+
return {"result": converted, "from": from_unit, "to": to_unit}
|
|
35
|
+
|
|
36
|
+
tool.run()
|
|
37
|
+
"""
|
|
38
|
+
import logging
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional
|
|
41
|
+
from uuid import uuid4
|
|
42
|
+
|
|
43
|
+
from .base import Agent
|
|
44
|
+
from ..context import PlatformContext
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class ToolRequest:
|
|
51
|
+
"""
|
|
52
|
+
A request to invoke a tool operation.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
operation: The operation to perform
|
|
56
|
+
data: Input parameters
|
|
57
|
+
request_id: Unique request identifier
|
|
58
|
+
correlation_id: Tracking ID
|
|
59
|
+
timeout: Request timeout in seconds
|
|
60
|
+
"""
|
|
61
|
+
operation: str
|
|
62
|
+
data: Dict[str, Any]
|
|
63
|
+
request_id: str = field(default_factory=lambda: str(uuid4()))
|
|
64
|
+
correlation_id: Optional[str] = None
|
|
65
|
+
session_id: Optional[str] = None
|
|
66
|
+
tenant_id: Optional[str] = None
|
|
67
|
+
timeout: Optional[float] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class ToolResponse:
|
|
72
|
+
"""
|
|
73
|
+
Response from a tool invocation.
|
|
74
|
+
|
|
75
|
+
Attributes:
|
|
76
|
+
request_id: Original request ID
|
|
77
|
+
success: Whether the operation succeeded
|
|
78
|
+
data: Result data (if successful)
|
|
79
|
+
error: Error message (if failed)
|
|
80
|
+
"""
|
|
81
|
+
request_id: str
|
|
82
|
+
success: bool
|
|
83
|
+
data: Optional[Dict[str, Any]] = None
|
|
84
|
+
error: Optional[str] = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Type alias for tool handlers
|
|
88
|
+
ToolHandler = Callable[[ToolRequest, PlatformContext], Awaitable[Dict[str, Any]]]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Tool(Agent):
|
|
92
|
+
"""
|
|
93
|
+
Atomic, stateless capability micro-service.
|
|
94
|
+
|
|
95
|
+
Tools are the "utilities" of the DisCo architecture. They:
|
|
96
|
+
1. Expose deterministic, stateless operations
|
|
97
|
+
2. Handle both event-driven and synchronous requests
|
|
98
|
+
3. Wrap external APIs or perform computations
|
|
99
|
+
4. Are highly reusable across different workflows
|
|
100
|
+
|
|
101
|
+
Key differences from Workers:
|
|
102
|
+
- Tools are stateless (no memory between calls)
|
|
103
|
+
- Tools are deterministic (same input = same output)
|
|
104
|
+
- Tools are typically rules-based, not cognitive
|
|
105
|
+
- Tools can also expose REST endpoints for sync calls
|
|
106
|
+
|
|
107
|
+
Attributes:
|
|
108
|
+
All Agent attributes, plus:
|
|
109
|
+
on_invoke: Decorator for registering operation handlers
|
|
110
|
+
|
|
111
|
+
Usage:
|
|
112
|
+
tool = Tool(
|
|
113
|
+
name="weather-api",
|
|
114
|
+
description="Fetches weather data",
|
|
115
|
+
capabilities=["current_weather", "forecast"],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
@tool.on_invoke("get_weather")
|
|
119
|
+
async def get_weather(request: ToolRequest, context: PlatformContext) -> Dict:
|
|
120
|
+
location = request.data["location"]
|
|
121
|
+
weather = await fetch_weather_api(location)
|
|
122
|
+
return {"temperature": weather.temp, "conditions": weather.conditions}
|
|
123
|
+
|
|
124
|
+
tool.run()
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
name: str,
|
|
130
|
+
description: str = "",
|
|
131
|
+
version: str = "0.1.0",
|
|
132
|
+
capabilities: Optional[List[str]] = None,
|
|
133
|
+
**kwargs,
|
|
134
|
+
):
|
|
135
|
+
"""
|
|
136
|
+
Initialize the Tool.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
name: Tool name
|
|
140
|
+
description: What this tool does
|
|
141
|
+
version: Version string
|
|
142
|
+
capabilities: Operations this tool provides
|
|
143
|
+
**kwargs: Additional Agent arguments
|
|
144
|
+
"""
|
|
145
|
+
# Tools consume tool.request and produce tool.response
|
|
146
|
+
events_consumed = kwargs.pop("events_consumed", [])
|
|
147
|
+
if "tool.request" not in events_consumed:
|
|
148
|
+
events_consumed.append("tool.request")
|
|
149
|
+
|
|
150
|
+
events_produced = kwargs.pop("events_produced", [])
|
|
151
|
+
if "tool.response" not in events_produced:
|
|
152
|
+
events_produced.append("tool.response")
|
|
153
|
+
|
|
154
|
+
super().__init__(
|
|
155
|
+
name=name,
|
|
156
|
+
description=description,
|
|
157
|
+
version=version,
|
|
158
|
+
agent_type="tool",
|
|
159
|
+
capabilities=capabilities or [],
|
|
160
|
+
events_consumed=events_consumed,
|
|
161
|
+
events_produced=events_produced,
|
|
162
|
+
**kwargs,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Operation handlers: operation_name -> handler
|
|
166
|
+
self._operation_handlers: Dict[str, ToolHandler] = {}
|
|
167
|
+
|
|
168
|
+
# Register the main tool.request handler
|
|
169
|
+
self._register_tool_request_handler()
|
|
170
|
+
|
|
171
|
+
def _register_tool_request_handler(self) -> None:
|
|
172
|
+
"""Register the main tool.request event handler."""
|
|
173
|
+
@self.on_event("tool.request")
|
|
174
|
+
async def handle_tool_request(event: Dict[str, Any], context: PlatformContext) -> None:
|
|
175
|
+
await self._handle_tool_request(event, context)
|
|
176
|
+
|
|
177
|
+
def on_invoke(self, operation: str) -> Callable[[ToolHandler], ToolHandler]:
|
|
178
|
+
"""
|
|
179
|
+
Decorator to register an operation handler.
|
|
180
|
+
|
|
181
|
+
Operation handlers receive a ToolRequest and PlatformContext,
|
|
182
|
+
and return a result dictionary.
|
|
183
|
+
|
|
184
|
+
Usage:
|
|
185
|
+
@tool.on_invoke("calculate")
|
|
186
|
+
async def calculate(request: ToolRequest, context: PlatformContext) -> Dict:
|
|
187
|
+
result = compute(request.data["expression"])
|
|
188
|
+
return {"result": result}
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
operation: The operation name to handle
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Decorator function
|
|
195
|
+
"""
|
|
196
|
+
def decorator(func: ToolHandler) -> ToolHandler:
|
|
197
|
+
self._operation_handlers[operation] = func
|
|
198
|
+
|
|
199
|
+
# Add to capabilities if not already there
|
|
200
|
+
if operation not in self.config.capabilities:
|
|
201
|
+
self.config.capabilities.append(operation)
|
|
202
|
+
|
|
203
|
+
logger.debug(f"Registered operation handler: {operation}")
|
|
204
|
+
return func
|
|
205
|
+
return decorator
|
|
206
|
+
|
|
207
|
+
async def _handle_tool_request(
|
|
208
|
+
self,
|
|
209
|
+
event: Dict[str, Any],
|
|
210
|
+
context: PlatformContext,
|
|
211
|
+
) -> None:
|
|
212
|
+
"""Handle an incoming tool.request event."""
|
|
213
|
+
data = event.get("data", {})
|
|
214
|
+
|
|
215
|
+
operation = data.get("operation")
|
|
216
|
+
target_tool = data.get("tool")
|
|
217
|
+
|
|
218
|
+
# Check if this request is for us
|
|
219
|
+
if not self._should_handle_request(target_tool):
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
handler = self._operation_handlers.get(operation)
|
|
223
|
+
if not handler:
|
|
224
|
+
logger.debug(f"No handler for operation: {operation}")
|
|
225
|
+
# Emit error response
|
|
226
|
+
await self._emit_error_response(
|
|
227
|
+
request_id=data.get("request_id", str(uuid4())),
|
|
228
|
+
error=f"Unknown operation: {operation}",
|
|
229
|
+
correlation_id=event.get("correlation_id"),
|
|
230
|
+
context=context,
|
|
231
|
+
)
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
# Create ToolRequest
|
|
235
|
+
request = ToolRequest(
|
|
236
|
+
operation=operation,
|
|
237
|
+
data=data.get("data", {}),
|
|
238
|
+
request_id=data.get("request_id", str(uuid4())),
|
|
239
|
+
correlation_id=event.get("correlation_id"),
|
|
240
|
+
session_id=event.get("session_id"),
|
|
241
|
+
tenant_id=event.get("tenant_id"),
|
|
242
|
+
timeout=data.get("timeout"),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
# Execute operation
|
|
247
|
+
logger.info(f"Executing operation: {operation} ({request.request_id})")
|
|
248
|
+
result = await handler(request, context)
|
|
249
|
+
|
|
250
|
+
# Emit tool.response
|
|
251
|
+
await context.bus.publish(
|
|
252
|
+
event_type="tool.response",
|
|
253
|
+
data={
|
|
254
|
+
"request_id": request.request_id,
|
|
255
|
+
"operation": operation,
|
|
256
|
+
"success": True,
|
|
257
|
+
"result": result,
|
|
258
|
+
},
|
|
259
|
+
topic="action-results", # Tools use action-results topic
|
|
260
|
+
correlation_id=request.correlation_id,
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
logger.info(f"Completed operation: {operation} ({request.request_id})")
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
logger.error(f"Operation failed: {operation} - {e}")
|
|
267
|
+
await self._emit_error_response(
|
|
268
|
+
request_id=request.request_id,
|
|
269
|
+
error=str(e),
|
|
270
|
+
correlation_id=request.correlation_id,
|
|
271
|
+
context=context,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
async def _emit_error_response(
|
|
275
|
+
self,
|
|
276
|
+
request_id: str,
|
|
277
|
+
error: str,
|
|
278
|
+
correlation_id: Optional[str],
|
|
279
|
+
context: PlatformContext,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Emit an error response."""
|
|
282
|
+
await context.bus.publish(
|
|
283
|
+
event_type="tool.response",
|
|
284
|
+
data={
|
|
285
|
+
"request_id": request_id,
|
|
286
|
+
"success": False,
|
|
287
|
+
"error": error,
|
|
288
|
+
},
|
|
289
|
+
topic="action-results",
|
|
290
|
+
correlation_id=correlation_id,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def _should_handle_request(self, target_tool: Optional[str]) -> bool:
|
|
294
|
+
"""Check if this tool should handle a request."""
|
|
295
|
+
if not target_tool:
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
# Match by name
|
|
299
|
+
if target_tool == self.name:
|
|
300
|
+
return True
|
|
301
|
+
|
|
302
|
+
# Match by agent_id
|
|
303
|
+
if target_tool == self.agent_id:
|
|
304
|
+
return True
|
|
305
|
+
|
|
306
|
+
# Match by capability (tool name might be a capability)
|
|
307
|
+
if target_tool in self.config.capabilities:
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
return False
|
|
311
|
+
|
|
312
|
+
async def invoke(
|
|
313
|
+
self,
|
|
314
|
+
operation: str,
|
|
315
|
+
data: Dict[str, Any],
|
|
316
|
+
) -> Dict[str, Any]:
|
|
317
|
+
"""
|
|
318
|
+
Programmatically invoke a tool operation.
|
|
319
|
+
|
|
320
|
+
This method allows invoking operations without going through the event bus,
|
|
321
|
+
useful for testing or direct integration.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
operation: Name of the operation
|
|
325
|
+
data: Input parameters
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
Operation result dictionary
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
ValueError: If no handler for operation
|
|
332
|
+
"""
|
|
333
|
+
handler = self._operation_handlers.get(operation)
|
|
334
|
+
if not handler:
|
|
335
|
+
raise ValueError(f"No handler for operation: {operation}")
|
|
336
|
+
|
|
337
|
+
request = ToolRequest(
|
|
338
|
+
operation=operation,
|
|
339
|
+
data=data,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return await handler(request, self.context)
|
|
343
|
+
|
|
344
|
+
async def invoke_remote(
|
|
345
|
+
self,
|
|
346
|
+
tool_name: str,
|
|
347
|
+
operation: str,
|
|
348
|
+
data: Dict[str, Any],
|
|
349
|
+
timeout: float = 30.0,
|
|
350
|
+
) -> Optional[Dict[str, Any]]:
|
|
351
|
+
"""
|
|
352
|
+
Invoke an operation on a remote tool.
|
|
353
|
+
|
|
354
|
+
This sends a tool.request event and waits for tool.response.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
tool_name: Name of the target tool
|
|
358
|
+
operation: Operation to invoke
|
|
359
|
+
data: Input parameters
|
|
360
|
+
timeout: Timeout in seconds
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Result dictionary if successful, None on timeout
|
|
364
|
+
"""
|
|
365
|
+
return await self.context.bus.request(
|
|
366
|
+
event_type="tool.request",
|
|
367
|
+
data={
|
|
368
|
+
"tool": tool_name,
|
|
369
|
+
"operation": operation,
|
|
370
|
+
"data": data,
|
|
371
|
+
},
|
|
372
|
+
timeout=timeout,
|
|
373
|
+
)
|