agentviz 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.
- agentviz/__init__.py +53 -0
- agentviz/__main__.py +4 -0
- agentviz/_server_app.py +322 -0
- agentviz/cli.py +116 -0
- agentviz/config.py +121 -0
- agentviz/http_client.py +393 -0
- agentviz/integrations/__init__.py +123 -0
- agentviz/integrations/crewai.py +396 -0
- agentviz/integrations/langchain.py +397 -0
- agentviz/integrations/langgraph.py +487 -0
- agentviz/integrations/openai_agents.py +477 -0
- agentviz/integrations/otel.py +201 -0
- agentviz/mcp_server.py +372 -0
- agentviz/py.typed +0 -0
- agentviz/recorder.py +176 -0
- agentviz/sdk.py +267 -0
- agentviz/static/agentviz-core/AgentViz.js +720 -0
- agentviz/static/agentviz-core/Animator.js +30 -0
- agentviz/static/agentviz-core/Environment.js +373 -0
- agentviz/static/agentviz-core/LayoutEngine.js +184 -0
- agentviz/static/agentviz-core/ProtocolHandler.js +347 -0
- agentviz/static/agentviz-core/Robot.js +404 -0
- agentviz/static/agentviz-core/index.js +6 -0
- agentviz/static/index.html +1247 -0
- agentviz/tracing.py +289 -0
- agentviz-0.3.0.dist-info/METADATA +272 -0
- agentviz-0.3.0.dist-info/RECORD +30 -0
- agentviz-0.3.0.dist-info/WHEEL +5 -0
- agentviz-0.3.0.dist-info/entry_points.txt +2 -0
- agentviz-0.3.0.dist-info/top_level.txt +1 -0
agentviz/__init__.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentViz — Real-time 3D visualization for multi-agent AI systems.
|
|
3
|
+
|
|
4
|
+
LangChain-style quick start
|
|
5
|
+
----------------------------
|
|
6
|
+
import agentviz
|
|
7
|
+
|
|
8
|
+
# Option A: env vars (no code changes needed)
|
|
9
|
+
# export AGENTVIZ_SERVER=https://agentviz.yourdomain.com
|
|
10
|
+
# export AGENTVIZ_PROJECT=my-team
|
|
11
|
+
|
|
12
|
+
# Option B: one-line programmatic config
|
|
13
|
+
agentviz.init(server="https://agentviz.yourdomain.com", project="my-team")
|
|
14
|
+
|
|
15
|
+
# Decorate any function — async or sync
|
|
16
|
+
@agentviz.trace
|
|
17
|
+
async def fetch_data(query: str) -> str:
|
|
18
|
+
return await db.query(query)
|
|
19
|
+
|
|
20
|
+
@agentviz.trace(name="Planner", to="orchestrator", color="#9B59B6")
|
|
21
|
+
async def plan(goal: str) -> str:
|
|
22
|
+
return await llm.plan(goal)
|
|
23
|
+
|
|
24
|
+
# Auto-patch every installed framework at once
|
|
25
|
+
agentviz.instrument_all()
|
|
26
|
+
|
|
27
|
+
Manual SDK (full control)
|
|
28
|
+
--------------------------
|
|
29
|
+
from agentviz import AgentVizClient # WebSocket — local / same-network
|
|
30
|
+
from agentviz import HttpAgentVizClient # HTTP — cloud / serverless
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# ── LangChain-style top-level API ─────────────────────────────────────────────
|
|
34
|
+
from .config import init, configure
|
|
35
|
+
from .tracing import trace, Agent, instrument_all
|
|
36
|
+
|
|
37
|
+
# ── Low-level SDK clients ─────────────────────────────────────────────────────
|
|
38
|
+
from .sdk import AgentVizClient
|
|
39
|
+
from .http_client import HttpAgentVizClient
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
# LangChain-style
|
|
43
|
+
"init",
|
|
44
|
+
"configure",
|
|
45
|
+
"trace",
|
|
46
|
+
"Agent",
|
|
47
|
+
"instrument_all",
|
|
48
|
+
# SDK clients
|
|
49
|
+
"AgentVizClient",
|
|
50
|
+
"HttpAgentVizClient",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
__version__ = "0.3.0"
|
agentviz/__main__.py
ADDED
agentviz/_server_app.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Standalone AgentViz server.
|
|
3
|
+
|
|
4
|
+
Used by `agentviz serve` (CLI) and can be imported directly:
|
|
5
|
+
|
|
6
|
+
from agentviz._server_app import create_app
|
|
7
|
+
app = create_app()
|
|
8
|
+
|
|
9
|
+
Room isolation
|
|
10
|
+
--------------
|
|
11
|
+
Every WebSocket / HTTP endpoint accepts an optional ?room= query parameter.
|
|
12
|
+
Agents and browser viewers in the same room share one 3D scene; different
|
|
13
|
+
rooms are completely isolated from each other.
|
|
14
|
+
|
|
15
|
+
This makes it safe to host one AgentViz instance on a public server (e.g.
|
|
16
|
+
Linode) and have multiple teams / projects use it simultaneously.
|
|
17
|
+
|
|
18
|
+
# Team A
|
|
19
|
+
AgentVizClient(server="wss://agentviz.example.com/agent-ws?room=team-a")
|
|
20
|
+
|
|
21
|
+
# Team B — sees nothing from Team A
|
|
22
|
+
AgentVizClient(server="wss://agentviz.example.com/agent-ws?room=team-b")
|
|
23
|
+
|
|
24
|
+
# Browser
|
|
25
|
+
http://agentviz.example.com?room=team-a
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
import json
|
|
30
|
+
import time
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from typing import Optional
|
|
33
|
+
|
|
34
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Query
|
|
35
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
36
|
+
from fastapi.responses import FileResponse, HTMLResponse
|
|
37
|
+
from fastapi.staticfiles import StaticFiles
|
|
38
|
+
from pydantic import BaseModel
|
|
39
|
+
|
|
40
|
+
from agentviz.recorder import SessionRecorder
|
|
41
|
+
|
|
42
|
+
STATIC_DIR = Path(__file__).parent / "static"
|
|
43
|
+
|
|
44
|
+
_CORE_SRC = Path(__file__).parent.parent / "packages" / "core" / "src"
|
|
45
|
+
_CORE_BUILT = STATIC_DIR / "agentviz-core"
|
|
46
|
+
CORE_DIR = _CORE_SRC if _CORE_SRC.exists() else (_CORE_BUILT if _CORE_BUILT.exists() else None)
|
|
47
|
+
|
|
48
|
+
DEFAULT_ROOM = "default"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def create_app() -> FastAPI:
|
|
52
|
+
app = FastAPI(title="AgentViz", version="0.2.0")
|
|
53
|
+
app.add_middleware(
|
|
54
|
+
CORSMiddleware,
|
|
55
|
+
allow_origins=["*"],
|
|
56
|
+
allow_methods=["*"],
|
|
57
|
+
allow_headers=["*"],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ── Per-room state ─────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
class Hub:
|
|
63
|
+
def __init__(self):
|
|
64
|
+
self._conns: list[WebSocket] = []
|
|
65
|
+
|
|
66
|
+
async def connect(self, ws: WebSocket):
|
|
67
|
+
await ws.accept()
|
|
68
|
+
self._conns.append(ws)
|
|
69
|
+
|
|
70
|
+
def disconnect(self, ws: WebSocket):
|
|
71
|
+
if ws in self._conns:
|
|
72
|
+
self._conns.remove(ws)
|
|
73
|
+
|
|
74
|
+
async def broadcast(self, data: dict):
|
|
75
|
+
dead = []
|
|
76
|
+
for ws in list(self._conns):
|
|
77
|
+
try:
|
|
78
|
+
await ws.send_json(data)
|
|
79
|
+
except Exception:
|
|
80
|
+
dead.append(ws)
|
|
81
|
+
for ws in dead:
|
|
82
|
+
self.disconnect(ws)
|
|
83
|
+
|
|
84
|
+
def __len__(self):
|
|
85
|
+
return len(self._conns)
|
|
86
|
+
|
|
87
|
+
class AgentRegistry:
|
|
88
|
+
def __init__(self):
|
|
89
|
+
self._agents: dict[str, dict] = {}
|
|
90
|
+
|
|
91
|
+
def register(self, info: dict):
|
|
92
|
+
aid = info["id"]
|
|
93
|
+
self._agents[aid] = {
|
|
94
|
+
"id": aid,
|
|
95
|
+
"name": info.get("name", aid),
|
|
96
|
+
"color": info.get("color", "#4488ff"),
|
|
97
|
+
"role": info.get("role", "worker"),
|
|
98
|
+
"capabilities": info.get("capabilities", []),
|
|
99
|
+
"connected_at": time.time(),
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def deregister(self, aid: str):
|
|
103
|
+
self._agents.pop(aid, None)
|
|
104
|
+
|
|
105
|
+
def snapshot(self) -> list[dict]:
|
|
106
|
+
return list(self._agents.values())
|
|
107
|
+
|
|
108
|
+
class Room:
|
|
109
|
+
"""All state for one isolated project / team room."""
|
|
110
|
+
|
|
111
|
+
def __init__(self, room_id: str):
|
|
112
|
+
self.id = room_id
|
|
113
|
+
self.hub = Hub()
|
|
114
|
+
self.registry = AgentRegistry()
|
|
115
|
+
self.recorder = SessionRecorder()
|
|
116
|
+
self._last_event_ts: float = 0.0
|
|
117
|
+
self._end_task: Optional[asyncio.Task] = None
|
|
118
|
+
|
|
119
|
+
def touch_session(self) -> None:
|
|
120
|
+
self._last_event_ts = time.time()
|
|
121
|
+
if self.recorder._current_session_id is None:
|
|
122
|
+
self.recorder.start_session()
|
|
123
|
+
if self._end_task and not self._end_task.done():
|
|
124
|
+
self._end_task.cancel()
|
|
125
|
+
self._end_task = asyncio.ensure_future(self._idle_close())
|
|
126
|
+
|
|
127
|
+
async def _idle_close(self) -> None:
|
|
128
|
+
await asyncio.sleep(45)
|
|
129
|
+
if time.time() - self._last_event_ts >= 44:
|
|
130
|
+
self.recorder.end_session()
|
|
131
|
+
|
|
132
|
+
# ── Room manager ──────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
_rooms: dict[str, Room] = {}
|
|
135
|
+
|
|
136
|
+
def get_room(room_id: str) -> Room:
|
|
137
|
+
room_id = (room_id or DEFAULT_ROOM).strip() or DEFAULT_ROOM
|
|
138
|
+
if room_id not in _rooms:
|
|
139
|
+
_rooms[room_id] = Room(room_id)
|
|
140
|
+
return _rooms[room_id]
|
|
141
|
+
|
|
142
|
+
# ── WebSocket: browser viewer ─────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
@app.websocket("/ws")
|
|
145
|
+
async def ws_frontend(ws: WebSocket, room: str = Query(default=DEFAULT_ROOM)):
|
|
146
|
+
r = get_room(room)
|
|
147
|
+
await r.hub.connect(ws)
|
|
148
|
+
if r.registry.snapshot():
|
|
149
|
+
await ws.send_json({"type": "registry_update", "agents": r.registry.snapshot()})
|
|
150
|
+
try:
|
|
151
|
+
while True:
|
|
152
|
+
await ws.receive_text()
|
|
153
|
+
except WebSocketDisconnect:
|
|
154
|
+
r.hub.disconnect(ws)
|
|
155
|
+
|
|
156
|
+
# ── WebSocket: SDK agents ─────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
@app.websocket("/agent-ws")
|
|
159
|
+
async def ws_agent(ws: WebSocket, room: str = Query(default=DEFAULT_ROOM)):
|
|
160
|
+
r = get_room(room)
|
|
161
|
+
await ws.accept()
|
|
162
|
+
agent_id: Optional[str] = None
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
async for raw in ws.iter_text():
|
|
166
|
+
raw = raw.strip()
|
|
167
|
+
if not raw:
|
|
168
|
+
continue
|
|
169
|
+
try:
|
|
170
|
+
msg = json.loads(raw)
|
|
171
|
+
except json.JSONDecodeError:
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
etype = msg.get("type", "")
|
|
175
|
+
|
|
176
|
+
if etype == "register":
|
|
177
|
+
agent_id = msg.get("id")
|
|
178
|
+
if agent_id:
|
|
179
|
+
r.registry.register(msg)
|
|
180
|
+
await r.hub.broadcast({
|
|
181
|
+
"type": "registry_update",
|
|
182
|
+
"agents": r.registry.snapshot(),
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
elif etype in ("call", "response", "thinking", "error", "complete"):
|
|
186
|
+
await r.hub.broadcast(msg)
|
|
187
|
+
r.touch_session()
|
|
188
|
+
r.recorder.record_event(msg)
|
|
189
|
+
|
|
190
|
+
except WebSocketDisconnect:
|
|
191
|
+
pass
|
|
192
|
+
finally:
|
|
193
|
+
if agent_id:
|
|
194
|
+
r.registry.deregister(agent_id)
|
|
195
|
+
await r.hub.broadcast({
|
|
196
|
+
"type": "registry_update",
|
|
197
|
+
"agents": r.registry.snapshot(),
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
# ── HTTP ingest (serverless / Lambda / @trace decorator) ──────────────────
|
|
201
|
+
|
|
202
|
+
class IngestPayload(BaseModel):
|
|
203
|
+
events: list[dict]
|
|
204
|
+
|
|
205
|
+
@app.post("/ingest")
|
|
206
|
+
async def ingest(payload: IngestPayload, room: str = Query(default=DEFAULT_ROOM)):
|
|
207
|
+
"""
|
|
208
|
+
Batch-ingest AgentViz events over plain HTTP.
|
|
209
|
+
Used by HttpAgentVizClient and the @trace decorator.
|
|
210
|
+
|
|
211
|
+
Pass ?room=<project> to route events to the right isolated scene.
|
|
212
|
+
"""
|
|
213
|
+
r = get_room(room)
|
|
214
|
+
ALLOWED = {"register", "call", "response", "thinking", "error", "complete", "token", "stream_end"}
|
|
215
|
+
accepted = 0
|
|
216
|
+
|
|
217
|
+
for msg in payload.events:
|
|
218
|
+
etype = msg.get("type", "")
|
|
219
|
+
if etype not in ALLOWED:
|
|
220
|
+
continue
|
|
221
|
+
if "timestamp" not in msg:
|
|
222
|
+
msg["timestamp"] = time.time()
|
|
223
|
+
|
|
224
|
+
if etype == "register":
|
|
225
|
+
agent_id = msg.get("id")
|
|
226
|
+
if agent_id:
|
|
227
|
+
r.registry.register(msg)
|
|
228
|
+
await r.hub.broadcast({
|
|
229
|
+
"type": "registry_update",
|
|
230
|
+
"agents": r.registry.snapshot(),
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
elif etype in ("call", "response", "thinking", "error", "complete", "token", "stream_end"):
|
|
234
|
+
await r.hub.broadcast(msg)
|
|
235
|
+
r.touch_session()
|
|
236
|
+
r.recorder.record_event(msg)
|
|
237
|
+
|
|
238
|
+
accepted += 1
|
|
239
|
+
|
|
240
|
+
return {"accepted": accepted, "total": len(payload.events), "room": room}
|
|
241
|
+
|
|
242
|
+
# ── Sessions ──────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
@app.get("/sessions")
|
|
245
|
+
async def list_sessions(room: str = Query(default=DEFAULT_ROOM)):
|
|
246
|
+
return {"sessions": get_room(room).recorder.list_sessions(), "room": room}
|
|
247
|
+
|
|
248
|
+
@app.get("/sessions/{session_id}/events")
|
|
249
|
+
async def get_session_events(session_id: str, room: str = Query(default=DEFAULT_ROOM)):
|
|
250
|
+
return {"events": get_room(room).recorder.get_session_events(session_id)}
|
|
251
|
+
|
|
252
|
+
@app.post("/sessions/{session_id}/replay")
|
|
253
|
+
async def replay_session(session_id: str, room: str = Query(default=DEFAULT_ROOM)):
|
|
254
|
+
r = get_room(room)
|
|
255
|
+
events = r.recorder.get_session_events(session_id)
|
|
256
|
+
if not events:
|
|
257
|
+
raise HTTPException(status_code=404, detail="Session not found or empty")
|
|
258
|
+
asyncio.create_task(_replay(events, r.hub))
|
|
259
|
+
return {"status": "replaying", "event_count": len(events)}
|
|
260
|
+
|
|
261
|
+
@app.delete("/sessions/{session_id}")
|
|
262
|
+
async def delete_session(session_id: str, room: str = Query(default=DEFAULT_ROOM)):
|
|
263
|
+
get_room(room).recorder.delete_session(session_id)
|
|
264
|
+
return {"status": "deleted"}
|
|
265
|
+
|
|
266
|
+
# ── Health ────────────────────────────────────────────────────────────────
|
|
267
|
+
|
|
268
|
+
@app.get("/health")
|
|
269
|
+
async def health():
|
|
270
|
+
return {
|
|
271
|
+
"status": "ok",
|
|
272
|
+
"rooms": {
|
|
273
|
+
rid: {
|
|
274
|
+
"agents": len(r.registry.snapshot()),
|
|
275
|
+
"browsers": len(r.hub),
|
|
276
|
+
}
|
|
277
|
+
for rid, r in _rooms.items()
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@app.get("/rooms")
|
|
282
|
+
async def list_rooms():
|
|
283
|
+
return {
|
|
284
|
+
"rooms": [
|
|
285
|
+
{
|
|
286
|
+
"id": rid,
|
|
287
|
+
"agents": len(r.registry.snapshot()),
|
|
288
|
+
"browsers": len(r.hub),
|
|
289
|
+
}
|
|
290
|
+
for rid, r in _rooms.items()
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# ── Static UI ─────────────────────────────────────────────────────────────
|
|
295
|
+
|
|
296
|
+
if CORE_DIR and CORE_DIR.exists():
|
|
297
|
+
app.mount("/static/agentviz-core", StaticFiles(directory=str(CORE_DIR)), name="core")
|
|
298
|
+
|
|
299
|
+
if STATIC_DIR.exists():
|
|
300
|
+
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
|
301
|
+
|
|
302
|
+
@app.get("/")
|
|
303
|
+
async def index():
|
|
304
|
+
index_html = STATIC_DIR / "index.html"
|
|
305
|
+
if index_html.exists():
|
|
306
|
+
return FileResponse(str(index_html))
|
|
307
|
+
return HTMLResponse("<h1>AgentViz server running</h1><p>Static UI not found.</p>")
|
|
308
|
+
|
|
309
|
+
return app
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# ── Replay helper ──────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
async def _replay(events: list[dict], hub):
|
|
315
|
+
await hub.broadcast({"type": "replay_start", "event_count": len(events)})
|
|
316
|
+
for i, ev in enumerate(events):
|
|
317
|
+
if i > 0:
|
|
318
|
+
delay = min((ev["timestamp"] - events[i - 1]["timestamp"]), 3.0)
|
|
319
|
+
if delay > 0:
|
|
320
|
+
await asyncio.sleep(delay)
|
|
321
|
+
await hub.broadcast(ev["data"])
|
|
322
|
+
await hub.broadcast({"type": "replay_end"})
|
agentviz/cli.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agentviz CLI
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
agentviz serve [--host HOST] [--port PORT] [--no-browser]
|
|
6
|
+
agentviz --version
|
|
7
|
+
agentviz --help
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
import webbrowser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def cli():
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="agentviz",
|
|
20
|
+
description="AgentViz — real-time 3D visualization for multi-agent AI systems",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("--version", action="store_true", help="Print version and exit")
|
|
23
|
+
|
|
24
|
+
sub = parser.add_subparsers(dest="command")
|
|
25
|
+
|
|
26
|
+
serve_p = sub.add_parser("serve", help="Start the AgentViz visualization server")
|
|
27
|
+
serve_p.add_argument("--host", default="0.0.0.0", help="Bind host (default: 0.0.0.0)")
|
|
28
|
+
serve_p.add_argument("--port", type=int, default=8000, help="Bind port (default: 8000)")
|
|
29
|
+
serve_p.add_argument(
|
|
30
|
+
"--no-browser", action="store_true", help="Don't open browser automatically"
|
|
31
|
+
)
|
|
32
|
+
serve_p.add_argument(
|
|
33
|
+
"--demo", action="store_true",
|
|
34
|
+
help="Also start the demo orchestrator + agents on ports 8100-8103",
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
args = parser.parse_args()
|
|
38
|
+
|
|
39
|
+
if args.version:
|
|
40
|
+
from agentviz import __version__
|
|
41
|
+
print(f"agentviz {__version__}")
|
|
42
|
+
sys.exit(0)
|
|
43
|
+
|
|
44
|
+
if args.command == "serve":
|
|
45
|
+
_serve(args)
|
|
46
|
+
else:
|
|
47
|
+
parser.print_help()
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _serve(args):
|
|
52
|
+
import uvicorn
|
|
53
|
+
from agentviz._server_app import create_app
|
|
54
|
+
|
|
55
|
+
app = create_app()
|
|
56
|
+
|
|
57
|
+
url = f"http://localhost:{args.port}"
|
|
58
|
+
|
|
59
|
+
if not args.no_browser:
|
|
60
|
+
# Open browser after a short delay so the server has time to bind
|
|
61
|
+
def _open():
|
|
62
|
+
time.sleep(1.2)
|
|
63
|
+
webbrowser.open(url)
|
|
64
|
+
|
|
65
|
+
threading.Thread(target=_open, daemon=True).start()
|
|
66
|
+
|
|
67
|
+
print(f"\n AgentViz v{_version()}")
|
|
68
|
+
print(f" Listening on {url}")
|
|
69
|
+
print(f"\n Connect your agents:")
|
|
70
|
+
print(f" Python SDK → ws://localhost:{args.port}/agent-ws")
|
|
71
|
+
print(f" JS SDK → ws://localhost:{args.port}/agent-ws")
|
|
72
|
+
print(f" HTTP ingest → POST http://localhost:{args.port}/ingest\n")
|
|
73
|
+
|
|
74
|
+
if args.demo:
|
|
75
|
+
_start_demo_processes()
|
|
76
|
+
|
|
77
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="warning")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _start_demo_processes():
|
|
81
|
+
"""Launch the demo orchestrator + agents as subprocesses."""
|
|
82
|
+
import subprocess
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
|
|
85
|
+
agents_dir = Path(__file__).parent.parent / "agents"
|
|
86
|
+
if not agents_dir.exists():
|
|
87
|
+
print(" [demo] agents/ directory not found — skipping demo agents")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
scripts = [
|
|
91
|
+
("orchestrator", agents_dir / "orchestrator.py"),
|
|
92
|
+
("alpha", agents_dir / "alpha_agent.py"),
|
|
93
|
+
("beta", agents_dir / "beta_agent.py"),
|
|
94
|
+
("gamma", agents_dir / "gamma_agent.py"),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
for name, script in scripts:
|
|
98
|
+
if script.exists():
|
|
99
|
+
subprocess.Popen(
|
|
100
|
+
[sys.executable, str(script)],
|
|
101
|
+
stdout=subprocess.DEVNULL,
|
|
102
|
+
stderr=subprocess.DEVNULL,
|
|
103
|
+
)
|
|
104
|
+
print(f" [demo] started {name}")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _version():
|
|
108
|
+
try:
|
|
109
|
+
from agentviz import __version__
|
|
110
|
+
return __version__
|
|
111
|
+
except Exception:
|
|
112
|
+
return "?"
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
if __name__ == "__main__":
|
|
116
|
+
cli()
|
agentviz/config.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AgentViz global configuration
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Reads from environment variables so no code changes are needed to enable
|
|
5
|
+
visualization — just set the env vars before running your agents.
|
|
6
|
+
|
|
7
|
+
Environment variables
|
|
8
|
+
---------------------
|
|
9
|
+
AGENTVIZ_SERVER Base URL of the AgentViz server.
|
|
10
|
+
Default: http://localhost:8000
|
|
11
|
+
Example: https://agentviz.yourdomain.com
|
|
12
|
+
|
|
13
|
+
AGENTVIZ_PROJECT Project / room name. Each project gets its own isolated
|
|
14
|
+
3D scene. Multiple agents in the same project share the
|
|
15
|
+
same visualization.
|
|
16
|
+
Default: "default"
|
|
17
|
+
|
|
18
|
+
AGENTVIZ_ENABLED Set to "false" to disable all telemetry without removing
|
|
19
|
+
@trace decorators from your code.
|
|
20
|
+
Default: "true"
|
|
21
|
+
|
|
22
|
+
Usage
|
|
23
|
+
-----
|
|
24
|
+
# Option A — env vars only (no code changes)
|
|
25
|
+
# export AGENTVIZ_SERVER=https://agentviz.yourdomain.com
|
|
26
|
+
# export AGENTVIZ_PROJECT=my-team
|
|
27
|
+
|
|
28
|
+
# Option B — programmatic (call before importing agents)
|
|
29
|
+
import agentviz
|
|
30
|
+
agentviz.init(server="https://agentviz.yourdomain.com", project="my-team")
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
from typing import Optional
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ── Defaults ─────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
_DEFAULTS = {
|
|
42
|
+
"server": "http://localhost:8000",
|
|
43
|
+
"project": "default",
|
|
44
|
+
"enabled": True,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# ── Live config dict (mutated by init / configure) ───────────────────────────
|
|
48
|
+
|
|
49
|
+
_cfg: dict = {
|
|
50
|
+
"server": os.getenv("AGENTVIZ_SERVER", _DEFAULTS["server"]),
|
|
51
|
+
"project": os.getenv("AGENTVIZ_PROJECT", _DEFAULTS["project"]),
|
|
52
|
+
"enabled": os.getenv("AGENTVIZ_ENABLED", "true").strip().lower() != "false",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ── Public API ────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
def init(
|
|
59
|
+
server: Optional[str] = None,
|
|
60
|
+
project: Optional[str] = None,
|
|
61
|
+
enabled: Optional[bool] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Configure AgentViz once at the top of your program.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
server Base URL of the AgentViz server.
|
|
69
|
+
e.g. "https://agentviz.yourdomain.com"
|
|
70
|
+
project Project / room name. Agents with the same project appear in
|
|
71
|
+
the same 3D scene. Agents in different projects are isolated.
|
|
72
|
+
enabled Set False to silence all telemetry without removing decorators.
|
|
73
|
+
"""
|
|
74
|
+
if server is not None: _cfg["server"] = server.rstrip("/")
|
|
75
|
+
if project is not None: _cfg["project"] = project
|
|
76
|
+
if enabled is not None: _cfg["enabled"] = enabled
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# configure is an alias for init — same ergonomics as langsmith.configure()
|
|
80
|
+
configure = init
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get() -> dict:
|
|
84
|
+
"""Return a snapshot of the current configuration."""
|
|
85
|
+
return dict(_cfg)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def is_enabled() -> bool:
|
|
89
|
+
return bool(_cfg["enabled"])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def server_url() -> str:
|
|
93
|
+
return _cfg["server"]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def project() -> str:
|
|
97
|
+
return _cfg["project"]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def ws_url() -> str:
|
|
101
|
+
"""WebSocket URL derived from the base server URL."""
|
|
102
|
+
base = _cfg["server"]
|
|
103
|
+
# Normalise scheme
|
|
104
|
+
if base.startswith("https://"):
|
|
105
|
+
ws_base = "wss://" + base[len("https://"):]
|
|
106
|
+
elif base.startswith("http://"):
|
|
107
|
+
ws_base = "ws://" + base[len("http://"):]
|
|
108
|
+
else:
|
|
109
|
+
ws_base = base # already ws:// or wss://
|
|
110
|
+
return f"{ws_base}/agent-ws?room={_cfg['project']}"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def http_ingest_url() -> str:
|
|
114
|
+
"""HTTP ingest URL derived from the base server URL."""
|
|
115
|
+
base = _cfg["server"]
|
|
116
|
+
# Normalise to http(s)
|
|
117
|
+
if base.startswith("wss://"):
|
|
118
|
+
base = "https://" + base[len("wss://"):]
|
|
119
|
+
elif base.startswith("ws://"):
|
|
120
|
+
base = "http://" + base[len("ws://"):]
|
|
121
|
+
return f"{base}/ingest?room={_cfg['project']}"
|