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 ADDED
@@ -0,0 +1,138 @@
1
+ """
2
+ Soorma Core - The Open Source Foundation for AI Agents.
3
+
4
+ The DisCo (Distributed Cognition) SDK for building production-grade AI agents.
5
+
6
+ Core Concepts:
7
+ - Agent: Base class for all autonomous agents
8
+ - Planner: Strategic reasoning engine that breaks goals into tasks
9
+ - Worker: Domain-specific cognitive node that executes tasks
10
+ - Tool: Atomic, stateless capability micro-service
11
+
12
+ Platform Context (available in all handlers):
13
+ - context.registry: Service discovery & capabilities
14
+ - context.memory: Distributed state management
15
+ - context.bus: Event choreography (pub/sub)
16
+ - context.tracker: Observability & state machines
17
+
18
+ Usage:
19
+ from soorma import Worker, PlatformContext
20
+
21
+ worker = Worker(
22
+ name="research-worker",
23
+ description="Searches and analyzes research papers",
24
+ capabilities=["paper_search", "citation_analysis"],
25
+ )
26
+
27
+ @worker.on_task("search_papers")
28
+ async def search_papers(task, context: PlatformContext):
29
+ # Access shared memory
30
+ prefs = await context.memory.retrieve(f"user:{task.session_id}:preferences")
31
+
32
+ # Perform the task
33
+ results = await search_papers(task.data["query"], prefs)
34
+
35
+ # Store results for other workers
36
+ await context.memory.store(f"results:{task.task_id}", results)
37
+
38
+ return {"papers": results}
39
+
40
+ worker.run()
41
+ """
42
+
43
+ __version__ = "0.3.0"
44
+
45
+ # Core imports
46
+ from .events import EventClient
47
+ from .context import (
48
+ PlatformContext,
49
+ RegistryClient,
50
+ MemoryClient,
51
+ BusClient,
52
+ TrackerClient,
53
+ )
54
+ from .agents import Agent, Planner, Worker, Tool
55
+ from .agents.planner import Goal, Plan, Task
56
+ from .agents.worker import TaskContext
57
+ from .agents.tool import ToolRequest, ToolResponse
58
+
59
+ # Public API
60
+ __all__ = [
61
+ # Version
62
+ "__version__",
63
+ # Core classes
64
+ "Agent",
65
+ "Planner",
66
+ "Worker",
67
+ "Tool",
68
+ # Context
69
+ "PlatformContext",
70
+ "RegistryClient",
71
+ "MemoryClient",
72
+ "BusClient",
73
+ "TrackerClient",
74
+ # Events
75
+ "EventClient",
76
+ # Data classes
77
+ "Goal",
78
+ "Plan",
79
+ "Task",
80
+ "TaskContext",
81
+ "ToolRequest",
82
+ "ToolResponse",
83
+ # Functions
84
+ "hello",
85
+ "event_handler",
86
+ ]
87
+
88
+
89
+ def hello():
90
+ """
91
+ Welcome to Soorma.
92
+
93
+ Displays version info and links to documentation.
94
+ """
95
+ print(f"Soorma Core v{__version__}")
96
+ print("=" * 50)
97
+ print("The DisCo (Distributed Cognition) SDK")
98
+ print("")
99
+ print("Domain Services (The Trinity):")
100
+ print(" • Planner: Strategic reasoning engine")
101
+ print(" • Worker: Domain-specific cognitive node")
102
+ print(" • Tool: Atomic, stateless capability")
103
+ print("")
104
+ print("Platform Context:")
105
+ print(" • context.registry - Service discovery")
106
+ print(" • context.memory - Distributed state")
107
+ print(" • context.bus - Event choreography")
108
+ print(" • context.tracker - Observability")
109
+ print("")
110
+ print("Quick Start:")
111
+ print(" soorma init my-agent # Create new agent")
112
+ print(" soorma dev # Start local stack")
113
+ print(" soorma deploy # Deploy to cloud")
114
+ print("")
115
+ print("Docs: https://soorma.ai")
116
+
117
+
118
+ def event_handler(event_type: str):
119
+ """
120
+ Decorator to register an event handler.
121
+
122
+ This is a legacy/convenience decorator. For new code, use:
123
+ - @agent.on_event("event.type") for generic agents
124
+ - @planner.on_goal("goal.type") for planners
125
+ - @worker.on_task("task_name") for workers
126
+ - @tool.on_invoke("operation") for tools
127
+
128
+ Usage:
129
+ @event_handler("example.request")
130
+ async def handle_example(event, context):
131
+ ...
132
+ """
133
+ from typing import Callable
134
+
135
+ def decorator(func: Callable) -> Callable:
136
+ func._soorma_event_type = event_type
137
+ return func
138
+ return decorator
@@ -0,0 +1,17 @@
1
+ """
2
+ Soorma Agents Module.
3
+
4
+ This module provides the base Agent class and the DisCo "Trinity":
5
+ - Planner: Strategic reasoning engine that breaks goals into tasks
6
+ - Worker: Domain-specific cognitive node that executes tasks
7
+ - Tool: Atomic, stateless capability for specific operations
8
+
9
+ All three extend the base Agent class but provide specialized interfaces
10
+ for their respective roles in the DisCo architecture.
11
+ """
12
+ from .base import Agent
13
+ from .planner import Planner
14
+ from .worker import Worker
15
+ from .tool import Tool
16
+
17
+ __all__ = ["Agent", "Planner", "Worker", "Tool"]
soorma/agents/base.py ADDED
@@ -0,0 +1,523 @@
1
+ """
2
+ Base Agent class for all Soorma agents.
3
+
4
+ The Agent class provides the foundation for all agent types in the DisCo architecture.
5
+ It handles:
6
+ - Platform context initialization (registry, memory, bus, tracker)
7
+ - Event subscription and publishing
8
+ - Agent lifecycle (startup, shutdown)
9
+ - Registry registration
10
+
11
+ Usage:
12
+ from soorma.agents import Agent
13
+
14
+ agent = Agent(
15
+ name="my-agent",
16
+ description="My custom agent",
17
+ capabilities=["data_analysis"],
18
+ )
19
+
20
+ @agent.on_startup
21
+ async def startup():
22
+ print("Agent starting...")
23
+
24
+ @agent.on_event("data.requested")
25
+ async def handle_data_request(event, context):
26
+ result = process_data(event["data"])
27
+ await context.bus.publish("data.completed", result)
28
+
29
+ agent.run()
30
+ """
31
+ import asyncio
32
+ import logging
33
+ import os
34
+ import signal
35
+ from abc import ABC
36
+ from dataclasses import dataclass, field
37
+ from typing import Any, Awaitable, Callable, Dict, List, Optional
38
+ from uuid import uuid4
39
+
40
+ from ..context import PlatformContext
41
+ from ..events import EventClient
42
+
43
+ logger = logging.getLogger(__name__)
44
+
45
+ # Type aliases
46
+ EventHandler = Callable[[Dict[str, Any], PlatformContext], Awaitable[None]]
47
+ LifecycleHandler = Callable[[], Awaitable[None]]
48
+
49
+
50
+ @dataclass
51
+ class AgentConfig:
52
+ """Configuration for an agent."""
53
+ # Core identity
54
+ agent_id: str
55
+ name: str
56
+ description: str
57
+ version: str
58
+ agent_type: str # "agent", "planner", "worker", "tool"
59
+
60
+ # Capabilities and events
61
+ capabilities: List[Any] = field(default_factory=list) # List[str] or List[AgentCapability]
62
+ events_consumed: List[str] = field(default_factory=list)
63
+ events_produced: List[str] = field(default_factory=list)
64
+
65
+ # Runtime settings
66
+ heartbeat_interval: float = 30.0 # seconds
67
+ auto_register: bool = True
68
+
69
+ # Platform URLs (from env or defaults)
70
+ registry_url: str = field(
71
+ default_factory=lambda: os.getenv("SOORMA_REGISTRY_URL", "http://localhost:8081")
72
+ )
73
+ event_service_url: str = field(
74
+ default_factory=lambda: os.getenv("SOORMA_EVENT_SERVICE_URL", "http://localhost:8082")
75
+ )
76
+ memory_url: str = field(
77
+ default_factory=lambda: os.getenv("SOORMA_MEMORY_URL", "http://localhost:8083")
78
+ )
79
+ tracker_url: str = field(
80
+ default_factory=lambda: os.getenv("SOORMA_TRACKER_URL", "http://localhost:8084")
81
+ )
82
+
83
+
84
+ class Agent(ABC):
85
+ """
86
+ Base class for all Soorma agents.
87
+
88
+ An Agent is an autonomous unit in the DisCo architecture that:
89
+ - Registers with the Soorma Registry on startup
90
+ - Subscribes to events it can handle
91
+ - Processes events using registered handlers
92
+ - Publishes result events
93
+
94
+ The base Agent class is framework-agnostic - you can use it directly
95
+ or through the specialized Planner, Worker, and Tool classes.
96
+
97
+ Attributes:
98
+ name: Human-readable name of the agent
99
+ description: What this agent does
100
+ version: Semantic version string
101
+ capabilities: List of capabilities this agent provides
102
+ config: Full agent configuration
103
+ context: Platform context (registry, memory, bus, tracker)
104
+
105
+ Usage:
106
+ agent = Agent(
107
+ name="data-processor",
108
+ description="Processes incoming data requests",
109
+ capabilities=["data_processing", "csv_parsing"],
110
+ )
111
+
112
+ @agent.on_event("data.requested")
113
+ async def handle_request(event, context):
114
+ # Process the event
115
+ result = await process(event["data"])
116
+ # Publish result
117
+ await context.bus.publish("data.completed", {"result": result})
118
+
119
+ agent.run()
120
+ """
121
+
122
+ def __init__(
123
+ self,
124
+ name: str,
125
+ description: str = "",
126
+ version: str = "0.1.0",
127
+ agent_type: str = "agent",
128
+ capabilities: Optional[List[str]] = None,
129
+ events_consumed: Optional[List[str]] = None,
130
+ events_produced: Optional[List[str]] = None,
131
+ agent_id: Optional[str] = None,
132
+ auto_register: bool = True,
133
+ ):
134
+ """
135
+ Initialize the agent.
136
+
137
+ Args:
138
+ name: Human-readable name
139
+ description: What this agent does
140
+ version: Semantic version (default: "0.1.0")
141
+ agent_type: Type of agent ("agent", "planner", "worker", "tool")
142
+ capabilities: List of capabilities provided
143
+ events_consumed: Event types this agent subscribes to
144
+ events_produced: Event types this agent publishes
145
+ agent_id: Unique ID (auto-generated if not provided)
146
+ auto_register: Whether to register with Registry on startup
147
+ """
148
+ self.config = AgentConfig(
149
+ agent_id=agent_id or f"{name}-{str(uuid4())[:8]}",
150
+ name=name,
151
+ description=description,
152
+ version=version,
153
+ agent_type=agent_type,
154
+ capabilities=capabilities or [],
155
+ events_consumed=events_consumed or [],
156
+ events_produced=events_produced or [],
157
+ auto_register=auto_register,
158
+ )
159
+
160
+ # Convenience accessors
161
+ self.name = name
162
+ self.description = description
163
+ self.version = version
164
+ self.capabilities = self.config.capabilities
165
+
166
+ # Platform context (initialized on run)
167
+ self._context: Optional[PlatformContext] = None
168
+
169
+ # Lifecycle handlers
170
+ self._startup_handlers: List[LifecycleHandler] = []
171
+ self._shutdown_handlers: List[LifecycleHandler] = []
172
+
173
+ # Event handlers: event_type -> handler
174
+ self._event_handlers: Dict[str, List[EventHandler]] = {}
175
+
176
+ # Runtime state
177
+ self._running = False
178
+ self._heartbeat_task: Optional[asyncio.Task] = None
179
+
180
+ @property
181
+ def agent_id(self) -> str:
182
+ """Get the unique agent ID."""
183
+ return self.config.agent_id
184
+
185
+ @property
186
+ def context(self) -> PlatformContext:
187
+ """
188
+ Get the platform context.
189
+
190
+ Raises:
191
+ RuntimeError: If accessed before agent.run() is called
192
+ """
193
+ if self._context is None:
194
+ raise RuntimeError("Platform context not initialized. Call agent.run() first.")
195
+ return self._context
196
+
197
+ # =========================================================================
198
+ # Decorators for registering handlers
199
+ # =========================================================================
200
+
201
+ def on_startup(self, func: LifecycleHandler) -> LifecycleHandler:
202
+ """
203
+ Decorator to register a startup handler.
204
+
205
+ Startup handlers are called after platform services connect
206
+ but before the agent starts processing events.
207
+
208
+ Usage:
209
+ @agent.on_startup
210
+ async def startup():
211
+ print("Agent starting...")
212
+ """
213
+ self._startup_handlers.append(func)
214
+ return func
215
+
216
+ def on_shutdown(self, func: LifecycleHandler) -> LifecycleHandler:
217
+ """
218
+ Decorator to register a shutdown handler.
219
+
220
+ Shutdown handlers are called when the agent is stopping,
221
+ before disconnecting from platform services.
222
+
223
+ Usage:
224
+ @agent.on_shutdown
225
+ async def shutdown():
226
+ print("Agent shutting down...")
227
+ """
228
+ self._shutdown_handlers.append(func)
229
+ return func
230
+
231
+ def on_event(self, event_type: str) -> Callable[[EventHandler], EventHandler]:
232
+ """
233
+ Decorator to register an event handler.
234
+
235
+ Handlers receive the event payload and the platform context.
236
+ Multiple handlers can be registered for the same event type.
237
+
238
+ Usage:
239
+ @agent.on_event("data.requested")
240
+ async def handle_request(event, context):
241
+ result = await process(event["data"])
242
+ await context.bus.publish("data.completed", result)
243
+
244
+ Args:
245
+ event_type: The event type to handle
246
+
247
+ Returns:
248
+ Decorator function
249
+ """
250
+ def decorator(func: EventHandler) -> EventHandler:
251
+ if event_type not in self._event_handlers:
252
+ self._event_handlers[event_type] = []
253
+ self._event_handlers[event_type].append(func)
254
+
255
+ # Track in consumed events
256
+ if event_type not in self.config.events_consumed:
257
+ self.config.events_consumed.append(event_type)
258
+
259
+ logger.debug(f"Registered handler for event: {event_type}")
260
+ return func
261
+ return decorator
262
+
263
+ # =========================================================================
264
+ # Event Publishing (convenience methods)
265
+ # =========================================================================
266
+
267
+ async def emit(
268
+ self,
269
+ event_type: str,
270
+ data: Dict[str, Any],
271
+ topic: Optional[str] = None,
272
+ correlation_id: Optional[str] = None,
273
+ ) -> str:
274
+ """
275
+ Emit an event to the event bus.
276
+
277
+ This is a convenience method that delegates to context.bus.publish().
278
+
279
+ Args:
280
+ event_type: Event type (e.g., "order.created")
281
+ data: Event payload
282
+ topic: Target topic (auto-inferred if not provided)
283
+ correlation_id: Optional correlation ID
284
+
285
+ Returns:
286
+ The event ID
287
+ """
288
+ return await self.context.bus.publish(
289
+ event_type=event_type,
290
+ data=data,
291
+ topic=topic,
292
+ correlation_id=correlation_id,
293
+ )
294
+
295
+ # =========================================================================
296
+ # Agent Lifecycle
297
+ # =========================================================================
298
+
299
+ async def _initialize_context(self) -> None:
300
+ """Initialize the platform context."""
301
+ logger.info(f"Initializing platform context for {self.name}")
302
+
303
+ # Create EventClient for bus
304
+ event_client = EventClient(
305
+ event_service_url=self.config.event_service_url,
306
+ agent_id=self.agent_id,
307
+ source=self.name,
308
+ )
309
+
310
+ # Register our event handlers with the EventClient
311
+ for event_type, handlers in self._event_handlers.items():
312
+ for handler in handlers:
313
+ @event_client.on_event(event_type)
314
+ async def wrapped_handler(event: Dict[str, Any], h=handler) -> None:
315
+ await h(event, self._context)
316
+
317
+ # Create context with configured clients
318
+ from ..context import RegistryClient, MemoryClient, BusClient, TrackerClient
319
+
320
+ self._context = PlatformContext(
321
+ registry=RegistryClient(base_url=self.config.registry_url),
322
+ memory=MemoryClient(base_url=self.config.memory_url),
323
+ bus=BusClient(event_client=event_client),
324
+ tracker=TrackerClient(base_url=self.config.tracker_url),
325
+ )
326
+
327
+ async def _register_with_registry(self) -> bool:
328
+ """Register the agent with the Registry service."""
329
+ if not self.config.auto_register:
330
+ return True
331
+
332
+ logger.info(f"Registering {self.name} with Registry")
333
+
334
+ success = await self.context.registry.register(
335
+ agent_id=self.agent_id,
336
+ name=self.name,
337
+ agent_type=self.config.agent_type,
338
+ capabilities=self.config.capabilities,
339
+ events_consumed=self.config.events_consumed,
340
+ events_produced=self.config.events_produced,
341
+ metadata={
342
+ "version": self.version,
343
+ "description": self.description,
344
+ },
345
+ )
346
+
347
+ if success:
348
+ logger.info(f"✓ Registered {self.name} ({self.agent_id})")
349
+ else:
350
+ logger.warning(f"⚠ Failed to register with Registry (continuing in offline mode)")
351
+
352
+ return success
353
+
354
+ async def _deregister_from_registry(self) -> None:
355
+ """Deregister from the Registry service."""
356
+ if not self.config.auto_register:
357
+ return
358
+
359
+ logger.info(f"Deregistering {self.name} from Registry")
360
+ await self.context.registry.deregister(self.agent_id)
361
+
362
+ async def _start_heartbeat(self) -> None:
363
+ """Start the heartbeat task."""
364
+ async def heartbeat_loop():
365
+ while self._running:
366
+ try:
367
+ await asyncio.sleep(self.config.heartbeat_interval)
368
+ if self._running:
369
+ await self.context.registry.heartbeat(self.agent_id)
370
+ except asyncio.CancelledError:
371
+ break
372
+ except Exception as e:
373
+ logger.debug(f"Heartbeat failed: {e}")
374
+
375
+ self._heartbeat_task = asyncio.create_task(heartbeat_loop())
376
+
377
+ async def _stop_heartbeat(self) -> None:
378
+ """Stop the heartbeat task."""
379
+ if self._heartbeat_task:
380
+ self._heartbeat_task.cancel()
381
+ try:
382
+ await self._heartbeat_task
383
+ except asyncio.CancelledError:
384
+ pass
385
+ self._heartbeat_task = None
386
+
387
+ async def _subscribe_to_events(self) -> None:
388
+ """Subscribe to event topics."""
389
+ if not self.config.events_consumed:
390
+ logger.info("No events to subscribe to")
391
+ return
392
+
393
+ # Derive topics from event types
394
+ topics = self._derive_topics(self.config.events_consumed)
395
+
396
+ logger.info(f"Subscribing to topics: {topics}")
397
+ await self.context.bus.subscribe(topics)
398
+
399
+ def _derive_topics(self, event_types: List[str]) -> List[str]:
400
+ """Derive topic patterns from event types."""
401
+ topics = set()
402
+ for event_type in event_types:
403
+ # Support wildcards in event types
404
+ if "*" in event_type:
405
+ topics.add(event_type)
406
+ elif event_type.endswith(".requested") or event_type.endswith(".request"):
407
+ topics.add("action-requests")
408
+ elif event_type.endswith(".completed") or event_type.endswith(".result"):
409
+ topics.add("action-results")
410
+ elif event_type.startswith("billing."):
411
+ topics.add("billing")
412
+ elif event_type.startswith("notification."):
413
+ topics.add("notifications")
414
+ else:
415
+ topics.add("business-facts")
416
+ return list(topics)
417
+
418
+ async def start(self) -> None:
419
+ """
420
+ Start the agent.
421
+
422
+ This method:
423
+ 1. Initializes the platform context
424
+ 2. Registers with the Registry
425
+ 3. Calls startup handlers
426
+ 4. Subscribes to events
427
+ 5. Starts the heartbeat
428
+
429
+ For blocking execution, use agent.run() instead.
430
+ """
431
+ if self._running:
432
+ logger.warning("Agent already running")
433
+ return
434
+
435
+ logger.info(f"🚀 Starting {self.name} v{self.version}")
436
+
437
+ # Initialize platform context
438
+ await self._initialize_context()
439
+
440
+ # Register with Registry
441
+ await self._register_with_registry()
442
+
443
+ # Call startup handlers
444
+ for handler in self._startup_handlers:
445
+ await handler()
446
+
447
+ # Subscribe to events
448
+ await self._subscribe_to_events()
449
+
450
+ # Start heartbeat
451
+ await self._start_heartbeat()
452
+
453
+ self._running = True
454
+ logger.info(f"✓ {self.name} is running")
455
+
456
+ async def stop(self) -> None:
457
+ """
458
+ Stop the agent gracefully.
459
+
460
+ This method:
461
+ 1. Stops the heartbeat
462
+ 2. Calls shutdown handlers
463
+ 3. Deregisters from Registry
464
+ 4. Closes platform connections
465
+ """
466
+ if not self._running:
467
+ return
468
+
469
+ logger.info(f"🛑 Stopping {self.name}")
470
+ self._running = False
471
+
472
+ # Stop heartbeat
473
+ await self._stop_heartbeat()
474
+
475
+ # Call shutdown handlers
476
+ for handler in self._shutdown_handlers:
477
+ try:
478
+ await handler()
479
+ except Exception as e:
480
+ logger.error(f"Shutdown handler error: {e}")
481
+
482
+ # Deregister
483
+ await self._deregister_from_registry()
484
+
485
+ # Close context
486
+ if self._context:
487
+ await self._context.close()
488
+ self._context = None
489
+
490
+ logger.info(f"👋 {self.name} stopped")
491
+
492
+ def run(self) -> None:
493
+ """
494
+ Start the agent and run until interrupted.
495
+
496
+ This is the main entry point for running an agent.
497
+ It handles signal handlers for graceful shutdown.
498
+
499
+ Usage:
500
+ if __name__ == "__main__":
501
+ agent.run()
502
+ """
503
+ async def _run():
504
+ # Setup signal handlers
505
+ loop = asyncio.get_event_loop()
506
+ stop_event = asyncio.Event()
507
+
508
+ def signal_handler():
509
+ stop_event.set()
510
+
511
+ for sig in (signal.SIGINT, signal.SIGTERM):
512
+ loop.add_signal_handler(sig, signal_handler)
513
+
514
+ try:
515
+ await self.start()
516
+ await stop_event.wait()
517
+ finally:
518
+ await self.stop()
519
+
520
+ try:
521
+ asyncio.run(_run())
522
+ except KeyboardInterrupt:
523
+ pass