universal-agent-context 0.2.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.
Files changed (47) hide show
  1. uacs/__init__.py +12 -0
  2. uacs/adapters/__init__.py +19 -0
  3. uacs/adapters/agent_skill_adapter.py +202 -0
  4. uacs/adapters/agents_md_adapter.py +330 -0
  5. uacs/adapters/base.py +261 -0
  6. uacs/adapters/clinerules_adapter.py +39 -0
  7. uacs/adapters/cursorrules_adapter.py +39 -0
  8. uacs/api.py +262 -0
  9. uacs/cli/__init__.py +6 -0
  10. uacs/cli/context.py +349 -0
  11. uacs/cli/main.py +195 -0
  12. uacs/cli/mcp.py +115 -0
  13. uacs/cli/memory.py +142 -0
  14. uacs/cli/packages.py +309 -0
  15. uacs/cli/skills.py +144 -0
  16. uacs/cli/utils.py +24 -0
  17. uacs/config/repositories.yaml +26 -0
  18. uacs/context/__init__.py +0 -0
  19. uacs/context/agent_context.py +406 -0
  20. uacs/context/shared_context.py +661 -0
  21. uacs/context/unified_context.py +332 -0
  22. uacs/mcp_server_entry.py +80 -0
  23. uacs/memory/__init__.py +5 -0
  24. uacs/memory/simple_memory.py +255 -0
  25. uacs/packages/__init__.py +26 -0
  26. uacs/packages/manager.py +413 -0
  27. uacs/packages/models.py +60 -0
  28. uacs/packages/sources.py +270 -0
  29. uacs/protocols/__init__.py +5 -0
  30. uacs/protocols/mcp/__init__.py +8 -0
  31. uacs/protocols/mcp/manager.py +77 -0
  32. uacs/protocols/mcp/skills_server.py +700 -0
  33. uacs/skills_validator.py +367 -0
  34. uacs/utils/__init__.py +5 -0
  35. uacs/utils/paths.py +24 -0
  36. uacs/visualization/README.md +132 -0
  37. uacs/visualization/__init__.py +36 -0
  38. uacs/visualization/models.py +195 -0
  39. uacs/visualization/static/index.html +857 -0
  40. uacs/visualization/storage.py +402 -0
  41. uacs/visualization/visualization.py +328 -0
  42. uacs/visualization/web_server.py +364 -0
  43. universal_agent_context-0.2.0.dist-info/METADATA +873 -0
  44. universal_agent_context-0.2.0.dist-info/RECORD +47 -0
  45. universal_agent_context-0.2.0.dist-info/WHEEL +4 -0
  46. universal_agent_context-0.2.0.dist-info/entry_points.txt +2 -0
  47. universal_agent_context-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,328 @@
