jarviscore-framework 0.2.1__py3-none-any.whl → 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.
- examples/cloud_deployment_example.py +162 -0
- examples/fastapi_integration_example.py +570 -0
- examples/listeneragent_cognitive_discovery_example.py +343 -0
- jarviscore/__init__.py +22 -5
- jarviscore/cli/smoketest.py +8 -4
- jarviscore/core/agent.py +227 -0
- jarviscore/data/examples/cloud_deployment_example.py +162 -0
- jarviscore/data/examples/fastapi_integration_example.py +570 -0
- jarviscore/data/examples/listeneragent_cognitive_discovery_example.py +343 -0
- jarviscore/docs/API_REFERENCE.md +296 -3
- jarviscore/docs/CHANGELOG.md +97 -0
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +832 -13
- jarviscore/docs/GETTING_STARTED.md +111 -7
- jarviscore/docs/USER_GUIDE.md +152 -6
- jarviscore/integrations/__init__.py +16 -0
- jarviscore/integrations/fastapi.py +247 -0
- jarviscore/p2p/broadcaster.py +10 -3
- jarviscore/p2p/coordinator.py +310 -14
- jarviscore/p2p/keepalive.py +45 -23
- jarviscore/p2p/peer_client.py +282 -10
- jarviscore/p2p/swim_manager.py +9 -4
- jarviscore/profiles/__init__.py +10 -2
- jarviscore/profiles/listeneragent.py +292 -0
- {jarviscore_framework-0.2.1.dist-info → jarviscore_framework-0.3.0.dist-info}/METADATA +37 -4
- {jarviscore_framework-0.2.1.dist-info → jarviscore_framework-0.3.0.dist-info}/RECORD +32 -18
- {jarviscore_framework-0.2.1.dist-info → jarviscore_framework-0.3.0.dist-info}/WHEEL +1 -1
- tests/test_13_dx_improvements.py +554 -0
- tests/test_14_cloud_deployment.py +403 -0
- tests/test_15_llm_cognitive_discovery.py +684 -0
- tests/test_16_unified_dx_flow.py +947 -0
- {jarviscore_framework-0.2.1.dist-info → jarviscore_framework-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {jarviscore_framework-0.2.1.dist-info → jarviscore_framework-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -12,6 +12,7 @@ Build your first AI agent in 5 minutes!
|
|
|
12
12
|
|---------|----------|--------------|
|
|
13
13
|
| **AutoAgent** | Rapid prototyping, LLM generates code from prompts | Yes |
|
|
14
14
|
| **CustomAgent** | Existing code, full control (LangChain, CrewAI, etc.) | Optional |
|
|
15
|
+
| **ListenerAgent** | API-first (FastAPI), just implement handlers | Optional |
|
|
15
16
|
|
|
16
17
|
### Execution Modes (How agents are orchestrated)
|
|
17
18
|
|
|
@@ -19,9 +20,12 @@ Build your first AI agent in 5 minutes!
|
|
|
19
20
|
|------|----------|------------|
|
|
20
21
|
| **Autonomous** | Single machine, simple pipelines | ✅ This guide |
|
|
21
22
|
| **P2P** | Direct agent communication, swarms | [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) |
|
|
22
|
-
| **Distributed** | Multi-node production systems | [
|
|
23
|
+
| **Distributed** | Multi-node production systems | [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) |
|
|
23
24
|
|
|
24
|
-
**Recommendation:**
|
|
25
|
+
**Recommendation:**
|
|
26
|
+
- **New to agents?** Start with **AutoAgent + Autonomous mode** below
|
|
27
|
+
- **Have existing code?** Jump to **CustomAgent** or **ListenerAgent** sections
|
|
28
|
+
- **Building APIs?** See **ListenerAgent + FastAPI** below
|
|
25
29
|
|
|
26
30
|
---
|
|
27
31
|
|
|
@@ -249,6 +253,90 @@ asyncio.run(main())
|
|
|
249
253
|
|
|
250
254
|
---
|
|
251
255
|
|
|
256
|
+
## Step 6: ListenerAgent + FastAPI (API-First Path)
|
|
257
|
+
|
|
258
|
+
Building an API where agents run in the background? **ListenerAgent** eliminates the boilerplate.
|
|
259
|
+
|
|
260
|
+
### The Problem
|
|
261
|
+
|
|
262
|
+
With CustomAgent, you write a `run()` loop to handle peer messages:
|
|
263
|
+
|
|
264
|
+
```python
|
|
265
|
+
# CustomAgent - you write the loop
|
|
266
|
+
class MyAgent(CustomAgent):
|
|
267
|
+
async def run(self):
|
|
268
|
+
while not self.shutdown_requested:
|
|
269
|
+
msg = await self.peers.receive(timeout=1.0)
|
|
270
|
+
if msg is None:
|
|
271
|
+
continue
|
|
272
|
+
if msg.type == MessageType.REQUEST:
|
|
273
|
+
result = await self.process(msg.data)
|
|
274
|
+
await self.peers.respond(msg, result)
|
|
275
|
+
# ... error handling, logging, etc.
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### The Solution
|
|
279
|
+
|
|
280
|
+
With ListenerAgent, just implement handlers:
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from jarviscore.profiles import ListenerAgent
|
|
284
|
+
|
|
285
|
+
class MyAgent(ListenerAgent):
|
|
286
|
+
role = "processor"
|
|
287
|
+
capabilities = ["processing"]
|
|
288
|
+
|
|
289
|
+
async def on_peer_request(self, msg):
|
|
290
|
+
"""Handle requests - return value is sent as response."""
|
|
291
|
+
return {"result": await self.process(msg.data)}
|
|
292
|
+
|
|
293
|
+
async def on_peer_notify(self, msg):
|
|
294
|
+
"""Handle fire-and-forget notifications."""
|
|
295
|
+
await self.log_event(msg.data)
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### FastAPI Integration (3 Lines)
|
|
299
|
+
|
|
300
|
+
```python
|
|
301
|
+
from fastapi import FastAPI, Request
|
|
302
|
+
from jarviscore.profiles import ListenerAgent
|
|
303
|
+
from jarviscore.integrations.fastapi import JarvisLifespan
|
|
304
|
+
|
|
305
|
+
class ProcessorAgent(ListenerAgent):
|
|
306
|
+
role = "processor"
|
|
307
|
+
capabilities = ["data_processing"]
|
|
308
|
+
|
|
309
|
+
async def on_peer_request(self, msg):
|
|
310
|
+
return {"processed": msg.data.get("task", "").upper()}
|
|
311
|
+
|
|
312
|
+
# Create agent and integrate with FastAPI
|
|
313
|
+
agent = ProcessorAgent()
|
|
314
|
+
app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p", bind_port=7950))
|
|
315
|
+
|
|
316
|
+
@app.post("/process")
|
|
317
|
+
async def process(data: dict, request: Request):
|
|
318
|
+
# Access your agent from the request
|
|
319
|
+
agent = request.app.state.jarvis_agents["processor"]
|
|
320
|
+
return await agent.process(data)
|
|
321
|
+
|
|
322
|
+
@app.get("/peers")
|
|
323
|
+
async def list_peers(request: Request):
|
|
324
|
+
agent = request.app.state.jarvis_agents["processor"]
|
|
325
|
+
return {"peers": agent.peers.list_peers()}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Run with: `uvicorn myapp:app --host 0.0.0.0 --port 8000`
|
|
329
|
+
|
|
330
|
+
**What you get:**
|
|
331
|
+
- HTTP endpoints (FastAPI routes) as primary interface
|
|
332
|
+
- P2P mesh participation in background
|
|
333
|
+
- Auto message dispatch to handlers
|
|
334
|
+
- Graceful startup/shutdown handled by JarvisLifespan
|
|
335
|
+
|
|
336
|
+
**For more:** See [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) for ListenerAgent details.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
252
340
|
## What Just Happened?
|
|
253
341
|
|
|
254
342
|
Behind the scenes, JarvisCore:
|
|
@@ -394,7 +482,23 @@ class MyAgent(CustomAgent):
|
|
|
394
482
|
...
|
|
395
483
|
```
|
|
396
484
|
|
|
397
|
-
### 3.
|
|
485
|
+
### 3. ListenerAgent Profile
|
|
486
|
+
|
|
487
|
+
The `ListenerAgent` profile is for API-first agents - just implement handlers:
|
|
488
|
+
|
|
489
|
+
```python
|
|
490
|
+
class MyAgent(ListenerAgent):
|
|
491
|
+
role = "unique_name"
|
|
492
|
+
capabilities = ["skill1", "skill2"]
|
|
493
|
+
|
|
494
|
+
async def on_peer_request(self, msg): # Handle requests (return = response)
|
|
495
|
+
return {"result": ...}
|
|
496
|
+
|
|
497
|
+
async def on_peer_notify(self, msg): # Handle notifications (fire-and-forget)
|
|
498
|
+
await self.log(msg.data)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
### 4. Mesh
|
|
398
502
|
|
|
399
503
|
The `Mesh` is the orchestrator that manages agents and workflows:
|
|
400
504
|
|
|
@@ -408,10 +512,10 @@ await mesh.stop() # Cleanup
|
|
|
408
512
|
|
|
409
513
|
**Modes:**
|
|
410
514
|
- `autonomous`: Workflow engine only (AutoAgent)
|
|
411
|
-
- `p2p`: P2P coordinator for agent-to-agent communication (CustomAgent)
|
|
412
|
-
- `distributed`: Both workflow engine AND P2P (CustomAgent)
|
|
515
|
+
- `p2p`: P2P coordinator for agent-to-agent communication (CustomAgent, ListenerAgent)
|
|
516
|
+
- `distributed`: Both workflow engine AND P2P (CustomAgent, ListenerAgent)
|
|
413
517
|
|
|
414
|
-
###
|
|
518
|
+
### 5. Workflow
|
|
415
519
|
|
|
416
520
|
A workflow is a list of tasks to execute:
|
|
417
521
|
|
|
@@ -425,7 +529,7 @@ results = await mesh.workflow("workflow-id", [
|
|
|
425
529
|
])
|
|
426
530
|
```
|
|
427
531
|
|
|
428
|
-
###
|
|
532
|
+
### 6. Results
|
|
429
533
|
|
|
430
534
|
Each task returns a result dict:
|
|
431
535
|
|
jarviscore/docs/USER_GUIDE.md
CHANGED
|
@@ -16,9 +16,12 @@ Practical guide to building agent systems with JarvisCore.
|
|
|
16
16
|
8. [Remote Sandbox](#remote-sandbox)
|
|
17
17
|
9. [Result Storage](#result-storage)
|
|
18
18
|
10. [Code Registry](#code-registry)
|
|
19
|
-
11. [
|
|
20
|
-
12. [
|
|
21
|
-
13. [
|
|
19
|
+
11. [FastAPI Integration (v0.3.0)](#fastapi-integration-v030)
|
|
20
|
+
12. [Cloud Deployment (v0.3.0)](#cloud-deployment-v030)
|
|
21
|
+
13. [Cognitive Discovery (v0.3.0)](#cognitive-discovery-v030)
|
|
22
|
+
14. [Best Practices](#best-practices)
|
|
23
|
+
15. [Common Patterns](#common-patterns)
|
|
24
|
+
16. [Troubleshooting](#troubleshooting)
|
|
22
25
|
|
|
23
26
|
---
|
|
24
27
|
|
|
@@ -138,12 +141,13 @@ mesh = Mesh(mode="distributed", config={'bind_port': 7950})
|
|
|
138
141
|
|
|
139
142
|
### Agents
|
|
140
143
|
|
|
141
|
-
**Agents** are workers that execute tasks. JarvisCore offers
|
|
144
|
+
**Agents** are workers that execute tasks. JarvisCore offers three profiles:
|
|
142
145
|
|
|
143
146
|
| Profile | Best For | How It Works |
|
|
144
147
|
|---------|----------|--------------|
|
|
145
148
|
| **AutoAgent** | Rapid prototyping | LLM generates + executes code from prompts |
|
|
146
149
|
| **CustomAgent** | Existing code | You provide `execute_task()` or `run()` |
|
|
150
|
+
| **ListenerAgent** | API-first agents | Just implement `on_peer_request()` handlers |
|
|
147
151
|
|
|
148
152
|
See [AutoAgent Guide](AUTOAGENT_GUIDE.md) and [CustomAgent Guide](CUSTOMAGENT_GUIDE.md) for details.
|
|
149
153
|
|
|
@@ -534,6 +538,148 @@ print(f"Registered: {func['registered_at']}")
|
|
|
534
538
|
|
|
535
539
|
---
|
|
536
540
|
|
|
541
|
+
## FastAPI Integration (v0.3.0)
|
|
542
|
+
|
|
543
|
+
Deploy agents as FastAPI services with minimal boilerplate:
|
|
544
|
+
|
|
545
|
+
### JarvisLifespan
|
|
546
|
+
|
|
547
|
+
```python
|
|
548
|
+
from fastapi import FastAPI, Request
|
|
549
|
+
from jarviscore.profiles import ListenerAgent
|
|
550
|
+
from jarviscore.integrations.fastapi import JarvisLifespan
|
|
551
|
+
|
|
552
|
+
class ProcessorAgent(ListenerAgent):
|
|
553
|
+
role = "processor"
|
|
554
|
+
capabilities = ["processing"]
|
|
555
|
+
|
|
556
|
+
async def on_peer_request(self, msg):
|
|
557
|
+
return {"result": msg.data.get("task", "").upper()}
|
|
558
|
+
|
|
559
|
+
# 3 lines to integrate
|
|
560
|
+
agent = ProcessorAgent()
|
|
561
|
+
app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p", bind_port=7950))
|
|
562
|
+
|
|
563
|
+
@app.get("/peers")
|
|
564
|
+
async def list_peers(request: Request):
|
|
565
|
+
agent = request.app.state.jarvis_agents["processor"]
|
|
566
|
+
return {"peers": agent.peers.list_peers()}
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
**What JarvisLifespan handles:**
|
|
570
|
+
- Mesh startup/shutdown
|
|
571
|
+
- Background task management for agent run() loops
|
|
572
|
+
- Graceful shutdown with timeouts
|
|
573
|
+
- State injection into FastAPI app
|
|
574
|
+
|
|
575
|
+
---
|
|
576
|
+
|
|
577
|
+
## Cloud Deployment (v0.3.0)
|
|
578
|
+
|
|
579
|
+
Deploy agents to containers without a central orchestrator:
|
|
580
|
+
|
|
581
|
+
### Self-Registration Pattern
|
|
582
|
+
|
|
583
|
+
```python
|
|
584
|
+
# In your container entrypoint
|
|
585
|
+
import asyncio
|
|
586
|
+
from jarviscore.profiles import ListenerAgent
|
|
587
|
+
|
|
588
|
+
class MyAgent(ListenerAgent):
|
|
589
|
+
role = "worker"
|
|
590
|
+
capabilities = ["processing"]
|
|
591
|
+
|
|
592
|
+
async def on_peer_request(self, msg):
|
|
593
|
+
return {"processed": msg.data}
|
|
594
|
+
|
|
595
|
+
async def main():
|
|
596
|
+
agent = MyAgent()
|
|
597
|
+
|
|
598
|
+
# Join existing mesh (uses JARVISCORE_SEED_NODES env var)
|
|
599
|
+
await agent.join_mesh()
|
|
600
|
+
|
|
601
|
+
print(f"Joined as {agent.role}, discovered: {agent.peers.list_peers()}")
|
|
602
|
+
|
|
603
|
+
# Run until shutdown, auto-leaves mesh on exit
|
|
604
|
+
await agent.run_standalone()
|
|
605
|
+
|
|
606
|
+
asyncio.run(main())
|
|
607
|
+
```
|
|
608
|
+
|
|
609
|
+
### Docker/Kubernetes
|
|
610
|
+
|
|
611
|
+
```dockerfile
|
|
612
|
+
FROM python:3.11-slim
|
|
613
|
+
WORKDIR /app
|
|
614
|
+
COPY . .
|
|
615
|
+
RUN pip install jarviscore-framework
|
|
616
|
+
|
|
617
|
+
# Point to existing mesh
|
|
618
|
+
ENV JARVISCORE_SEED_NODES=mesh-service:7946
|
|
619
|
+
|
|
620
|
+
CMD ["python", "-m", "myapp.agent"]
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Environment Variables:**
|
|
624
|
+
- `JARVISCORE_SEED_NODES` - Comma-separated list of seed nodes
|
|
625
|
+
- `JARVISCORE_MESH_ENDPOINT` - Single endpoint to join
|
|
626
|
+
|
|
627
|
+
---
|
|
628
|
+
|
|
629
|
+
## Cognitive Discovery (v0.3.0)
|
|
630
|
+
|
|
631
|
+
Let LLMs dynamically discover mesh peers instead of hardcoding agent names:
|
|
632
|
+
|
|
633
|
+
### The Problem
|
|
634
|
+
|
|
635
|
+
```python
|
|
636
|
+
# Before: Hardcoded peer names in prompts
|
|
637
|
+
system_prompt = """
|
|
638
|
+
You can delegate to:
|
|
639
|
+
- analyst for data analysis
|
|
640
|
+
- scout for research
|
|
641
|
+
"""
|
|
642
|
+
# Breaks when mesh composition changes!
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
### The Solution
|
|
646
|
+
|
|
647
|
+
```python
|
|
648
|
+
# After: Dynamic discovery
|
|
649
|
+
system_prompt = self.peers.build_system_prompt(
|
|
650
|
+
"You are a coordinator agent."
|
|
651
|
+
)
|
|
652
|
+
# Automatically includes all available peers!
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
### get_cognitive_context()
|
|
656
|
+
|
|
657
|
+
```python
|
|
658
|
+
# Get prompt-ready peer descriptions
|
|
659
|
+
context = self.peers.get_cognitive_context(format="markdown")
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
**Output:**
|
|
663
|
+
```markdown
|
|
664
|
+
## AVAILABLE MESH PEERS
|
|
665
|
+
|
|
666
|
+
You are part of a multi-agent mesh. The following peers are available:
|
|
667
|
+
|
|
668
|
+
- **analyst** (`agent-analyst-abc123`)
|
|
669
|
+
- Capabilities: analysis, charting, reporting
|
|
670
|
+
- Description: Analyzes data and generates insights
|
|
671
|
+
|
|
672
|
+
- **scout** (`agent-scout-def456`)
|
|
673
|
+
- Capabilities: research, reconnaissance
|
|
674
|
+
- Description: Gathers information
|
|
675
|
+
|
|
676
|
+
Use the `ask_peer` tool to delegate tasks to these specialists.
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**Formats:** `markdown`, `json`, `text`
|
|
680
|
+
|
|
681
|
+
---
|
|
682
|
+
|
|
537
683
|
## Best Practices
|
|
538
684
|
|
|
539
685
|
### 1. Always Use Context Managers
|
|
@@ -759,6 +905,6 @@ mesh = Mesh(config=config)
|
|
|
759
905
|
|
|
760
906
|
## Version
|
|
761
907
|
|
|
762
|
-
User Guide for JarvisCore v0.
|
|
908
|
+
User Guide for JarvisCore v0.3.0
|
|
763
909
|
|
|
764
|
-
Last Updated: 2026-01-
|
|
910
|
+
Last Updated: 2026-01-29
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Framework integrations for JarvisCore.
|
|
3
|
+
|
|
4
|
+
Provides first-class support for popular web frameworks,
|
|
5
|
+
reducing boilerplate for production deployments.
|
|
6
|
+
|
|
7
|
+
Available integrations:
|
|
8
|
+
- FastAPI: JarvisLifespan, create_jarvis_app
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from .fastapi import JarvisLifespan, create_jarvis_app
|
|
13
|
+
__all__ = ['JarvisLifespan', 'create_jarvis_app']
|
|
14
|
+
except ImportError:
|
|
15
|
+
# FastAPI not installed - integrations not available
|
|
16
|
+
__all__ = []
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FastAPI integration for JarvisCore.
|
|
3
|
+
|
|
4
|
+
Reduces boilerplate from ~100 lines to 3 lines for integrating
|
|
5
|
+
JarvisCore agents with FastAPI applications.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from fastapi import FastAPI
|
|
9
|
+
from jarviscore.integrations.fastapi import JarvisLifespan
|
|
10
|
+
|
|
11
|
+
agent = MyAgent()
|
|
12
|
+
app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p", bind_port=7950))
|
|
13
|
+
|
|
14
|
+
@app.get("/peers")
|
|
15
|
+
async def get_peers(request: Request):
|
|
16
|
+
agent = request.app.state.jarvis_agents.get("my_role")
|
|
17
|
+
return {"peers": agent.peers.list_peers()}
|
|
18
|
+
"""
|
|
19
|
+
from contextlib import asynccontextmanager
|
|
20
|
+
from typing import Union, List, TYPE_CHECKING
|
|
21
|
+
import asyncio
|
|
22
|
+
import logging
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from jarviscore.core.agent import Agent
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class JarvisLifespan:
|
|
31
|
+
"""
|
|
32
|
+
FastAPI lifespan manager for JarvisCore agents.
|
|
33
|
+
|
|
34
|
+
Handles the complete lifecycle of JarvisCore mesh integration:
|
|
35
|
+
- Mesh initialization on startup
|
|
36
|
+
- Background task management for agent run() loops
|
|
37
|
+
- Graceful shutdown with proper cleanup
|
|
38
|
+
- State injection into FastAPI app for handler access
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
agents: Single agent instance or list of agents to run
|
|
42
|
+
mode: Mesh mode - "p2p", "distributed", or "autonomous"
|
|
43
|
+
**mesh_config: Additional Mesh configuration options:
|
|
44
|
+
- bind_host: P2P bind address (default: "127.0.0.1")
|
|
45
|
+
- bind_port: P2P bind port (default: 7946)
|
|
46
|
+
- seed_nodes: Comma-separated seed nodes for joining cluster
|
|
47
|
+
- node_name: Node identifier for P2P network
|
|
48
|
+
|
|
49
|
+
Example - Single Agent:
|
|
50
|
+
from fastapi import FastAPI, Request
|
|
51
|
+
from jarviscore.integrations.fastapi import JarvisLifespan
|
|
52
|
+
from jarviscore.profiles import ListenerAgent
|
|
53
|
+
|
|
54
|
+
class MyAgent(ListenerAgent):
|
|
55
|
+
role = "processor"
|
|
56
|
+
capabilities = ["processing"]
|
|
57
|
+
|
|
58
|
+
async def on_peer_request(self, msg):
|
|
59
|
+
return {"processed": msg.data}
|
|
60
|
+
|
|
61
|
+
agent = MyAgent()
|
|
62
|
+
app = FastAPI(lifespan=JarvisLifespan(agent, mode="p2p", bind_port=7950))
|
|
63
|
+
|
|
64
|
+
@app.get("/health")
|
|
65
|
+
async def health(request: Request):
|
|
66
|
+
mesh = request.app.state.jarvis_mesh
|
|
67
|
+
return {"status": "ok", "mesh_started": mesh._started}
|
|
68
|
+
|
|
69
|
+
Example - Multiple Agents:
|
|
70
|
+
agents = [ProcessorAgent(), AnalyzerAgent()]
|
|
71
|
+
app = FastAPI(lifespan=JarvisLifespan(agents, mode="p2p"))
|
|
72
|
+
|
|
73
|
+
Example - Joining Existing Cluster:
|
|
74
|
+
app = FastAPI(lifespan=JarvisLifespan(
|
|
75
|
+
agent,
|
|
76
|
+
mode="p2p",
|
|
77
|
+
seed_nodes="192.168.1.10:7946,192.168.1.11:7946"
|
|
78
|
+
))
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
def __init__(
|
|
82
|
+
self,
|
|
83
|
+
agents: Union['Agent', List['Agent']],
|
|
84
|
+
mode: str = "p2p",
|
|
85
|
+
**mesh_config
|
|
86
|
+
):
|
|
87
|
+
"""
|
|
88
|
+
Initialize JarvisLifespan.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
agents: Single agent or list of agents to run
|
|
92
|
+
mode: Mesh mode ("p2p", "distributed", "autonomous")
|
|
93
|
+
**mesh_config: Additional Mesh configuration
|
|
94
|
+
"""
|
|
95
|
+
self.agents = agents if isinstance(agents, list) else [agents]
|
|
96
|
+
self.mode = mode
|
|
97
|
+
self.mesh_config = mesh_config
|
|
98
|
+
self.mesh = None
|
|
99
|
+
self._background_tasks: List[asyncio.Task] = []
|
|
100
|
+
self._nodes: List['Agent'] = []
|
|
101
|
+
|
|
102
|
+
@asynccontextmanager
|
|
103
|
+
async def __call__(self, app):
|
|
104
|
+
"""
|
|
105
|
+
ASGI lifespan context manager.
|
|
106
|
+
|
|
107
|
+
Called by FastAPI/Starlette on app startup/shutdown.
|
|
108
|
+
Manages the complete mesh lifecycle.
|
|
109
|
+
"""
|
|
110
|
+
from jarviscore import Mesh
|
|
111
|
+
|
|
112
|
+
# ─────────────────────────────────────────────────────────────
|
|
113
|
+
# STARTUP
|
|
114
|
+
# ─────────────────────────────────────────────────────────────
|
|
115
|
+
logger.info(f"JarvisLifespan: Starting mesh in {self.mode} mode...")
|
|
116
|
+
|
|
117
|
+
# 1. Create mesh with provided configuration
|
|
118
|
+
self.mesh = Mesh(mode=self.mode, config=self.mesh_config)
|
|
119
|
+
|
|
120
|
+
# 2. Register all agents with the mesh
|
|
121
|
+
self._nodes = []
|
|
122
|
+
for agent in self.agents:
|
|
123
|
+
node = self.mesh.add(agent)
|
|
124
|
+
self._nodes.append(node)
|
|
125
|
+
logger.debug(f"JarvisLifespan: Registered agent {node.role}")
|
|
126
|
+
|
|
127
|
+
# 3. Start mesh (initializes P2P coordinator, injects PeerClients)
|
|
128
|
+
await self.mesh.start()
|
|
129
|
+
logger.info(f"JarvisLifespan: Mesh started with {len(self._nodes)} agent(s)")
|
|
130
|
+
|
|
131
|
+
# 4. Launch agent run() loops as background tasks
|
|
132
|
+
# This is crucial - without backgrounding, the HTTP server would hang
|
|
133
|
+
for node in self._nodes:
|
|
134
|
+
if hasattr(node, 'run') and asyncio.iscoroutinefunction(node.run):
|
|
135
|
+
task = asyncio.create_task(
|
|
136
|
+
self._run_agent_with_error_handling(node),
|
|
137
|
+
name=f"jarvis-agent-{node.agent_id}"
|
|
138
|
+
)
|
|
139
|
+
self._background_tasks.append(task)
|
|
140
|
+
logger.info(f"JarvisLifespan: Started background loop for {node.role}")
|
|
141
|
+
|
|
142
|
+
# 5. Inject state into FastAPI app for handler access
|
|
143
|
+
app.state.jarvis_mesh = self.mesh
|
|
144
|
+
app.state.jarvis_agents = {node.role: node for node in self._nodes}
|
|
145
|
+
|
|
146
|
+
logger.info("JarvisLifespan: Startup complete")
|
|
147
|
+
|
|
148
|
+
# ─────────────────────────────────────────────────────────────
|
|
149
|
+
# APP RUNS HERE
|
|
150
|
+
# ─────────────────────────────────────────────────────────────
|
|
151
|
+
try:
|
|
152
|
+
yield
|
|
153
|
+
finally:
|
|
154
|
+
# ─────────────────────────────────────────────────────────────
|
|
155
|
+
# SHUTDOWN
|
|
156
|
+
# ─────────────────────────────────────────────────────────────
|
|
157
|
+
logger.info("JarvisLifespan: Shutting down...")
|
|
158
|
+
|
|
159
|
+
# 1. Request shutdown for all agents (signals run() loops to exit)
|
|
160
|
+
for node in self._nodes:
|
|
161
|
+
node.request_shutdown()
|
|
162
|
+
|
|
163
|
+
# 2. Cancel background tasks gracefully with timeout
|
|
164
|
+
for task in self._background_tasks:
|
|
165
|
+
if not task.done():
|
|
166
|
+
task.cancel()
|
|
167
|
+
try:
|
|
168
|
+
await asyncio.wait_for(task, timeout=5.0)
|
|
169
|
+
except (asyncio.CancelledError, asyncio.TimeoutError):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
# 3. Stop mesh (cleanup P2P coordinator, call agent teardown)
|
|
173
|
+
if self.mesh:
|
|
174
|
+
await self.mesh.stop()
|
|
175
|
+
|
|
176
|
+
logger.info("JarvisLifespan: Shutdown complete")
|
|
177
|
+
|
|
178
|
+
async def _run_agent_with_error_handling(self, agent: 'Agent'):
|
|
179
|
+
"""
|
|
180
|
+
Run agent loop with error handling and logging.
|
|
181
|
+
|
|
182
|
+
Wraps the agent's run() method to catch and log errors
|
|
183
|
+
without crashing the entire application.
|
|
184
|
+
"""
|
|
185
|
+
try:
|
|
186
|
+
await agent.run()
|
|
187
|
+
except asyncio.CancelledError:
|
|
188
|
+
logger.debug(f"Agent {agent.agent_id} loop cancelled")
|
|
189
|
+
raise
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.error(f"Agent {agent.agent_id} loop error: {e}", exc_info=True)
|
|
192
|
+
# Re-raise to allow task to be marked as failed
|
|
193
|
+
raise
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def create_jarvis_app(
|
|
197
|
+
agent: 'Agent',
|
|
198
|
+
mode: str = "p2p",
|
|
199
|
+
title: str = "JarvisCore Agent",
|
|
200
|
+
description: str = "API powered by JarvisCore",
|
|
201
|
+
version: str = "1.0.0",
|
|
202
|
+
**mesh_config
|
|
203
|
+
) -> 'FastAPI':
|
|
204
|
+
"""
|
|
205
|
+
Create a FastAPI app with JarvisCore integration pre-configured.
|
|
206
|
+
|
|
207
|
+
Convenience function for simple single-agent deployments.
|
|
208
|
+
For more control, use JarvisLifespan directly.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
agent: Agent instance to run
|
|
212
|
+
mode: Mesh mode ("p2p", "distributed", "autonomous")
|
|
213
|
+
title: FastAPI app title
|
|
214
|
+
description: FastAPI app description
|
|
215
|
+
version: API version
|
|
216
|
+
**mesh_config: Mesh configuration options
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Configured FastAPI app with JarvisCore integration
|
|
220
|
+
|
|
221
|
+
Example:
|
|
222
|
+
from jarviscore.integrations.fastapi import create_jarvis_app
|
|
223
|
+
from jarviscore.profiles import ListenerAgent
|
|
224
|
+
|
|
225
|
+
class MyAgent(ListenerAgent):
|
|
226
|
+
role = "processor"
|
|
227
|
+
capabilities = ["processing"]
|
|
228
|
+
|
|
229
|
+
async def on_peer_request(self, msg):
|
|
230
|
+
return {"result": "processed"}
|
|
231
|
+
|
|
232
|
+
app = create_jarvis_app(MyAgent(), mode="p2p", bind_port=7950)
|
|
233
|
+
|
|
234
|
+
@app.get("/health")
|
|
235
|
+
async def health():
|
|
236
|
+
return {"status": "ok"}
|
|
237
|
+
|
|
238
|
+
# Run with: uvicorn myapp:app --host 0.0.0.0 --port 8000
|
|
239
|
+
"""
|
|
240
|
+
from fastapi import FastAPI
|
|
241
|
+
|
|
242
|
+
return FastAPI(
|
|
243
|
+
title=title,
|
|
244
|
+
description=description,
|
|
245
|
+
version=version,
|
|
246
|
+
lifespan=JarvisLifespan(agent, mode=mode, **mesh_config)
|
|
247
|
+
)
|
jarviscore/p2p/broadcaster.py
CHANGED
|
@@ -143,12 +143,19 @@ class StepOutputBroadcaster:
|
|
|
143
143
|
"""
|
|
144
144
|
try:
|
|
145
145
|
message_id = message_data.get('id', str(uuid.uuid4()))
|
|
146
|
-
|
|
147
|
-
|
|
146
|
+
|
|
147
|
+
# The sender uses 'step_output_data' as the key (json string)
|
|
148
|
+
step_output_json = message_data.get('step_output_data', '')
|
|
149
|
+
if step_output_json:
|
|
150
|
+
step_result_data = json.loads(step_output_json)
|
|
151
|
+
else:
|
|
152
|
+
# Fallback for legacy format
|
|
153
|
+
step_result_data = message_data.get('step_result', {})
|
|
154
|
+
|
|
148
155
|
# Send acknowledgment if callback provided
|
|
149
156
|
if send_ack_callback:
|
|
150
157
|
await send_ack_callback(sender_swim_id, message_id, "STEP_OUTPUT_BROADCAST")
|
|
151
|
-
|
|
158
|
+
|
|
152
159
|
# Create StepExecutionResult from received data
|
|
153
160
|
result = StepExecutionResult(**step_result_data)
|
|
154
161
|
|