jarviscore-framework 0.3.0__py3-none-any.whl → 0.3.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.
- examples/cloud_deployment_example.py +3 -3
- examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +6 -6
- examples/fastapi_integration_example.py +4 -4
- jarviscore/__init__.py +8 -11
- jarviscore/cli/smoketest.py +1 -1
- jarviscore/core/mesh.py +9 -0
- jarviscore/data/examples/cloud_deployment_example.py +3 -3
- jarviscore/data/examples/custom_profile_decorator.py +134 -0
- jarviscore/data/examples/custom_profile_wrap.py +168 -0
- jarviscore/data/examples/{listeneragent_cognitive_discovery_example.py → customagent_cognitive_discovery_example.py} +6 -6
- jarviscore/data/examples/fastapi_integration_example.py +4 -4
- jarviscore/docs/API_REFERENCE.md +32 -45
- jarviscore/docs/CHANGELOG.md +42 -0
- jarviscore/docs/CONFIGURATION.md +1 -1
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +246 -153
- jarviscore/docs/GETTING_STARTED.md +186 -329
- jarviscore/docs/TROUBLESHOOTING.md +1 -1
- jarviscore/docs/USER_GUIDE.md +8 -9
- jarviscore/integrations/fastapi.py +4 -4
- jarviscore/p2p/peer_client.py +29 -2
- jarviscore/profiles/__init__.py +2 -4
- jarviscore/profiles/customagent.py +295 -74
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.1.dist-info}/METADATA +61 -46
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.1.dist-info}/RECORD +30 -29
- tests/test_13_dx_improvements.py +37 -37
- tests/test_15_llm_cognitive_discovery.py +18 -18
- tests/test_16_unified_dx_flow.py +3 -3
- jarviscore/profiles/listeneragent.py +0 -292
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.1.dist-info}/WHEEL +0 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.1.dist-info}/licenses/LICENSE +0 -0
- {jarviscore_framework-0.3.0.dist-info → jarviscore_framework-0.3.1.dist-info}/top_level.txt +0 -0
jarviscore/docs/USER_GUIDE.md
CHANGED
|
@@ -146,8 +146,7 @@ mesh = Mesh(mode="distributed", config={'bind_port': 7950})
|
|
|
146
146
|
| Profile | Best For | How It Works |
|
|
147
147
|
|---------|----------|--------------|
|
|
148
148
|
| **AutoAgent** | Rapid prototyping | LLM generates + executes code from prompts |
|
|
149
|
-
| **CustomAgent** |
|
|
150
|
-
| **ListenerAgent** | API-first agents | Just implement `on_peer_request()` handlers |
|
|
149
|
+
| **CustomAgent** | Your own code | Implement `on_peer_request()` for P2P or `execute_task()` for workflows |
|
|
151
150
|
|
|
152
151
|
See [AutoAgent Guide](AUTOAGENT_GUIDE.md) and [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) for details.
|
|
153
152
|
|
|
@@ -227,7 +226,7 @@ asyncio.run(data_analyst_demo())
|
|
|
227
226
|
|
|
228
227
|
## Custom Profile Tutorial
|
|
229
228
|
|
|
230
|
-
|
|
229
|
+
Use **CustomAgent** profile.
|
|
231
230
|
|
|
232
231
|
See [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) for:
|
|
233
232
|
- Converting standalone agents to JarvisCore
|
|
@@ -546,10 +545,10 @@ Deploy agents as FastAPI services with minimal boilerplate:
|
|
|
546
545
|
|
|
547
546
|
```python
|
|
548
547
|
from fastapi import FastAPI, Request
|
|
549
|
-
from jarviscore.profiles import
|
|
548
|
+
from jarviscore.profiles import CustomAgent
|
|
550
549
|
from jarviscore.integrations.fastapi import JarvisLifespan
|
|
551
550
|
|
|
552
|
-
class ProcessorAgent(
|
|
551
|
+
class ProcessorAgent(CustomAgent):
|
|
553
552
|
role = "processor"
|
|
554
553
|
capabilities = ["processing"]
|
|
555
554
|
|
|
@@ -583,9 +582,9 @@ Deploy agents to containers without a central orchestrator:
|
|
|
583
582
|
```python
|
|
584
583
|
# In your container entrypoint
|
|
585
584
|
import asyncio
|
|
586
|
-
from jarviscore.profiles import
|
|
585
|
+
from jarviscore.profiles import CustomAgent
|
|
587
586
|
|
|
588
|
-
class MyAgent(
|
|
587
|
+
class MyAgent(CustomAgent):
|
|
589
588
|
role = "worker"
|
|
590
589
|
capabilities = ["processing"]
|
|
591
590
|
|
|
@@ -905,6 +904,6 @@ mesh = Mesh(config=config)
|
|
|
905
904
|
|
|
906
905
|
## Version
|
|
907
906
|
|
|
908
|
-
User Guide for JarvisCore v0.3.
|
|
907
|
+
User Guide for JarvisCore v0.3.1
|
|
909
908
|
|
|
910
|
-
Last Updated: 2026-
|
|
909
|
+
Last Updated: 2026-02-02
|
|
@@ -49,9 +49,9 @@ class JarvisLifespan:
|
|
|
49
49
|
Example - Single Agent:
|
|
50
50
|
from fastapi import FastAPI, Request
|
|
51
51
|
from jarviscore.integrations.fastapi import JarvisLifespan
|
|
52
|
-
from jarviscore.profiles import
|
|
52
|
+
from jarviscore.profiles import CustomAgent
|
|
53
53
|
|
|
54
|
-
class MyAgent(
|
|
54
|
+
class MyAgent(CustomAgent):
|
|
55
55
|
role = "processor"
|
|
56
56
|
capabilities = ["processing"]
|
|
57
57
|
|
|
@@ -220,9 +220,9 @@ def create_jarvis_app(
|
|
|
220
220
|
|
|
221
221
|
Example:
|
|
222
222
|
from jarviscore.integrations.fastapi import create_jarvis_app
|
|
223
|
-
from jarviscore.profiles import
|
|
223
|
+
from jarviscore.profiles import CustomAgent
|
|
224
224
|
|
|
225
|
-
class MyAgent(
|
|
225
|
+
class MyAgent(CustomAgent):
|
|
226
226
|
role = "processor"
|
|
227
227
|
capabilities = ["processing"]
|
|
228
228
|
|
jarviscore/p2p/peer_client.py
CHANGED
|
@@ -153,6 +153,7 @@ class PeerClient:
|
|
|
153
153
|
"""
|
|
154
154
|
results = []
|
|
155
155
|
|
|
156
|
+
# Search LOCAL agents first
|
|
156
157
|
if role:
|
|
157
158
|
agents = self._agent_registry.get(role, [])
|
|
158
159
|
for agent in agents:
|
|
@@ -166,7 +167,7 @@ class PeerClient:
|
|
|
166
167
|
))
|
|
167
168
|
|
|
168
169
|
elif capability:
|
|
169
|
-
# Search all agents for capability
|
|
170
|
+
# Search all local agents for capability
|
|
170
171
|
for role_name, agents in self._agent_registry.items():
|
|
171
172
|
for agent in agents:
|
|
172
173
|
if agent.agent_id != self._agent_id: # Exclude self
|
|
@@ -180,7 +181,7 @@ class PeerClient:
|
|
|
180
181
|
))
|
|
181
182
|
|
|
182
183
|
else:
|
|
183
|
-
# Return all peers
|
|
184
|
+
# Return all local peers
|
|
184
185
|
for role_name, agents in self._agent_registry.items():
|
|
185
186
|
for agent in agents:
|
|
186
187
|
if agent.agent_id != self._agent_id: # Exclude self
|
|
@@ -192,6 +193,32 @@ class PeerClient:
|
|
|
192
193
|
status="alive"
|
|
193
194
|
))
|
|
194
195
|
|
|
196
|
+
# BUG FIX: Also search REMOTE agents from other nodes
|
|
197
|
+
# Access coordinator's _remote_agent_registry
|
|
198
|
+
if self._coordinator and hasattr(self._coordinator, '_remote_agent_registry'):
|
|
199
|
+
remote_registry = self._coordinator._remote_agent_registry
|
|
200
|
+
|
|
201
|
+
for agent_id, info in remote_registry.items():
|
|
202
|
+
if agent_id == self._agent_id: # Exclude self
|
|
203
|
+
continue
|
|
204
|
+
|
|
205
|
+
# Filter by role if specified
|
|
206
|
+
if role and info.get('role') != role:
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
# Filter by capability if specified
|
|
210
|
+
if capability and capability not in info.get('capabilities', []):
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
# Add remote peer
|
|
214
|
+
results.append(PeerInfo(
|
|
215
|
+
agent_id=info['agent_id'],
|
|
216
|
+
role=info['role'],
|
|
217
|
+
capabilities=info.get('capabilities', []),
|
|
218
|
+
node_id=info.get('node_id', 'unknown'),
|
|
219
|
+
status="alive"
|
|
220
|
+
))
|
|
221
|
+
|
|
195
222
|
return results
|
|
196
223
|
|
|
197
224
|
@property
|
jarviscore/profiles/__init__.py
CHANGED
|
@@ -3,12 +3,10 @@ Execution profiles for agents.
|
|
|
3
3
|
|
|
4
4
|
Profiles define HOW agents execute tasks:
|
|
5
5
|
- AutoAgent: LLM-powered code generation + sandboxed execution
|
|
6
|
-
- CustomAgent: User-defined logic
|
|
7
|
-
- ListenerAgent: API-first agents with background P2P listening
|
|
6
|
+
- CustomAgent: User-defined logic with P2P message handling
|
|
8
7
|
"""
|
|
9
8
|
|
|
10
9
|
from .autoagent import AutoAgent
|
|
11
10
|
from .customagent import CustomAgent
|
|
12
|
-
from .listeneragent import ListenerAgent
|
|
13
11
|
|
|
14
|
-
__all__ = ["AutoAgent", "CustomAgent"
|
|
12
|
+
__all__ = ["AutoAgent", "CustomAgent"]
|
|
@@ -1,87 +1,130 @@
|
|
|
1
1
|
"""
|
|
2
|
-
CustomAgent - User-controlled execution profile.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
7
|
-
-
|
|
8
|
-
|
|
9
|
-
-
|
|
2
|
+
CustomAgent - User-controlled execution profile with P2P message handling.
|
|
3
|
+
|
|
4
|
+
Unified profile for building agents that:
|
|
5
|
+
- Handle P2P mesh communication (requests, notifications)
|
|
6
|
+
- Execute workflow tasks
|
|
7
|
+
- Integrate with HTTP APIs (FastAPI, Flask, etc.)
|
|
8
|
+
|
|
9
|
+
Example - Basic P2P Agent:
|
|
10
|
+
class AnalystAgent(CustomAgent):
|
|
11
|
+
role = "analyst"
|
|
12
|
+
capabilities = ["analysis"]
|
|
13
|
+
|
|
14
|
+
async def on_peer_request(self, msg):
|
|
15
|
+
result = await self.analyze(msg.data)
|
|
16
|
+
return {"status": "success", "result": result}
|
|
17
|
+
|
|
18
|
+
Example - With FastAPI:
|
|
19
|
+
from fastapi import FastAPI
|
|
20
|
+
from jarviscore.integrations.fastapi import JarvisLifespan
|
|
21
|
+
|
|
22
|
+
class ProcessorAgent(CustomAgent):
|
|
23
|
+
role = "processor"
|
|
24
|
+
capabilities = ["processing"]
|
|
25
|
+
|
|
26
|
+
async def on_peer_request(self, msg):
|
|
27
|
+
return {"result": await self.process(msg.data)}
|
|
28
|
+
|
|
29
|
+
app = FastAPI(lifespan=JarvisLifespan(ProcessorAgent(), mode="p2p"))
|
|
10
30
|
"""
|
|
11
|
-
from typing import Dict, Any
|
|
31
|
+
from typing import Dict, Any, Optional
|
|
32
|
+
import asyncio
|
|
33
|
+
import logging
|
|
34
|
+
|
|
12
35
|
from jarviscore.core.profile import Profile
|
|
13
36
|
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
14
39
|
|
|
15
40
|
class CustomAgent(Profile):
|
|
16
41
|
"""
|
|
17
|
-
|
|
42
|
+
User-controlled agent profile with P2P message handling.
|
|
43
|
+
|
|
44
|
+
For P2P messaging, implement these handlers:
|
|
45
|
+
on_peer_request(msg) - Handle requests, return response
|
|
46
|
+
on_peer_notify(msg) - Handle notifications (fire-and-forget)
|
|
47
|
+
on_error(error, msg) - Handle errors
|
|
18
48
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- setup(): Initialize custom framework/tools
|
|
23
|
-
- execute_task(): Custom execution logic
|
|
49
|
+
For workflow execution:
|
|
50
|
+
execute_task(task) - Handle workflow tasks directly
|
|
51
|
+
(defaults to delegating to on_peer_request)
|
|
24
52
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- State management (crash recovery, HITL)
|
|
29
|
-
- Cost tracking (if user provides token counts)
|
|
53
|
+
Configuration:
|
|
54
|
+
listen_timeout: Seconds to wait for messages (default: 1.0)
|
|
55
|
+
auto_respond: Auto-send on_peer_request return value (default: True)
|
|
30
56
|
|
|
31
|
-
Example
|
|
32
|
-
class
|
|
33
|
-
role = "
|
|
34
|
-
capabilities = ["
|
|
57
|
+
Example - P2P Agent:
|
|
58
|
+
class AnalystAgent(CustomAgent):
|
|
59
|
+
role = "analyst"
|
|
60
|
+
capabilities = ["analysis"]
|
|
61
|
+
|
|
62
|
+
async def on_peer_request(self, msg):
|
|
63
|
+
result = await self.analyze(msg.data)
|
|
64
|
+
return {"status": "success", "result": result}
|
|
65
|
+
|
|
66
|
+
Example - With LangChain:
|
|
67
|
+
class LangChainAgent(CustomAgent):
|
|
68
|
+
role = "assistant"
|
|
69
|
+
capabilities = ["chat"]
|
|
35
70
|
|
|
36
71
|
async def setup(self):
|
|
37
72
|
await super().setup()
|
|
38
73
|
from langchain.agents import Agent
|
|
39
74
|
self.lc_agent = Agent(...)
|
|
40
75
|
|
|
41
|
-
async def
|
|
42
|
-
result = await self.lc_agent.run(
|
|
76
|
+
async def on_peer_request(self, msg):
|
|
77
|
+
result = await self.lc_agent.run(msg.data["query"])
|
|
43
78
|
return {"status": "success", "output": result}
|
|
44
79
|
|
|
45
|
-
Example
|
|
80
|
+
Example - With MCP:
|
|
46
81
|
class MCPAgent(CustomAgent):
|
|
47
82
|
role = "tool_user"
|
|
48
83
|
capabilities = ["mcp_tools"]
|
|
49
|
-
mcp_server_url = "stdio://./server.py"
|
|
50
84
|
|
|
51
85
|
async def setup(self):
|
|
52
86
|
await super().setup()
|
|
53
87
|
from mcp import Client
|
|
54
|
-
self.mcp = Client(
|
|
88
|
+
self.mcp = Client("stdio://./server.py")
|
|
55
89
|
await self.mcp.connect()
|
|
56
90
|
|
|
57
|
-
async def
|
|
58
|
-
result = await self.mcp.call_tool("my_tool",
|
|
91
|
+
async def on_peer_request(self, msg):
|
|
92
|
+
result = await self.mcp.call_tool("my_tool", msg.data)
|
|
59
93
|
return {"status": "success", "data": result}
|
|
60
94
|
|
|
61
|
-
Example
|
|
62
|
-
|
|
95
|
+
Example - With FastAPI:
|
|
96
|
+
from fastapi import FastAPI
|
|
97
|
+
from jarviscore.integrations.fastapi import JarvisLifespan
|
|
98
|
+
|
|
99
|
+
class ProcessorAgent(CustomAgent):
|
|
63
100
|
role = "processor"
|
|
64
101
|
capabilities = ["data_processing"]
|
|
65
102
|
|
|
66
|
-
async def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
103
|
+
async def on_peer_request(self, msg):
|
|
104
|
+
if msg.data.get("action") == "process":
|
|
105
|
+
return {"result": await self.process(msg.data["payload"])}
|
|
106
|
+
return {"error": "unknown action"}
|
|
107
|
+
|
|
108
|
+
agent = ProcessorAgent()
|
|
109
|
+
app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p"))
|
|
110
|
+
|
|
111
|
+
@app.post("/process")
|
|
112
|
+
async def process_endpoint(data: dict, request: Request):
|
|
113
|
+
# HTTP endpoint - primary interface
|
|
114
|
+
agent = request.app.state.jarvis_agents["processor"]
|
|
115
|
+
return await agent.process(data)
|
|
71
116
|
"""
|
|
72
117
|
|
|
73
|
-
|
|
74
|
-
|
|
118
|
+
# Configuration - can be overridden in subclasses
|
|
119
|
+
listen_timeout: float = 1.0 # Seconds to wait for messages
|
|
120
|
+
auto_respond: bool = True # Automatically send response for requests
|
|
75
121
|
|
|
76
|
-
|
|
77
|
-
|
|
122
|
+
def __init__(self, agent_id: Optional[str] = None):
|
|
123
|
+
super().__init__(agent_id)
|
|
78
124
|
|
|
79
125
|
async def setup(self):
|
|
80
126
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
DAY 1: Base implementation (user overrides)
|
|
84
|
-
DAY 5+: Full examples with LangChain, MCP, etc.
|
|
127
|
+
Initialize agent resources. Override to add custom setup.
|
|
85
128
|
|
|
86
129
|
Example:
|
|
87
130
|
async def setup(self):
|
|
@@ -91,47 +134,225 @@ class CustomAgent(Profile):
|
|
|
91
134
|
self.agent = Agent(...)
|
|
92
135
|
"""
|
|
93
136
|
await super().setup()
|
|
94
|
-
|
|
95
137
|
self._logger.info(f"CustomAgent setup: {self.agent_id}")
|
|
96
|
-
|
|
97
|
-
|
|
138
|
+
|
|
139
|
+
# ─────────────────────────────────────────────────────────────────
|
|
140
|
+
# P2P Message Handling
|
|
141
|
+
# ─────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
async def run(self):
|
|
144
|
+
"""
|
|
145
|
+
Listener loop - receives and dispatches P2P messages.
|
|
146
|
+
|
|
147
|
+
Runs automatically in P2P mode. Dispatches messages to:
|
|
148
|
+
- on_peer_request() for request-response messages
|
|
149
|
+
- on_peer_notify() for fire-and-forget notifications
|
|
150
|
+
|
|
151
|
+
You typically don't need to override this. Just implement the handlers.
|
|
152
|
+
"""
|
|
153
|
+
self._logger.info(f"[{self.role}] Listener loop started")
|
|
154
|
+
|
|
155
|
+
while not self.shutdown_requested:
|
|
156
|
+
try:
|
|
157
|
+
# Wait for incoming message with timeout
|
|
158
|
+
# Timeout allows periodic shutdown_requested checks
|
|
159
|
+
msg = await self.peers.receive(timeout=self.listen_timeout)
|
|
160
|
+
|
|
161
|
+
if msg is None:
|
|
162
|
+
# Timeout - no message, continue loop to check shutdown
|
|
163
|
+
continue
|
|
164
|
+
|
|
165
|
+
# Dispatch to appropriate handler
|
|
166
|
+
await self._dispatch_message(msg)
|
|
167
|
+
|
|
168
|
+
except asyncio.CancelledError:
|
|
169
|
+
self._logger.debug(f"[{self.role}] Listener loop cancelled")
|
|
170
|
+
raise
|
|
171
|
+
except Exception as e:
|
|
172
|
+
self._logger.error(f"[{self.role}] Listener loop error: {e}")
|
|
173
|
+
await self.on_error(e, None)
|
|
174
|
+
|
|
175
|
+
self._logger.info(f"[{self.role}] Listener loop stopped")
|
|
176
|
+
|
|
177
|
+
async def _dispatch_message(self, msg):
|
|
178
|
+
"""
|
|
179
|
+
Dispatch message to appropriate handler based on message type.
|
|
180
|
+
|
|
181
|
+
Handles:
|
|
182
|
+
- REQUEST messages: calls on_peer_request, sends response if auto_respond=True
|
|
183
|
+
- NOTIFY messages: calls on_peer_notify
|
|
184
|
+
"""
|
|
185
|
+
from jarviscore.p2p.messages import MessageType
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
# Check if this is a request (expects response)
|
|
189
|
+
is_request = (
|
|
190
|
+
msg.type == MessageType.REQUEST or
|
|
191
|
+
getattr(msg, 'is_request', False) or
|
|
192
|
+
msg.correlation_id is not None
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if is_request:
|
|
196
|
+
# Request-response: call handler, optionally send response
|
|
197
|
+
response = await self.on_peer_request(msg)
|
|
198
|
+
|
|
199
|
+
if self.auto_respond and response is not None:
|
|
200
|
+
await self.peers.respond(msg, response)
|
|
201
|
+
self._logger.debug(
|
|
202
|
+
f"[{self.role}] Sent response to {msg.sender}"
|
|
203
|
+
)
|
|
204
|
+
else:
|
|
205
|
+
# Notification: fire-and-forget
|
|
206
|
+
await self.on_peer_notify(msg)
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
self._logger.error(
|
|
210
|
+
f"[{self.role}] Error handling message from {msg.sender}: {e}"
|
|
211
|
+
)
|
|
212
|
+
await self.on_error(e, msg)
|
|
213
|
+
|
|
214
|
+
# ─────────────────────────────────────────────────────────────────
|
|
215
|
+
# Message Handlers - Override in your agent
|
|
216
|
+
# ─────────────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
async def on_peer_request(self, msg) -> Any:
|
|
219
|
+
"""
|
|
220
|
+
Handle incoming peer request.
|
|
221
|
+
|
|
222
|
+
Override to process request-response messages from other agents.
|
|
223
|
+
The return value is automatically sent as response (if auto_respond=True).
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
msg: IncomingMessage with:
|
|
227
|
+
- msg.sender: Sender agent ID or role
|
|
228
|
+
- msg.data: Request payload (dict)
|
|
229
|
+
- msg.correlation_id: For response matching (handled automatically)
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Response data (dict) to send back to the requester.
|
|
233
|
+
Return None to skip sending a response.
|
|
234
|
+
|
|
235
|
+
Example:
|
|
236
|
+
async def on_peer_request(self, msg):
|
|
237
|
+
action = msg.data.get("action")
|
|
238
|
+
|
|
239
|
+
if action == "analyze":
|
|
240
|
+
result = await self.analyze(msg.data["payload"])
|
|
241
|
+
return {"status": "success", "result": result}
|
|
242
|
+
|
|
243
|
+
elif action == "status":
|
|
244
|
+
return {"status": "ok", "queue_size": self.queue_size}
|
|
245
|
+
|
|
246
|
+
return {"status": "error", "message": f"Unknown action: {action}"}
|
|
247
|
+
"""
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
async def on_peer_notify(self, msg) -> None:
|
|
251
|
+
"""
|
|
252
|
+
Handle incoming peer notification.
|
|
253
|
+
|
|
254
|
+
Override to process fire-and-forget messages from other agents.
|
|
255
|
+
No response is expected or sent.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
msg: IncomingMessage with:
|
|
259
|
+
- msg.sender: Sender agent ID or role
|
|
260
|
+
- msg.data: Notification payload (dict)
|
|
261
|
+
|
|
262
|
+
Example:
|
|
263
|
+
async def on_peer_notify(self, msg):
|
|
264
|
+
event = msg.data.get("event")
|
|
265
|
+
|
|
266
|
+
if event == "task_complete":
|
|
267
|
+
await self.update_dashboard(msg.data)
|
|
268
|
+
self._logger.info(f"Task completed by {msg.sender}")
|
|
269
|
+
|
|
270
|
+
elif event == "peer_joined":
|
|
271
|
+
self._logger.info(f"New peer in mesh: {msg.data.get('role')}")
|
|
272
|
+
"""
|
|
273
|
+
self._logger.debug(
|
|
274
|
+
f"[{self.role}] Received notify from {msg.sender}: "
|
|
275
|
+
f"{list(msg.data.keys()) if isinstance(msg.data, dict) else 'data'}"
|
|
98
276
|
)
|
|
99
277
|
|
|
278
|
+
async def on_error(self, error: Exception, msg=None) -> None:
|
|
279
|
+
"""
|
|
280
|
+
Handle errors during message processing.
|
|
281
|
+
|
|
282
|
+
Override to customize error handling (logging, alerting, metrics, etc.)
|
|
283
|
+
Default implementation logs the error and continues processing.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
error: The exception that occurred
|
|
287
|
+
msg: The message being processed when error occurred (may be None)
|
|
288
|
+
|
|
289
|
+
Example:
|
|
290
|
+
async def on_error(self, error, msg):
|
|
291
|
+
# Log with context
|
|
292
|
+
self._logger.error(
|
|
293
|
+
f"Error processing message: {error}",
|
|
294
|
+
extra={"sender": msg.sender if msg else None}
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Send to error tracking service
|
|
298
|
+
await self.error_tracker.capture(error, context={"msg": msg})
|
|
299
|
+
|
|
300
|
+
# Optionally notify the sender of failure
|
|
301
|
+
if msg and msg.correlation_id:
|
|
302
|
+
await self.peers.respond(msg, {
|
|
303
|
+
"status": "error",
|
|
304
|
+
"error": str(error)
|
|
305
|
+
})
|
|
306
|
+
"""
|
|
307
|
+
if msg:
|
|
308
|
+
self._logger.error(
|
|
309
|
+
f"[{self.role}] Error processing message from {msg.sender}: {error}"
|
|
310
|
+
)
|
|
311
|
+
else:
|
|
312
|
+
self._logger.error(f"[{self.role}] Error in listener loop: {error}")
|
|
313
|
+
|
|
314
|
+
# ─────────────────────────────────────────────────────────────────
|
|
315
|
+
# Workflow Compatibility
|
|
316
|
+
# ─────────────────────────────────────────────────────────────────
|
|
317
|
+
|
|
100
318
|
async def execute_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
|
|
101
319
|
"""
|
|
102
|
-
|
|
320
|
+
Execute a task (for workflow/distributed modes).
|
|
103
321
|
|
|
104
|
-
|
|
105
|
-
|
|
322
|
+
Default: Delegates to on_peer_request via synthetic message.
|
|
323
|
+
Override for custom workflow logic.
|
|
106
324
|
|
|
107
325
|
Args:
|
|
108
|
-
task: Task specification
|
|
326
|
+
task: Task specification dict
|
|
109
327
|
|
|
110
328
|
Returns:
|
|
111
|
-
Result
|
|
112
|
-
- status: "success" or "failure"
|
|
113
|
-
- output: Task result
|
|
114
|
-
- error (optional): Error message if failed
|
|
115
|
-
- tokens_used (optional): For cost tracking
|
|
116
|
-
- cost_usd (optional): For cost tracking
|
|
329
|
+
Result dict with status and output
|
|
117
330
|
|
|
118
331
|
Raises:
|
|
119
|
-
NotImplementedError:
|
|
120
|
-
|
|
121
|
-
Example:
|
|
122
|
-
async def execute_task(self, task):
|
|
123
|
-
result = await self.my_framework.run(task)
|
|
124
|
-
return {
|
|
125
|
-
"status": "success",
|
|
126
|
-
"output": result,
|
|
127
|
-
"tokens_used": 1000, # Optional
|
|
128
|
-
"cost_usd": 0.002 # Optional
|
|
129
|
-
}
|
|
332
|
+
NotImplementedError: If on_peer_request returns None and
|
|
333
|
+
execute_task is not overridden
|
|
130
334
|
"""
|
|
335
|
+
from jarviscore.p2p.messages import IncomingMessage, MessageType
|
|
336
|
+
|
|
337
|
+
# Create a synthetic message to pass to the handler
|
|
338
|
+
synthetic_msg = IncomingMessage(
|
|
339
|
+
sender="workflow",
|
|
340
|
+
sender_node="local",
|
|
341
|
+
type=MessageType.REQUEST,
|
|
342
|
+
data=task,
|
|
343
|
+
correlation_id=None,
|
|
344
|
+
timestamp=0
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
result = await self.on_peer_request(synthetic_msg)
|
|
348
|
+
|
|
349
|
+
if result is not None:
|
|
350
|
+
return {"status": "success", "output": result}
|
|
351
|
+
|
|
131
352
|
raise NotImplementedError(
|
|
132
|
-
f"{self.__class__.__name__} must implement execute_task()\n\n"
|
|
353
|
+
f"{self.__class__.__name__} must implement on_peer_request() or execute_task()\n\n"
|
|
133
354
|
f"Example:\n"
|
|
134
|
-
f" async def
|
|
135
|
-
f" result = await self.
|
|
136
|
-
f" return {{'status': 'success', '
|
|
355
|
+
f" async def on_peer_request(self, msg):\n"
|
|
356
|
+
f" result = await self.process(msg.data)\n"
|
|
357
|
+
f" return {{'status': 'success', 'result': result}}\n"
|
|
137
358
|
)
|