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.
- uacs/__init__.py +12 -0
- uacs/adapters/__init__.py +19 -0
- uacs/adapters/agent_skill_adapter.py +202 -0
- uacs/adapters/agents_md_adapter.py +330 -0
- uacs/adapters/base.py +261 -0
- uacs/adapters/clinerules_adapter.py +39 -0
- uacs/adapters/cursorrules_adapter.py +39 -0
- uacs/api.py +262 -0
- uacs/cli/__init__.py +6 -0
- uacs/cli/context.py +349 -0
- uacs/cli/main.py +195 -0
- uacs/cli/mcp.py +115 -0
- uacs/cli/memory.py +142 -0
- uacs/cli/packages.py +309 -0
- uacs/cli/skills.py +144 -0
- uacs/cli/utils.py +24 -0
- uacs/config/repositories.yaml +26 -0
- uacs/context/__init__.py +0 -0
- uacs/context/agent_context.py +406 -0
- uacs/context/shared_context.py +661 -0
- uacs/context/unified_context.py +332 -0
- uacs/mcp_server_entry.py +80 -0
- uacs/memory/__init__.py +5 -0
- uacs/memory/simple_memory.py +255 -0
- uacs/packages/__init__.py +26 -0
- uacs/packages/manager.py +413 -0
- uacs/packages/models.py +60 -0
- uacs/packages/sources.py +270 -0
- uacs/protocols/__init__.py +5 -0
- uacs/protocols/mcp/__init__.py +8 -0
- uacs/protocols/mcp/manager.py +77 -0
- uacs/protocols/mcp/skills_server.py +700 -0
- uacs/skills_validator.py +367 -0
- uacs/utils/__init__.py +5 -0
- uacs/utils/paths.py +24 -0
- uacs/visualization/README.md +132 -0
- uacs/visualization/__init__.py +36 -0
- uacs/visualization/models.py +195 -0
- uacs/visualization/static/index.html +857 -0
- uacs/visualization/storage.py +402 -0
- uacs/visualization/visualization.py +328 -0
- uacs/visualization/web_server.py +364 -0
- universal_agent_context-0.2.0.dist-info/METADATA +873 -0
- universal_agent_context-0.2.0.dist-info/RECORD +47 -0
- universal_agent_context-0.2.0.dist-info/WHEEL +4 -0
- universal_agent_context-0.2.0.dist-info/entry_points.txt +2 -0
- 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
|