jarviscore-framework 0.3.1__py3-none-any.whl → 0.3.2__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/customagent_cognitive_discovery_example.py +49 -8
- examples/customagent_distributed_example.py +140 -1
- examples/fastapi_integration_example.py +70 -7
- jarviscore/__init__.py +1 -1
- jarviscore/core/mesh.py +149 -0
- jarviscore/data/examples/customagent_cognitive_discovery_example.py +49 -8
- jarviscore/data/examples/customagent_distributed_example.py +140 -1
- jarviscore/data/examples/fastapi_integration_example.py +70 -7
- jarviscore/docs/API_REFERENCE.md +547 -5
- jarviscore/docs/CHANGELOG.md +89 -0
- jarviscore/docs/CONFIGURATION.md +1 -1
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +347 -2
- jarviscore/docs/TROUBLESHOOTING.md +1 -1
- jarviscore/docs/USER_GUIDE.md +286 -5
- jarviscore/p2p/coordinator.py +36 -7
- jarviscore/p2p/messages.py +13 -0
- jarviscore/p2p/peer_client.py +355 -23
- jarviscore/p2p/peer_tool.py +17 -11
- jarviscore/profiles/customagent.py +9 -2
- jarviscore/testing/__init__.py +35 -0
- jarviscore/testing/mocks.py +578 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/METADATA +2 -2
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/RECORD +31 -24
- tests/test_17_session_context.py +489 -0
- tests/test_18_mesh_diagnostics.py +465 -0
- tests/test_19_async_requests.py +516 -0
- tests/test_20_load_balancing.py +546 -0
- tests/test_21_mock_testing.py +776 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/WHEEL +0 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/licenses/LICENSE +0 -0
- {jarviscore_framework-0.3.1.dist-info → jarviscore_framework-0.3.2.dist-info}/top_level.txt +0 -0
|
@@ -1,25 +1,30 @@
|
|
|
1
1
|
"""
|
|
2
2
|
CustomAgent + Cognitive Discovery Example
|
|
3
3
|
|
|
4
|
-
Demonstrates
|
|
4
|
+
Demonstrates v0.3.0 and v0.3.2 features:
|
|
5
5
|
|
|
6
6
|
1. CustomAgent - Handler-based P2P agents (no run() loop needed)
|
|
7
7
|
- on_peer_request() handles incoming requests
|
|
8
8
|
- on_peer_notify() handles broadcast notifications
|
|
9
9
|
|
|
10
|
-
2. Cognitive Discovery - Dynamic peer awareness for LLMs
|
|
10
|
+
2. Cognitive Discovery (v0.3.0) - Dynamic peer awareness for LLMs
|
|
11
11
|
- get_cognitive_context() generates LLM-ready peer descriptions
|
|
12
12
|
- No hardcoded agent names in prompts
|
|
13
13
|
- LLM autonomously decides when to delegate
|
|
14
14
|
|
|
15
|
+
3. Session Context (v0.3.2) - Request tracking with metadata
|
|
16
|
+
- Pass context={mission_id, request_id} with peer requests
|
|
17
|
+
- Track requests across agent boundaries for debugging/tracing
|
|
18
|
+
|
|
15
19
|
Usage:
|
|
16
|
-
python examples/
|
|
20
|
+
python examples/customagent_cognitive_discovery_example.py
|
|
17
21
|
|
|
18
22
|
Prerequisites:
|
|
19
23
|
- .env file with CLAUDE_API_KEY (or other LLM provider)
|
|
20
24
|
"""
|
|
21
25
|
import asyncio
|
|
22
26
|
import sys
|
|
27
|
+
import uuid
|
|
23
28
|
from pathlib import Path
|
|
24
29
|
|
|
25
30
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
@@ -45,13 +50,21 @@ class AnalystAgent(CustomAgent):
|
|
|
45
50
|
async def on_peer_request(self, msg):
|
|
46
51
|
"""Handle incoming analysis requests."""
|
|
47
52
|
query = msg.data.get("question", msg.data.get("query", ""))
|
|
48
|
-
|
|
53
|
+
|
|
54
|
+
# v0.3.2: Access session context for request tracking
|
|
55
|
+
context = msg.context or {}
|
|
56
|
+
mission_id = context.get("mission_id", "unknown")
|
|
57
|
+
request_id = context.get("request_id", "unknown")
|
|
58
|
+
|
|
59
|
+
print(f"\n[Analyst] Received request (mission={mission_id[:8]}..., req={request_id[:8]}...)")
|
|
60
|
+
print(f"[Analyst] Query: {query[:50]}...")
|
|
49
61
|
|
|
50
62
|
# Simulate analysis (in real usage, this would use an LLM)
|
|
51
63
|
result = {
|
|
52
64
|
"analysis": f"Analysis of '{query}': The data shows positive trends.",
|
|
53
65
|
"confidence": 0.85,
|
|
54
|
-
"insights": ["Trend is upward", "Growth rate: 15%", "Recommendation: Continue"]
|
|
66
|
+
"insights": ["Trend is upward", "Growth rate: 15%", "Recommendation: Continue"],
|
|
67
|
+
"context": {"mission_id": mission_id, "request_id": request_id} # Echo back for tracing
|
|
55
68
|
}
|
|
56
69
|
|
|
57
70
|
print(f"[Analyst] Sending response with {len(result['insights'])} insights")
|
|
@@ -78,6 +91,9 @@ class CoordinatorAgent(CustomAgent):
|
|
|
78
91
|
async def setup(self):
|
|
79
92
|
await super().setup()
|
|
80
93
|
self.llm = self._create_llm_client()
|
|
94
|
+
# v0.3.2: Track missions for context propagation
|
|
95
|
+
self.mission_id = str(uuid.uuid4())
|
|
96
|
+
self.request_counter = 0
|
|
81
97
|
|
|
82
98
|
def _create_llm_client(self):
|
|
83
99
|
"""Create LLM client with fallback to mock."""
|
|
@@ -167,10 +183,22 @@ Never try to do analysis yourself - always delegate to the analyst."""
|
|
|
167
183
|
# Mock: simulate LLM deciding to delegate
|
|
168
184
|
if any(word in user_query.lower() for word in ["analyze", "analysis", "statistics", "data"]):
|
|
169
185
|
print("[Coordinator] Mock LLM decides to delegate to analyst")
|
|
186
|
+
|
|
187
|
+
# v0.3.2: Generate request context for tracking
|
|
188
|
+
self.request_counter += 1
|
|
189
|
+
request_context = {
|
|
190
|
+
"mission_id": self.mission_id,
|
|
191
|
+
"request_id": str(uuid.uuid4()),
|
|
192
|
+
"request_num": self.request_counter,
|
|
193
|
+
"source": "coordinator"
|
|
194
|
+
}
|
|
195
|
+
print(f"[Coordinator] Sending with context: mission={self.mission_id[:8]}...")
|
|
196
|
+
|
|
170
197
|
response = await self.peers.request(
|
|
171
198
|
"analyst",
|
|
172
199
|
{"question": user_query},
|
|
173
|
-
timeout=30
|
|
200
|
+
timeout=30,
|
|
201
|
+
context=request_context # v0.3.2: Pass context
|
|
174
202
|
)
|
|
175
203
|
return f"Based on the analyst's findings: {response.get('analysis', 'No response')}"
|
|
176
204
|
return f"I can help with: {user_query}"
|
|
@@ -261,12 +289,24 @@ Never try to do analysis yourself - always delegate to the analyst."""
|
|
|
261
289
|
role = args.get("role", "")
|
|
262
290
|
question = args.get("question", "")
|
|
263
291
|
|
|
292
|
+
# v0.3.2: Generate request context for tracking
|
|
293
|
+
self.request_counter += 1
|
|
294
|
+
request_context = {
|
|
295
|
+
"mission_id": self.mission_id,
|
|
296
|
+
"request_id": str(uuid.uuid4()),
|
|
297
|
+
"request_num": self.request_counter,
|
|
298
|
+
"source": "coordinator",
|
|
299
|
+
"tool": "ask_peer"
|
|
300
|
+
}
|
|
301
|
+
|
|
264
302
|
print(f"[Coordinator] Asking {role}: {question[:50]}...")
|
|
303
|
+
print(f"[Coordinator] Context: mission={self.mission_id[:8]}..., req_num={self.request_counter}")
|
|
265
304
|
|
|
266
305
|
response = await self.peers.request(
|
|
267
306
|
role,
|
|
268
307
|
{"question": question},
|
|
269
|
-
timeout=30
|
|
308
|
+
timeout=30,
|
|
309
|
+
context=request_context # v0.3.2: Pass context
|
|
270
310
|
)
|
|
271
311
|
|
|
272
312
|
return response
|
|
@@ -284,7 +324,8 @@ Never try to do analysis yourself - always delegate to the analyst."""
|
|
|
284
324
|
|
|
285
325
|
async def main():
|
|
286
326
|
print("=" * 60)
|
|
287
|
-
print("
|
|
327
|
+
print("CustomAgent + Cognitive Discovery + Session Context")
|
|
328
|
+
print("Features: v0.3.0 Cognitive Discovery, v0.3.2 Session Context")
|
|
288
329
|
print("=" * 60)
|
|
289
330
|
|
|
290
331
|
# Create mesh with both agents
|
|
@@ -6,6 +6,10 @@ Demonstrates CustomAgent in distributed mode, which combines:
|
|
|
6
6
|
- Workflow orchestration (step execution, dependencies)
|
|
7
7
|
- User-controlled execution logic (you write execute_task)
|
|
8
8
|
|
|
9
|
+
v0.3.2 Features Demonstrated:
|
|
10
|
+
- Async Requests (ask_async) - Non-blocking parallel requests to multiple agents
|
|
11
|
+
- Load Balancing (strategy="round_robin") - Distribute requests across agent instances
|
|
12
|
+
|
|
9
13
|
This is ideal for:
|
|
10
14
|
- Multi-node deployments with custom logic
|
|
11
15
|
- Integrating external frameworks (LangChain, CrewAI, etc.)
|
|
@@ -231,6 +235,7 @@ async def main():
|
|
|
231
235
|
"""Run CustomAgent distributed mode example."""
|
|
232
236
|
print("\n" + "="*70)
|
|
233
237
|
print("JarvisCore: CustomAgent in Distributed Mode")
|
|
238
|
+
print("v0.3.2: Also supports --async and --load-balance demos")
|
|
234
239
|
print("="*70)
|
|
235
240
|
|
|
236
241
|
# ─────────────────────────────────────────────────────────────────────────
|
|
@@ -358,5 +363,139 @@ async def peer_communication_example():
|
|
|
358
363
|
pass
|
|
359
364
|
|
|
360
365
|
|
|
366
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
367
|
+
# v0.3.2 FEATURES: ASYNC REQUESTS & LOAD BALANCING
|
|
368
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
369
|
+
|
|
370
|
+
async def async_requests_demo():
|
|
371
|
+
"""
|
|
372
|
+
Demonstrate v0.3.2 async requests for parallel agent communication.
|
|
373
|
+
|
|
374
|
+
ask_async() returns a Future that can be awaited later, enabling:
|
|
375
|
+
- Fire multiple requests in parallel
|
|
376
|
+
- Continue other work while waiting
|
|
377
|
+
- Gather results when needed
|
|
378
|
+
"""
|
|
379
|
+
print("\n" + "="*70)
|
|
380
|
+
print("v0.3.2 Feature: Async Requests (ask_async)")
|
|
381
|
+
print("="*70)
|
|
382
|
+
|
|
383
|
+
mesh = Mesh(mode="p2p", config={"bind_port": 7966})
|
|
384
|
+
|
|
385
|
+
# Add multiple agents
|
|
386
|
+
mesh.add(ContentResearcherAgent)
|
|
387
|
+
mesh.add(ContentWriterAgent)
|
|
388
|
+
mesh.add(ContentReviewerAgent)
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
await mesh.start()
|
|
392
|
+
|
|
393
|
+
# Get an agent with peer access
|
|
394
|
+
researcher = next((a for a in mesh.agents if a.role == "content_researcher"), None)
|
|
395
|
+
if not researcher or not researcher.peers:
|
|
396
|
+
print("Peers not available")
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
print("\n[Demo] Firing parallel requests to multiple agents...")
|
|
400
|
+
|
|
401
|
+
# v0.3.2: ask_async returns a Future - doesn't block!
|
|
402
|
+
future1 = researcher.peers.ask_async(
|
|
403
|
+
"content_writer",
|
|
404
|
+
{"question": "What makes good technical writing?"}
|
|
405
|
+
)
|
|
406
|
+
future2 = researcher.peers.ask_async(
|
|
407
|
+
"content_reviewer",
|
|
408
|
+
{"question": "What are common writing mistakes?"}
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
print("[Demo] Requests sent! Doing other work while waiting...")
|
|
412
|
+
await asyncio.sleep(0.1) # Simulate other work
|
|
413
|
+
|
|
414
|
+
# Gather results when ready
|
|
415
|
+
print("[Demo] Gathering results...")
|
|
416
|
+
results = await asyncio.gather(future1, future2, return_exceptions=True)
|
|
417
|
+
|
|
418
|
+
for i, result in enumerate(results):
|
|
419
|
+
if isinstance(result, Exception):
|
|
420
|
+
print(f" Request {i+1}: Error - {result}")
|
|
421
|
+
else:
|
|
422
|
+
print(f" Request {i+1}: Got response")
|
|
423
|
+
|
|
424
|
+
print("\n[Demo] Async requests complete!")
|
|
425
|
+
|
|
426
|
+
finally:
|
|
427
|
+
await mesh.stop()
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
async def load_balancing_demo():
|
|
431
|
+
"""
|
|
432
|
+
Demonstrate v0.3.2 load balancing strategies.
|
|
433
|
+
|
|
434
|
+
When multiple agents have the same capability, use strategy parameter:
|
|
435
|
+
- "random" (default): Random selection
|
|
436
|
+
- "round_robin": Distribute evenly across instances
|
|
437
|
+
"""
|
|
438
|
+
print("\n" + "="*70)
|
|
439
|
+
print("v0.3.2 Feature: Load Balancing Strategies")
|
|
440
|
+
print("="*70)
|
|
441
|
+
|
|
442
|
+
mesh = Mesh(mode="p2p", config={"bind_port": 7967})
|
|
443
|
+
|
|
444
|
+
# Add agents
|
|
445
|
+
mesh.add(ContentResearcherAgent)
|
|
446
|
+
mesh.add(ContentWriterAgent)
|
|
447
|
+
|
|
448
|
+
try:
|
|
449
|
+
await mesh.start()
|
|
450
|
+
|
|
451
|
+
researcher = next((a for a in mesh.agents if a.role == "content_researcher"), None)
|
|
452
|
+
if not researcher or not researcher.peers:
|
|
453
|
+
print("Peers not available")
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
print("\n[Demo] Load balancing with strategy='round_robin'")
|
|
457
|
+
print("[Demo] Sending 3 requests to 'writing' capability...")
|
|
458
|
+
|
|
459
|
+
# v0.3.2: Use discover_one() with strategy for load balancing
|
|
460
|
+
for i in range(3):
|
|
461
|
+
# round_robin distributes requests evenly across matching peers
|
|
462
|
+
# First, discover which peer to use with the strategy
|
|
463
|
+
target = researcher.peers.discover_one(
|
|
464
|
+
role="content_writer",
|
|
465
|
+
strategy="round_robin" # v0.3.2: Load balancing
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
if target:
|
|
469
|
+
# Then make the request to that specific peer
|
|
470
|
+
response = await researcher.peers.request(
|
|
471
|
+
target.role,
|
|
472
|
+
{"question": f"Request #{i+1}"},
|
|
473
|
+
timeout=10
|
|
474
|
+
)
|
|
475
|
+
print(f" Request {i+1}: Handled by {target.agent_id[:8]}...")
|
|
476
|
+
else:
|
|
477
|
+
print(f" Request {i+1}: No peer found")
|
|
478
|
+
|
|
479
|
+
print("\n[Demo] Load balancing complete!")
|
|
480
|
+
print("[Demo] In a multi-node setup with multiple writers,")
|
|
481
|
+
print(" round_robin would distribute across all instances.")
|
|
482
|
+
|
|
483
|
+
finally:
|
|
484
|
+
await mesh.stop()
|
|
485
|
+
|
|
486
|
+
|
|
361
487
|
if __name__ == "__main__":
|
|
362
|
-
|
|
488
|
+
import sys
|
|
489
|
+
|
|
490
|
+
if len(sys.argv) > 1:
|
|
491
|
+
if sys.argv[1] == "--async":
|
|
492
|
+
asyncio.run(async_requests_demo())
|
|
493
|
+
elif sys.argv[1] == "--load-balance":
|
|
494
|
+
asyncio.run(load_balancing_demo())
|
|
495
|
+
else:
|
|
496
|
+
print("Usage:")
|
|
497
|
+
print(" python customagent_distributed_example.py # Main workflow demo")
|
|
498
|
+
print(" python customagent_distributed_example.py --async # Async requests demo")
|
|
499
|
+
print(" python customagent_distributed_example.py --load-balance # Load balancing demo")
|
|
500
|
+
else:
|
|
501
|
+
asyncio.run(main())
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
FastAPI Integration Example (v0.3.0)
|
|
2
|
+
FastAPI Integration Example (v0.3.0 + v0.3.2)
|
|
3
3
|
|
|
4
4
|
Demonstrates JarvisLifespan for 3-line FastAPI integration with autonomous agents.
|
|
5
5
|
|
|
@@ -8,6 +8,8 @@ Features shown:
|
|
|
8
8
|
2. CustomAgent - API-first agents with on_peer_request handlers
|
|
9
9
|
3. Cognitive Discovery - get_cognitive_context() for LLM awareness
|
|
10
10
|
4. Autonomous Agents - Each agent has MESH as a TOOL, LLM decides when to delegate
|
|
11
|
+
5. Mesh Diagnostics (v0.3.2) - /health endpoint using get_diagnostics()
|
|
12
|
+
6. Session Context (v0.3.2) - Request tracking with context parameter
|
|
11
13
|
|
|
12
14
|
Real-World Flow:
|
|
13
15
|
HTTP Request → Agent A (with LLM) → LLM sees peers as tools
|
|
@@ -22,6 +24,9 @@ Usage:
|
|
|
22
24
|
-H "Content-Type: application/json" \
|
|
23
25
|
-d '{"message": "Analyze the Q4 sales trends"}'
|
|
24
26
|
|
|
27
|
+
# Check mesh health (v0.3.2)
|
|
28
|
+
curl http://localhost:8000/health
|
|
29
|
+
|
|
25
30
|
# Optional: Start a standalone agent that joins the mesh (in another terminal)
|
|
26
31
|
python examples/fastapi_integration_example.py --join-as scout
|
|
27
32
|
|
|
@@ -32,6 +37,7 @@ Prerequisites:
|
|
|
32
37
|
import asyncio
|
|
33
38
|
import sys
|
|
34
39
|
import os
|
|
40
|
+
import uuid
|
|
35
41
|
from pathlib import Path
|
|
36
42
|
|
|
37
43
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
@@ -104,14 +110,27 @@ class LLMAgent(CustomAgent):
|
|
|
104
110
|
}
|
|
105
111
|
}]
|
|
106
112
|
|
|
107
|
-
async def _ask_peer(self, role: str, question: str) -> dict:
|
|
113
|
+
async def _ask_peer(self, role: str, question: str, request_id: str = None) -> dict:
|
|
108
114
|
"""Execute ask_peer tool - send request to another agent."""
|
|
109
115
|
print(f"[{self.role}] Asking {role}: {question[:50]}...")
|
|
110
|
-
|
|
116
|
+
|
|
117
|
+
# v0.3.2: Pass context for request tracking
|
|
118
|
+
context = {
|
|
119
|
+
"request_id": request_id or str(uuid.uuid4()),
|
|
120
|
+
"source_agent": self.role,
|
|
121
|
+
"tool": "ask_peer"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
response = await self.peers.request(
|
|
125
|
+
role,
|
|
126
|
+
{"question": question},
|
|
127
|
+
timeout=30,
|
|
128
|
+
context=context # v0.3.2: Session context
|
|
129
|
+
)
|
|
111
130
|
print(f"[{self.role}] Got response from {role}")
|
|
112
131
|
return response
|
|
113
132
|
|
|
114
|
-
async def chat(self, message: str) -> dict:
|
|
133
|
+
async def chat(self, message: str, request_id: str = None) -> dict:
|
|
115
134
|
"""
|
|
116
135
|
Process a message with LLM that can discover and delegate to peers.
|
|
117
136
|
|
|
@@ -119,7 +138,14 @@ class LLMAgent(CustomAgent):
|
|
|
119
138
|
1. Build system prompt with WHO I AM + WHO ELSE IS AVAILABLE
|
|
120
139
|
2. LLM sees available peers as potential helpers
|
|
121
140
|
3. LLM decides whether to handle directly or delegate
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
message: The user message to process
|
|
144
|
+
request_id: Optional request ID for tracking (v0.3.2)
|
|
122
145
|
"""
|
|
146
|
+
# v0.3.2: Generate request_id for tracking if not provided
|
|
147
|
+
request_id = request_id or str(uuid.uuid4())
|
|
148
|
+
|
|
123
149
|
if not self.llm:
|
|
124
150
|
return await self._chat_mock(message)
|
|
125
151
|
|
|
@@ -154,7 +180,8 @@ class LLMAgent(CustomAgent):
|
|
|
154
180
|
role = tool_use_block.input.get("role")
|
|
155
181
|
question = tool_use_block.input.get("question")
|
|
156
182
|
|
|
157
|
-
|
|
183
|
+
# v0.3.2: Pass request_id for tracing
|
|
184
|
+
peer_response = await self._ask_peer(role, question, request_id=request_id)
|
|
158
185
|
|
|
159
186
|
# Continue with tool result
|
|
160
187
|
messages = [{"role": "user", "content": message}]
|
|
@@ -467,6 +494,36 @@ def create_app():
|
|
|
467
494
|
}
|
|
468
495
|
return result
|
|
469
496
|
|
|
497
|
+
@app.get("/health")
|
|
498
|
+
async def health_check(request: Request):
|
|
499
|
+
"""
|
|
500
|
+
Health check endpoint using mesh diagnostics (v0.3.2).
|
|
501
|
+
|
|
502
|
+
Returns mesh status, connected agents, and network health.
|
|
503
|
+
Useful for Kubernetes probes, load balancer checks, and monitoring.
|
|
504
|
+
"""
|
|
505
|
+
# Get the mesh from JarvisLifespan state
|
|
506
|
+
mesh = getattr(request.app.state, "jarvis_mesh", None)
|
|
507
|
+
if not mesh:
|
|
508
|
+
return JSONResponse(
|
|
509
|
+
status_code=503,
|
|
510
|
+
content={"status": "unhealthy", "error": "Mesh not initialized"}
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# v0.3.2: Use get_diagnostics() for comprehensive health info
|
|
514
|
+
diagnostics = mesh.get_diagnostics()
|
|
515
|
+
|
|
516
|
+
return {
|
|
517
|
+
"status": "healthy",
|
|
518
|
+
"mesh": {
|
|
519
|
+
"mode": diagnostics.get("mode", "unknown"),
|
|
520
|
+
"agent_count": diagnostics.get("agent_count", 0),
|
|
521
|
+
"agents": diagnostics.get("agents", []),
|
|
522
|
+
},
|
|
523
|
+
"network": diagnostics.get("network", {}),
|
|
524
|
+
"uptime_seconds": diagnostics.get("uptime_seconds", 0)
|
|
525
|
+
}
|
|
526
|
+
|
|
470
527
|
@app.post("/chat")
|
|
471
528
|
async def chat(request: Request):
|
|
472
529
|
"""
|
|
@@ -476,16 +533,21 @@ def create_app():
|
|
|
476
533
|
1. Sees other agents via get_cognitive_context()
|
|
477
534
|
2. Decides if it needs to delegate
|
|
478
535
|
3. Uses ask_peer tool to communicate
|
|
536
|
+
|
|
537
|
+
v0.3.2: Supports request_id for tracking across agent boundaries.
|
|
479
538
|
"""
|
|
480
539
|
body = await request.json()
|
|
481
540
|
message = body.get("message", "")
|
|
482
541
|
|
|
542
|
+
# v0.3.2: Generate request_id for tracking
|
|
543
|
+
request_id = str(uuid.uuid4())
|
|
544
|
+
|
|
483
545
|
assistant = request.app.state.jarvis_agents.get("assistant")
|
|
484
546
|
if not assistant:
|
|
485
547
|
return JSONResponse(status_code=503, content={"error": "Assistant not available"})
|
|
486
548
|
|
|
487
|
-
result = await assistant.chat(message)
|
|
488
|
-
return {"message": message, **result}
|
|
549
|
+
result = await assistant.chat(message, request_id=request_id)
|
|
550
|
+
return {"message": message, "request_id": request_id, **result}
|
|
489
551
|
|
|
490
552
|
@app.post("/ask/{agent_role}")
|
|
491
553
|
async def ask_agent(agent_role: str, request: Request):
|
|
@@ -551,6 +613,7 @@ def main():
|
|
|
551
613
|
print(" - LLM decides when to delegate autonomously")
|
|
552
614
|
print(" - Standalone agents can join with --join-as flag")
|
|
553
615
|
print("\nEndpoints:")
|
|
616
|
+
print(" GET /health - Mesh health diagnostics (v0.3.2)")
|
|
554
617
|
print(" GET /agents - Show what each agent sees")
|
|
555
618
|
print(" POST /chat - Chat with assistant (may delegate)")
|
|
556
619
|
print(" POST /ask/{role} - Ask specific agent directly")
|
jarviscore/__init__.py
CHANGED
jarviscore/core/mesh.py
CHANGED
|
@@ -624,6 +624,155 @@ class Mesh:
|
|
|
624
624
|
"""
|
|
625
625
|
return self._capability_index.get(capability, [])
|
|
626
626
|
|
|
627
|
+
# ─────────────────────────────────────────────────────────────────
|
|
628
|
+
# DIAGNOSTICS
|
|
629
|
+
# ─────────────────────────────────────────────────────────────────
|
|
630
|
+
|
|
631
|
+
def get_diagnostics(self) -> Dict[str, Any]:
|
|
632
|
+
"""
|
|
633
|
+
Get diagnostic information about the mesh and P2P connectivity.
|
|
634
|
+
|
|
635
|
+
Useful for debugging P2P issues, monitoring mesh health,
|
|
636
|
+
and understanding the current state of the distributed system.
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
Dictionary containing:
|
|
640
|
+
- local_node: This node's configuration and status
|
|
641
|
+
- known_peers: List of discovered remote peers
|
|
642
|
+
- local_agents: List of local agents with capabilities
|
|
643
|
+
- connectivity_status: Overall health assessment
|
|
644
|
+
- keepalive_status: Keepalive manager health (if P2P enabled)
|
|
645
|
+
- swim_status: SWIM protocol status (if P2P enabled)
|
|
646
|
+
- capability_map: Mapping of capabilities to agent IDs
|
|
647
|
+
|
|
648
|
+
Example:
|
|
649
|
+
diagnostics = mesh.get_diagnostics()
|
|
650
|
+
print(f"Status: {diagnostics['connectivity_status']}")
|
|
651
|
+
for peer in diagnostics['known_peers']:
|
|
652
|
+
print(f" {peer['role']} at {peer['node_id']}: {peer['status']}")
|
|
653
|
+
"""
|
|
654
|
+
result = {
|
|
655
|
+
"local_node": self._get_local_node_info(),
|
|
656
|
+
"known_peers": self._get_peer_list(),
|
|
657
|
+
"local_agents": self._get_local_agents_info(),
|
|
658
|
+
"connectivity_status": self._assess_connectivity_status()
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
# Add P2P-specific diagnostics if coordinator is available
|
|
662
|
+
if self._p2p_coordinator:
|
|
663
|
+
result["keepalive_status"] = self._get_keepalive_status()
|
|
664
|
+
result["swim_status"] = self._get_swim_status()
|
|
665
|
+
result["capability_map"] = self._get_capability_map()
|
|
666
|
+
|
|
667
|
+
return result
|
|
668
|
+
|
|
669
|
+
def _get_local_node_info(self) -> Dict[str, Any]:
|
|
670
|
+
"""Get local node information."""
|
|
671
|
+
info = {
|
|
672
|
+
"mode": self.mode.value,
|
|
673
|
+
"started": self._started,
|
|
674
|
+
"agent_count": len(self.agents)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if self._p2p_coordinator and self._p2p_coordinator.swim_manager:
|
|
678
|
+
addr = self._p2p_coordinator.swim_manager.bind_addr
|
|
679
|
+
if addr:
|
|
680
|
+
info["bind_address"] = f"{addr[0]}:{addr[1]}"
|
|
681
|
+
|
|
682
|
+
return info
|
|
683
|
+
|
|
684
|
+
def _get_peer_list(self) -> List[Dict[str, Any]]:
|
|
685
|
+
"""Get list of known remote peers."""
|
|
686
|
+
peers = []
|
|
687
|
+
|
|
688
|
+
if self._p2p_coordinator:
|
|
689
|
+
for agent in self._p2p_coordinator.list_remote_agents():
|
|
690
|
+
peers.append({
|
|
691
|
+
"role": agent.get("role", "unknown"),
|
|
692
|
+
"agent_id": agent.get("agent_id", "unknown"),
|
|
693
|
+
"node_id": agent.get("node_id", "unknown"),
|
|
694
|
+
"capabilities": agent.get("capabilities", []),
|
|
695
|
+
"status": "connected"
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
return peers
|
|
699
|
+
|
|
700
|
+
def _get_local_agents_info(self) -> List[Dict[str, Any]]:
|
|
701
|
+
"""Get information about local agents."""
|
|
702
|
+
return [
|
|
703
|
+
{
|
|
704
|
+
"role": agent.role,
|
|
705
|
+
"agent_id": agent.agent_id,
|
|
706
|
+
"capabilities": list(agent.capabilities),
|
|
707
|
+
"description": getattr(agent, 'description', ''),
|
|
708
|
+
"has_peers": hasattr(agent, 'peers') and agent.peers is not None
|
|
709
|
+
}
|
|
710
|
+
for agent in self.agents
|
|
711
|
+
]
|
|
712
|
+
|
|
713
|
+
def _assess_connectivity_status(self) -> str:
|
|
714
|
+
"""
|
|
715
|
+
Assess overall connectivity status.
|
|
716
|
+
|
|
717
|
+
Returns:
|
|
718
|
+
"healthy" - P2P fully operational with peers
|
|
719
|
+
"isolated" - No peers connected
|
|
720
|
+
"degraded" - Some connectivity issues detected
|
|
721
|
+
"not_started" - Mesh not yet started
|
|
722
|
+
"local_only" - Not in distributed/p2p mode
|
|
723
|
+
"""
|
|
724
|
+
if not self._started:
|
|
725
|
+
return "not_started"
|
|
726
|
+
|
|
727
|
+
if self.mode == MeshMode.AUTONOMOUS:
|
|
728
|
+
return "local_only"
|
|
729
|
+
|
|
730
|
+
if not self._p2p_coordinator:
|
|
731
|
+
return "local_only"
|
|
732
|
+
|
|
733
|
+
# Check SWIM health
|
|
734
|
+
if self._p2p_coordinator.swim_manager:
|
|
735
|
+
if not self._p2p_coordinator.swim_manager.is_healthy():
|
|
736
|
+
return "degraded"
|
|
737
|
+
|
|
738
|
+
# Check for connected peers
|
|
739
|
+
remote_agents = self._p2p_coordinator.list_remote_agents()
|
|
740
|
+
if not remote_agents:
|
|
741
|
+
return "isolated"
|
|
742
|
+
|
|
743
|
+
# Check keepalive health if available
|
|
744
|
+
if hasattr(self._p2p_coordinator, 'keepalive_manager') and self._p2p_coordinator.keepalive_manager:
|
|
745
|
+
health = self._p2p_coordinator.keepalive_manager.get_health_status()
|
|
746
|
+
if health.get('circuit_state') == 'OPEN':
|
|
747
|
+
return "degraded"
|
|
748
|
+
|
|
749
|
+
return "healthy"
|
|
750
|
+
|
|
751
|
+
def _get_keepalive_status(self) -> Optional[Dict[str, Any]]:
|
|
752
|
+
"""Get keepalive manager status."""
|
|
753
|
+
if not self._p2p_coordinator:
|
|
754
|
+
return None
|
|
755
|
+
|
|
756
|
+
if hasattr(self._p2p_coordinator, 'keepalive_manager') and self._p2p_coordinator.keepalive_manager:
|
|
757
|
+
return self._p2p_coordinator.keepalive_manager.get_health_status()
|
|
758
|
+
|
|
759
|
+
return None
|
|
760
|
+
|
|
761
|
+
def _get_swim_status(self) -> Optional[Dict[str, Any]]:
|
|
762
|
+
"""Get SWIM protocol status."""
|
|
763
|
+
if not self._p2p_coordinator or not self._p2p_coordinator.swim_manager:
|
|
764
|
+
return None
|
|
765
|
+
|
|
766
|
+
return self._p2p_coordinator.swim_manager.get_status()
|
|
767
|
+
|
|
768
|
+
def _get_capability_map(self) -> Dict[str, List[str]]:
|
|
769
|
+
"""Get the capability to agent_id mapping."""
|
|
770
|
+
if not self._p2p_coordinator:
|
|
771
|
+
return {}
|
|
772
|
+
|
|
773
|
+
# Convert defaultdict to regular dict for serialization
|
|
774
|
+
return dict(self._p2p_coordinator._capability_map)
|
|
775
|
+
|
|
627
776
|
def __repr__(self) -> str:
|
|
628
777
|
"""String representation of mesh."""
|
|
629
778
|
return (
|