1
+ """Visualization for shared context and agent interactions.
2
+
3
+ Provides real-time terminal visualizations of:
4
+ 1. Context flow between agents
5
+ 2. Token usage and compression
6
+ 3. Agent dependency graphs
7
+ 4. Memory efficiency
8
+ """
9
+
10
+ import time
11
+ from typing import Any
12
+
13
+ from rich import box
14
+ from rich.align import Align
15
+ from rich.console import Console
16
+ from rich.layout import Layout
17
+ from rich.live import Live
18
+ from rich.panel import Panel
19
+ from rich.table import Table
20
+ from rich.tree import Tree
21
+
22
+
23
+ class ContextVisualizer:
24
+ """Real-time visualization of shared context."""
25
+
26
+ def __init__(self, console: Console | None = None):
27
+ """Initialize visualizer.
28
+
29
+ Args:
30
+ console: Rich console instance
31
+ """
32
+ self.console = console or Console()
33
+
34
+ def render_context_graph(self, graph: dict[str, Any]) -> Panel:
35
+ """Render context graph as tree structure.
36
+
37
+ Args:
38
+ graph: Graph structure from SharedContextManager
39
+
40
+ Returns:
41
+ Rich Panel with graph visualization
42
+ """
43
+ tree = Tree("🧠 Shared Context Graph")
44
+
45
+ # Group nodes by type
46
+ entries = [n for n in graph["nodes"] if n["type"] == "entry"]
47
+ summaries = [n for n in graph["nodes"] if n["type"] == "summary"]
48
+
49
+ # Add entry branch
50
+ if entries:
51
+ entries_branch = tree.add("📝 Context Entries")
52
+ for entry in entries:
53
+ agent_icon = self._get_agent_icon(entry.get("agent", "unknown"))
54
+ entries_branch.add(
55
+ f"{agent_icon} {entry['id']} "
56
+ f"({entry['tokens']} tokens) "
57
+ f"[dim]{entry['timestamp'][:19]}[/dim]"
58
+ )
59
+
60
+ # Add summary branch
61
+ if summaries:
62
+ summaries_branch = tree.add("🗜️ Summaries")
63
+ for summary in summaries:
64
+ summaries_branch.add(
65
+ f"📦 {summary['id']} "
66
+ f"({summary['entry_count']} entries, "
67
+ f"{summary['tokens_saved']} tokens saved)"
68
+ )
69
+
70
+ # Add edges info
71
+ if graph["edges"]:
72
+ edges_branch = tree.add(f"🔗 References ({len(graph['edges'])})")
73
+ for edge in graph["edges"][:5]: # Show first 5
74
+ edges_branch.add(
75
+ f"{edge['source']} → {edge['target']} [dim]({edge['type']})[/dim]"
76
+ )
77
+
78
+ return Panel(
79
+ tree, title="Context Relationships", border_style="cyan", box=box.ROUNDED
80
+ )
81
+
82
+ def render_stats_table(self, stats: dict[str, Any]) -> Table:
83
+ """Render context statistics as table.
84
+
85
+ Args:
86
+ stats: Statistics from SharedContextManager
87
+
88
+ Returns:
89
+ Rich Table with statistics
90
+ """
91
+ table = Table(
92
+ title="💾 Context Statistics",
93
+ show_header=False,
94
+ box=box.SIMPLE,
95
+ padding=(0, 2),
96
+ )
97
+
98
+ table.add_column("Metric", style="cyan")
99
+ table.add_column("Value", style="green")
100
+
101
+ table.add_row("Entries", str(stats["entry_count"]))
102
+ table.add_row("Summaries", str(stats["summary_count"]))
103
+ table.add_row("Total Tokens", f"{stats['total_tokens']:,}")
104
+ table.add_row("Tokens Saved", f"{stats['tokens_saved']:,}")
105
+ table.add_row("Compression", stats["compression_ratio"])
106
+ table.add_row("Storage", f"{stats['storage_size_mb']:.2f} MB")
107
+
108
+ return table
109
+
110
+ def render_agent_flow(self, entries: list) -> Panel:
111
+ """Render agent interaction flow.
112
+
113
+ Args:
114
+ entries: List of context entries
115
+
116
+ Returns:
117
+ Rich Panel with flow visualization
118
+ """
119
+ flow_lines = []
120
+
121
+ for i, entry in enumerate(entries[-10:]): # Last 10 entries
122
+ agent = entry.get("agent", "unknown")
123
+ agent_icon = self._get_agent_icon(agent)
124
+ tokens = entry.get("token_estimate", 0)
125
+
126
+ # Create flow arrow
127
+ arrow = " └─>" if i > 0 else " ┌─>"
128
+ flow_lines.append(
129
+ f"{arrow} {agent_icon} [bold]{agent}[/bold] "
130
+ f"[dim]({tokens} tokens)[/dim]"
131
+ )
132
+
133
+ flow_text = "\n".join(flow_lines) if flow_lines else "[dim]No entries yet[/dim]"
134
+
135
+ return Panel(
136
+ flow_text,
137
+ title="🔄 Agent Interaction Flow",
138
+ border_style="blue",
139
+ box=box.ROUNDED,
140
+ )
141
+
142
+ def render_token_meter(self, used_tokens: int, max_tokens: int = 8000) -> Panel:
143
+ """Render token usage meter.
144
+
145
+ Args:
146
+ used_tokens: Current token count
147
+ max_tokens: Maximum tokens
148
+
149
+ Returns:
150
+ Rich Panel with token meter
151
+ """
152
+ percentage = (used_tokens / max_tokens) * 100
153
+ bar_width = 40
154
+ filled = int((percentage / 100) * bar_width)
155
+
156
+ # Choose color based on usage
157
+ if percentage < 50:
158
+ color = "green"
159
+ elif percentage < 80:
160
+ color = "yellow"
161
+ else:
162
+ color = "red"
163
+
164
+ bar = f"[{color}]{'█' * filled}[/]{color}{'░' * (bar_width - filled)}"
165
+
166
+ meter_text = f"""
167
+ {bar}
168
+
169
+ Used: {used_tokens:,} / {max_tokens:,} tokens ({percentage:.1f}%)
170
+ Remaining: {max_tokens - used_tokens:,} tokens
171
+ """
172
+
173
+ return Panel(
174
+ meter_text.strip(),
175
+ title="🎯 Token Budget",
176
+ border_style=color,
177
+ box=box.ROUNDED,
178
+ )
179
+
180
+ def render_dashboard(
181
+ self,
182
+ graph: dict[str, Any],
183
+ stats: dict[str, Any],
184
+ token_usage: dict[str, int] | None = None,
185
+ ) -> Layout:
186
+ """Render complete dashboard layout.
187
+
188
+ Args:
189
+ graph: Context graph
190
+ stats: Context statistics
191
+ token_usage: Token usage info
192
+
193
+ Returns:
194
+ Rich Layout with full dashboard
195
+ """
196
+ layout = Layout()
197
+
198
+ # Split into sections
199
+ layout.split_column(
200
+ Layout(name="header", size=3),
201
+ Layout(name="body"),
202
+ Layout(name="footer", size=3),
203
+ )
204
+
205
+ # Header
206
+ layout["header"].update(
207
+ Panel(Align.center("🤖 Multi-Agent Context Dashboard"), style="bold cyan")
208
+ )
209
+
210
+ # Body - split into left and right
211
+ layout["body"].split_row(Layout(name="left"), Layout(name="right"))
212
+
213
+ # Left side - graph and flow
214
+ layout["left"].split_column(Layout(name="graph"), Layout(name="flow"))
215
+
216
+ layout["graph"].update(self.render_context_graph(graph))
217
+
218
+ if graph["nodes"]:
219
+ layout["flow"].update(self.render_agent_flow(graph["nodes"]))
220
+
221
+ # Right side - stats and tokens
222
+ layout["right"].split_column(Layout(name="stats"), Layout(name="tokens"))
223
+
224
+ layout["stats"].update(Panel(self.render_stats_table(stats)))
225
+
226
+ if token_usage:
227
+ layout["tokens"].update(
228
+ self.render_token_meter(
229
+ token_usage.get("used", 0), token_usage.get("max", 8000)
230
+ )
231
+ )
232
+
233
+ # Footer
234
+ layout["footer"].update(
235
+ Panel(
236
+ "[dim]Press Ctrl+C to exit | Data updates every 2 seconds[/dim]",
237
+ style="dim",
238
+ )
239
+ )
240
+
241
+ return layout
242
+
243
+ def live_dashboard(self, context_manager, update_interval: float = 2.0):
244
+ """Run live updating dashboard.
245
+
246
+ Args:
247
+ context_manager: SharedContextManager instance
248
+ update_interval: Seconds between updates
249
+ """
250
+ with Live(
251
+ self.render_dashboard(
252
+ context_manager.get_context_graph(),
253
+ context_manager.get_stats(),
254
+ {
255
+ "used": sum(
256
+ e.token_estimate for e in context_manager.entries.values()
257
+ )
258
+ },
259
+ ),
260
+ refresh_per_second=1,
261
+ screen=True,
262
+ ) as live:
263
+ try:
264
+ while True:
265
+ time.sleep(update_interval)
266
+
267
+ # Update dashboard
268
+ graph = context_manager.get_context_graph()
269
+ stats = context_manager.get_stats()
270
+ token_usage = {
271
+ "used": sum(
272
+ e.token_estimate for e in context_manager.entries.values()
273
+ ),
274
+ "max": 8000,
275
+ }
276
+
277
+ live.update(self.render_dashboard(graph, stats, token_usage))
278
+ except KeyboardInterrupt:
279
+ pass
280
+
281
+ def _get_agent_icon(self, agent: str) -> str:
282
+ """Get icon for agent type.
283
+
284
+ Args:
285
+ agent: Agent name
286
+
287
+ Returns:
288
+ Icon emoji
289
+ """
290
+ icons = {
291
+ "claude": "🔵",
292
+ "gemini": "🟣",
293
+ "copilot": "🟢",
294
+ "openai": "🟡",
295
+ "orchestrator": "🎭",
296
+ }
297
+ return icons.get(agent.lower(), "⚪")
298
+
299
+ def render_compression_viz(
300
+ self, before_tokens: int, after_tokens: int, method: str = "summary"
301
+ ) -> Panel:
302
+ """Visualize compression effect.
303
+
304
+ Args:
305
+ before_tokens: Tokens before compression
306
+ after_tokens: Tokens after compression
307
+ method: Compression method used
308
+
309
+ Returns:
310
+ Rich Panel showing compression
311
+ """
312
+ savings = before_tokens - after_tokens
313
+ percentage = (savings / before_tokens * 100) if before_tokens > 0 else 0
314
+
315
+ viz = f"""
316
+ Before: {"█" * int(before_tokens / 100)} ({before_tokens:,} tokens)
317
+ After: {"█" * int(after_tokens / 100)} ({after_tokens:,} tokens)
318
+
319
+ [green]Saved: {savings:,} tokens ({percentage:.1f}%)[/green]
320
+ Method: {method}
321
+ """
322
+
323
+ return Panel(
324
+ viz.strip(),
325
+ title="🗜️ Compression Effect",
326
+ border_style="green",
327
+ box=box.ROUNDED,
328
+ )
@@ -0,0 +1,364 @@
1
+ """Web server for UACS Context Graph Visualization.
2
+
3
+ Provides a FastAPI HTTP server with WebSocket support for real-time
4
+ visualization of context graphs, token usage, and deduplication.
5
+ """
6
+
7
+ import asyncio
8
+ import json
9
+ import logging
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from fastapi import FastAPI, WebSocket, WebSocketDisconnect
14
+ from fastapi.responses import HTMLResponse, JSONResponse
15
+ from fastapi.staticfiles import StaticFiles
16
+ from starlette.middleware.cors import CORSMiddleware
17
+
18
+ from uacs.context.shared_context import SharedContextManager
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ class VisualizationServer:
24
+ """Web server for context visualization."""
25
+
26
+ def __init__(
27
+ self,
28
+ context_manager: SharedContextManager,
29
+ host: str = "localhost",
30
+ port: int = 8081,
31
+ ):
32
+ """Initialize visualization server.
33
+
34
+ Args:
35
+ context_manager: SharedContextManager instance
36
+ host: Server host
37
+ port: Server port
38
+ """
39
+ self.context_manager = context_manager
40
+ self.host = host
41
+ self.port = port
42
+ self.app = FastAPI(title="UACS Context Visualizer")
43
+
44
+ # Store active WebSocket connections
45
+ self.active_connections: list[WebSocket] = []
46
+
47
+ # Setup CORS
48
+ self.app.add_middleware(
49
+ CORSMiddleware,
50
+ allow_origins=["*"],
51
+ allow_credentials=True,
52
+ allow_methods=["*"],
53
+ allow_headers=["*"],
54
+ )
55
+
56
+ # Setup routes
57
+ self._setup_routes()
58
+
59
+ # Setup static files
60
+ static_dir = Path(__file__).parent / "static"
61
+ if static_dir.exists():
62
+ self.app.mount("/static", StaticFiles(directory=str(static_dir)), name="static")
63
+
64
+ def _setup_routes(self):
65
+ """Setup API routes."""
66
+
67
+ @self.app.get("/", response_class=HTMLResponse)
68
+ async def index():
69
+ """Serve main visualization page."""
70
+ html_file = Path(__file__).parent / "static" / "index.html"
71
+ if html_file.exists():
72
+ return html_file.read_text()
73
+ return HTMLResponse(content=self._get_default_html(), status_code=200)
74
+
75
+ @self.app.get("/api/graph")
76
+ async def get_graph():
77
+ """Get context graph data.
78
+
79
+ Returns:
80
+ JSON representation of context graph
81
+ """
82
+ try:
83
+ graph = self.context_manager.get_context_graph()
84
+ return JSONResponse(content=graph)
85
+ except Exception as e:
86
+ logger.error("Error getting graph: %s", e)
87
+ return JSONResponse(
88
+ content={"error": str(e)},
89
+ status_code=500
90
+ )
91
+
92
+ @self.app.get("/api/stats")
93
+ async def get_stats():
94
+ """Get token and compression statistics.
95
+
96
+ Returns:
97
+ JSON representation of stats
98
+ """
99
+ try:
100
+ stats = self.context_manager.get_stats()
101
+ return JSONResponse(content=stats)
102
+ except Exception as e:
103
+ logger.error("Error getting stats: %s", e)
104
+ return JSONResponse(
105
+ content={"error": str(e)},
106
+ status_code=500
107
+ )
108
+
109
+ @self.app.get("/api/topics")
110
+ async def get_topics():
111
+ """Get topic clusters from context entries.
112
+
113
+ Returns:
114
+ JSON representation of topic clusters
115
+ """
116
+ try:
117
+ topics = self._get_topic_clusters()
118
+ return JSONResponse(content=topics)
119
+ except Exception as e:
120
+ logger.error("Error getting topics: %s", e)
121
+ return JSONResponse(
122
+ content={"error": str(e)},
123
+ status_code=500
124
+ )
125
+
126
+ @self.app.get("/api/deduplication")
127
+ async def get_deduplication():
128
+ """Get deduplication information.
129
+
130
+ Returns:
131
+ JSON representation of duplicate content areas
132
+ """
133
+ try:
134
+ dedup_data = self._get_deduplication_data()
135
+ return JSONResponse(content=dedup_data)
136
+ except Exception as e:
137
+ logger.error("Error getting deduplication data: %s", e)
138
+ return JSONResponse(
139
+ content={"error": str(e)},
140
+ status_code=500
141
+ )
142
+
143
+ @self.app.get("/api/quality")
144
+ async def get_quality():
145
+ """Get quality distribution of context entries.
146
+
147
+ Returns:
148
+ JSON representation of quality scores
149
+ """
150
+ try:
151
+ quality_data = self._get_quality_distribution()
152
+ return JSONResponse(content=quality_data)
153
+ except Exception as e:
154
+ logger.error("Error getting quality data: %s", e)
155
+ return JSONResponse(
156
+ content={"error": str(e)},
157
+ status_code=500
158
+ )
159
+
160
+ @self.app.websocket("/ws")
161
+ async def websocket_endpoint(websocket: WebSocket):
162
+ """WebSocket endpoint for real-time updates."""
163
+ await self._handle_websocket(websocket)
164
+
165
+ @self.app.get("/health")
166
+ async def health():
167
+ """Health check endpoint."""
168
+ return JSONResponse(content={"status": "ok"})
169
+
170
+ async def _handle_websocket(self, websocket: WebSocket):
171
+ """Handle WebSocket connection for real-time updates.
172
+
173
+ Args:
174
+ websocket: WebSocket connection
175
+ """
176
+ await websocket.accept()
177
+ self.active_connections.append(websocket)
178
+
179
+ try:
180
+ while True:
181
+ # Send updates every 2 seconds
182
+ await asyncio.sleep(2)
183
+
184
+ data = {
185
+ "type": "update",
186
+ "graph": self.context_manager.get_context_graph(),
187
+ "stats": self.context_manager.get_stats(),
188
+ "topics": self._get_topic_clusters(),
189
+ "quality": self._get_quality_distribution(),
190
+ }
191
+
192
+ await websocket.send_json(data)
193
+ except WebSocketDisconnect:
194
+ self.active_connections.remove(websocket)
195
+ except Exception as e:
196
+ logger.error("WebSocket error: %s", e)
197
+ if websocket in self.active_connections:
198
+ self.active_connections.remove(websocket)
199
+
200
+ def _get_topic_clusters(self) -> dict[str, Any]:
201
+ """Get topic clusters from context entries.
202
+
203
+ Returns:
204
+ Dictionary with topic cluster information
205
+ """
206
+ # Count topics across entries
207
+ topic_counts: dict[str, int] = {}
208
+ topic_entries: dict[str, list[str]] = {}
209
+
210
+ for entry in self.context_manager.entries.values():
211
+ for topic in entry.topics:
212
+ topic_counts[topic] = topic_counts.get(topic, 0) + 1
213
+ if topic not in topic_entries:
214
+ topic_entries[topic] = []
215
+ topic_entries[topic].append(entry.id)
216
+
217
+ # Create clusters
218
+ clusters = []
219
+ for topic, count in topic_counts.items():
220
+ clusters.append({
221
+ "topic": topic,
222
+ "count": count,
223
+ "entries": topic_entries[topic],
224
+ })
225
+
226
+ # Sort by count
227
+ clusters.sort(key=lambda x: x["count"], reverse=True)
228
+
229
+ return {
230
+ "clusters": clusters,
231
+ "total_topics": len(clusters),
232
+ }
233
+
234
+ def _get_deduplication_data(self) -> dict[str, Any]:
235
+ """Get deduplication information.
236
+
237
+ Returns:
238
+ Dictionary with deduplication statistics
239
+ """
240
+ # Get all unique hashes
241
+ unique_hashes = set(entry.hash for entry in self.context_manager.entries.values())
242
+
243
+ # Count duplicates prevented by dedup index
244
+ total_possible = len(self.context_manager.entries) + len(self.context_manager.dedup_index) - len(unique_hashes)
245
+ duplicates_prevented = len(self.context_manager.dedup_index) - len(unique_hashes)
246
+
247
+ return {
248
+ "unique_entries": len(unique_hashes),
249
+ "total_entries": len(self.context_manager.entries),
250
+ "duplicates_prevented": duplicates_prevented,
251
+ "deduplication_rate": f"{(duplicates_prevented / total_possible * 100):.1f}%" if total_possible > 0 else "0%",
252
+ }
253
+
254
+ def _get_quality_distribution(self) -> dict[str, Any]:
255
+ """Get quality distribution of entries.
256
+
257
+ Returns:
258
+ Dictionary with quality distribution data
259
+ """
260
+ qualities = [entry.quality for entry in self.context_manager.entries.values()]
261
+
262
+ if not qualities:
263
+ return {
264
+ "distribution": [],
265
+ "average": 0,
266
+ "high_quality": 0,
267
+ "medium_quality": 0,
268
+ "low_quality": 0,
269
+ }
270
+
271
+ # Create distribution buckets
272
+ high_quality = sum(1 for q in qualities if q >= 0.8)
273
+ medium_quality = sum(1 for q in qualities if 0.5 <= q < 0.8)
274
+ low_quality = sum(1 for q in qualities if q < 0.5)
275
+
276
+ distribution = [
277
+ {"range": "High (0.8-1.0)", "count": high_quality},
278
+ {"range": "Medium (0.5-0.8)", "count": medium_quality},
279
+ {"range": "Low (0-0.5)", "count": low_quality},
280
+ ]
281
+
282
+ return {
283
+ "distribution": distribution,
284
+ "average": sum(qualities) / len(qualities),
285
+ "high_quality": high_quality,
286
+ "medium_quality": medium_quality,
287
+ "low_quality": low_quality,
288
+ }
289
+
290
+ def _get_default_html(self) -> str:
291
+ """Get default HTML if static file doesn't exist.
292
+
293
+ Returns:
294
+ HTML string
295
+ """
296
+ return """
297
+ <!DOCTYPE html>
298
+ <html>
299
+ <head>
300
+ <title>UACS Context Visualizer</title>
301
+ </head>
302
+ <body>
303
+ <h1>UACS Context Visualizer</h1>
304
+ <p>Static files not found. Please check the installation.</p>
305
+ </body>
306
+ </html>
307
+ """
308
+
309
+ async def broadcast_update(self, data: dict[str, Any]):
310
+ """Broadcast update to all connected WebSocket clients.
311
+
312
+ Args:
313
+ data: Data to broadcast
314
+ """
315
+ disconnected = []
316
+
317
+ for connection in self.active_connections:
318
+ try:
319
+ await connection.send_json(data)
320
+ except Exception as e:
321
+ logger.error("Error broadcasting to connection: %s", e)
322
+ disconnected.append(connection)
323
+
324
+ # Remove disconnected clients
325
+ for connection in disconnected:
326
+ self.active_connections.remove(connection)
327
+
328
+
329
+ async def start_visualization_server(
330
+ context_manager: SharedContextManager,
331
+ host: str = "localhost",
332
+ port: int = 8081,
333
+ ) -> VisualizationServer:
334
+ """Start the visualization server.
335
+
336
+ Args:
337
+ context_manager: SharedContextManager instance
338
+ host: Server host
339
+ port: Server port
340
+
341
+ Returns:
342
+ VisualizationServer instance
343
+ """
344
+ import uvicorn
345
+
346
+ server = VisualizationServer(context_manager, host, port)
347
+
348
+ # Create server config
349
+ config = uvicorn.Config(
350
+ server.app,
351
+ host=host,
352
+ port=port,
353
+ log_level="info",
354
+ )
355
+
356
+ # Start server in background
357
+ uvicorn_server = uvicorn.Server(config)
358
+
359
+ logger.info("Starting visualization server on %s:%s", host, port)
360
+
361
+ # Run server
362
+ await uvicorn_server.serve()
363
+
364
+ return server