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/__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
|