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
uacs/skills_validator.py
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""Agent Skills SKILL.md validator based on agentskills.io specification.
|
|
2
|
+
|
|
3
|
+
Validates SKILL.md files against the official Agent Skills format specification:
|
|
4
|
+
- YAML frontmatter validation
|
|
5
|
+
- Required fields: name, description
|
|
6
|
+
- Allowed fields: name, description, license, allowed-tools, metadata, compatibility
|
|
7
|
+
- Name constraints: kebab-case, max 64 chars, no leading/trailing hyphens
|
|
8
|
+
- Description: max 1024 chars
|
|
9
|
+
- Compatibility: max 500 chars
|
|
10
|
+
- Directory name must match skill name
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
import unicodedata
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, ClassVar
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ValidationError:
|
|
24
|
+
"""A single validation error."""
|
|
25
|
+
|
|
26
|
+
field: str
|
|
27
|
+
message: str
|
|
28
|
+
line: int | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ValidationResult:
|
|
33
|
+
"""Result of validating a SKILL.md file."""
|
|
34
|
+
|
|
35
|
+
valid: bool
|
|
36
|
+
errors: list[ValidationError]
|
|
37
|
+
warnings: list[ValidationError]
|
|
38
|
+
metadata: dict[str, Any] | None = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class SkillValidator:
|
|
42
|
+
"""Validates Agent Skills SKILL.md files against specification.
|
|
43
|
+
|
|
44
|
+
See https://agentskills.io/specification for full format specification.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
# Field constraints from spec
|
|
48
|
+
MAX_NAME_LENGTH: ClassVar[int] = 64
|
|
49
|
+
MAX_DESCRIPTION_LENGTH: ClassVar[int] = 1024
|
|
50
|
+
MAX_COMPATIBILITY_LENGTH: ClassVar[int] = 500
|
|
51
|
+
|
|
52
|
+
# Allowed frontmatter fields
|
|
53
|
+
ALLOWED_FIELDS: ClassVar[set[str]] = {
|
|
54
|
+
"name",
|
|
55
|
+
"description",
|
|
56
|
+
"license",
|
|
57
|
+
"allowed-tools",
|
|
58
|
+
"metadata",
|
|
59
|
+
"compatibility",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Required fields
|
|
63
|
+
REQUIRED_FIELDS: ClassVar[set[str]] = {"name", "description"}
|
|
64
|
+
|
|
65
|
+
# Name pattern: lowercase letters, numbers, hyphens only
|
|
66
|
+
# Must not start or end with hyphen
|
|
67
|
+
NAME_PATTERN: ClassVar[re.Pattern] = re.compile(r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?$")
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def normalize_unicode(text: str) -> str:
|
|
71
|
+
"""Normalize Unicode text to NFC form."""
|
|
72
|
+
return unicodedata.normalize("NFC", text)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def validate_name(name: str) -> list[ValidationError]:
|
|
76
|
+
"""Validate skill name against spec constraints.
|
|
77
|
+
|
|
78
|
+
Constraints:
|
|
79
|
+
- Max 64 characters
|
|
80
|
+
- Lowercase letters, numbers, and hyphens only
|
|
81
|
+
- Must not start or end with a hyphen
|
|
82
|
+
- No consecutive hyphens
|
|
83
|
+
- Unicode normalized (NFC)
|
|
84
|
+
"""
|
|
85
|
+
errors = []
|
|
86
|
+
|
|
87
|
+
if not name:
|
|
88
|
+
errors.append(ValidationError("name", "Name is required"))
|
|
89
|
+
return errors
|
|
90
|
+
|
|
91
|
+
# Normalize Unicode
|
|
92
|
+
normalized = SkillValidator.normalize_unicode(name)
|
|
93
|
+
if name != normalized:
|
|
94
|
+
errors.append(
|
|
95
|
+
ValidationError(
|
|
96
|
+
"name",
|
|
97
|
+
f"Name must be Unicode normalized (NFC). "
|
|
98
|
+
f"Got: {name!r}, expected: {normalized!r}",
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Length check
|
|
103
|
+
if len(name) > SkillValidator.MAX_NAME_LENGTH:
|
|
104
|
+
errors.append(
|
|
105
|
+
ValidationError(
|
|
106
|
+
"name",
|
|
107
|
+
f"Name exceeds maximum length of {SkillValidator.MAX_NAME_LENGTH} "
|
|
108
|
+
"characters",
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Pattern check (kebab-case)
|
|
113
|
+
if not SkillValidator.NAME_PATTERN.match(name):
|
|
114
|
+
errors.append(
|
|
115
|
+
ValidationError(
|
|
116
|
+
"name",
|
|
117
|
+
"Name must contain only lowercase letters, numbers, and hyphens. "
|
|
118
|
+
"Must not start or end with a hyphen.",
|
|
119
|
+
)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Check for consecutive hyphens
|
|
123
|
+
if "--" in name:
|
|
124
|
+
errors.append(
|
|
125
|
+
ValidationError("name", "Name must not contain consecutive hyphens")
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return errors
|
|
129
|
+
|
|
130
|
+
@staticmethod
|
|
131
|
+
def validate_description(description: str) -> list[ValidationError]:
|
|
132
|
+
"""Validate skill description against spec constraints.
|
|
133
|
+
|
|
134
|
+
Constraints:
|
|
135
|
+
- Required
|
|
136
|
+
- Max 1024 characters
|
|
137
|
+
- Non-empty
|
|
138
|
+
"""
|
|
139
|
+
errors = []
|
|
140
|
+
|
|
141
|
+
if not description:
|
|
142
|
+
errors.append(ValidationError("description", "Description is required"))
|
|
143
|
+
return errors
|
|
144
|
+
|
|
145
|
+
if not description.strip():
|
|
146
|
+
errors.append(
|
|
147
|
+
ValidationError("description", "Description must not be empty")
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if len(description) > SkillValidator.MAX_DESCRIPTION_LENGTH:
|
|
151
|
+
errors.append(
|
|
152
|
+
ValidationError(
|
|
153
|
+
"description",
|
|
154
|
+
f"Description exceeds maximum length of "
|
|
155
|
+
f"{SkillValidator.MAX_DESCRIPTION_LENGTH} characters",
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return errors
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def validate_compatibility(compatibility: str | None) -> list[ValidationError]:
|
|
163
|
+
"""Validate compatibility field against spec constraints.
|
|
164
|
+
|
|
165
|
+
Constraints:
|
|
166
|
+
- Optional
|
|
167
|
+
- Max 500 characters
|
|
168
|
+
"""
|
|
169
|
+
errors = []
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
compatibility
|
|
173
|
+
and len(compatibility) > SkillValidator.MAX_COMPATIBILITY_LENGTH
|
|
174
|
+
):
|
|
175
|
+
errors.append(
|
|
176
|
+
ValidationError(
|
|
177
|
+
"compatibility",
|
|
178
|
+
f"Compatibility exceeds maximum length of "
|
|
179
|
+
f"{SkillValidator.MAX_COMPATIBILITY_LENGTH} characters",
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
return errors
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def validate_frontmatter_fields(
|
|
187
|
+
frontmatter: dict[str, Any],
|
|
188
|
+
) -> list[ValidationError]:
|
|
189
|
+
"""Validate that only allowed fields are present in frontmatter."""
|
|
190
|
+
errors = []
|
|
191
|
+
|
|
192
|
+
extra_fields = set(frontmatter.keys()) - SkillValidator.ALLOWED_FIELDS
|
|
193
|
+
if extra_fields:
|
|
194
|
+
errors.append(
|
|
195
|
+
ValidationError(
|
|
196
|
+
"frontmatter",
|
|
197
|
+
f"Unexpected fields in frontmatter: "
|
|
198
|
+
f"{', '.join(sorted(extra_fields))}. "
|
|
199
|
+
f"Allowed fields: "
|
|
200
|
+
f"{', '.join(sorted(SkillValidator.ALLOWED_FIELDS))}",
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
missing_fields = SkillValidator.REQUIRED_FIELDS - set(frontmatter.keys())
|
|
204
|
+
if missing_fields:
|
|
205
|
+
errors.append(
|
|
206
|
+
ValidationError(
|
|
207
|
+
"frontmatter",
|
|
208
|
+
f"Missing required fields: {', '.join(sorted(missing_fields))}",
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return errors
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def validate_directory_name(
|
|
216
|
+
skill_path: Path, skill_name: str
|
|
217
|
+
) -> list[ValidationError]:
|
|
218
|
+
"""Validate that directory name matches skill name.
|
|
219
|
+
|
|
220
|
+
Directory name must match the skill name from frontmatter (kebab-case).
|
|
221
|
+
"""
|
|
222
|
+
errors = []
|
|
223
|
+
|
|
224
|
+
dir_name = skill_path.name
|
|
225
|
+
if dir_name != skill_name:
|
|
226
|
+
errors.append(
|
|
227
|
+
ValidationError(
|
|
228
|
+
"directory",
|
|
229
|
+
f"Directory name '{dir_name}' does not match skill name '{skill_name}'",
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return errors
|
|
234
|
+
|
|
235
|
+
@staticmethod
|
|
236
|
+
def extract_frontmatter(
|
|
237
|
+
content: str,
|
|
238
|
+
) -> tuple[dict[str, Any] | None, str | None, list[ValidationError]]:
|
|
239
|
+
"""Extract and parse YAML frontmatter from SKILL.md content.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
Tuple of (parsed_frontmatter, remaining_content, errors)
|
|
243
|
+
"""
|
|
244
|
+
errors = []
|
|
245
|
+
|
|
246
|
+
# Check for YAML frontmatter markers
|
|
247
|
+
if not content.startswith("---\n"):
|
|
248
|
+
errors.append(
|
|
249
|
+
ValidationError(
|
|
250
|
+
"frontmatter", "SKILL.md must start with YAML frontmatter (---)"
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
return None, content, errors
|
|
254
|
+
|
|
255
|
+
# Find end of frontmatter
|
|
256
|
+
lines = content.split("\n")
|
|
257
|
+
end_marker_idx = None
|
|
258
|
+
for i, line in enumerate(lines[1:], start=1):
|
|
259
|
+
if line.strip() == "---":
|
|
260
|
+
end_marker_idx = i
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
if end_marker_idx is None:
|
|
264
|
+
errors.append(
|
|
265
|
+
ValidationError(
|
|
266
|
+
"frontmatter",
|
|
267
|
+
"YAML frontmatter not properly closed (missing closing ---)",
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
return None, content, errors
|
|
271
|
+
|
|
272
|
+
# Extract frontmatter YAML
|
|
273
|
+
frontmatter_text = "\n".join(lines[1:end_marker_idx])
|
|
274
|
+
|
|
275
|
+
# Parse YAML
|
|
276
|
+
try:
|
|
277
|
+
frontmatter = yaml.safe_load(frontmatter_text)
|
|
278
|
+
if not isinstance(frontmatter, dict):
|
|
279
|
+
errors.append(
|
|
280
|
+
ValidationError(
|
|
281
|
+
"frontmatter",
|
|
282
|
+
"Frontmatter must be a YAML mapping (key-value pairs)",
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
return None, content, errors
|
|
286
|
+
except yaml.YAMLError as e:
|
|
287
|
+
errors.append(
|
|
288
|
+
ValidationError("frontmatter", f"Invalid YAML in frontmatter: {e}")
|
|
289
|
+
)
|
|
290
|
+
return None, content, errors
|
|
291
|
+
|
|
292
|
+
# Extract remaining content
|
|
293
|
+
remaining = "\n".join(lines[end_marker_idx + 1 :])
|
|
294
|
+
|
|
295
|
+
return frontmatter, remaining, errors
|
|
296
|
+
|
|
297
|
+
@staticmethod
|
|
298
|
+
def validate_file(skill_path: Path) -> ValidationResult:
|
|
299
|
+
"""Validate a SKILL.md file at the given path.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
skill_path: Path to directory containing SKILL.md
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
ValidationResult with errors, warnings, and metadata
|
|
306
|
+
"""
|
|
307
|
+
errors = []
|
|
308
|
+
warnings = []
|
|
309
|
+
|
|
310
|
+
# Check that SKILL.md exists
|
|
311
|
+
skill_file = skill_path / "SKILL.md"
|
|
312
|
+
if not skill_file.exists():
|
|
313
|
+
errors.append(
|
|
314
|
+
ValidationError("file", f"SKILL.md file not found in {skill_path}")
|
|
315
|
+
)
|
|
316
|
+
return ValidationResult(valid=False, errors=errors, warnings=warnings)
|
|
317
|
+
|
|
318
|
+
# Read file content
|
|
319
|
+
try:
|
|
320
|
+
content = skill_file.read_text(encoding="utf-8")
|
|
321
|
+
except Exception as e:
|
|
322
|
+
errors.append(ValidationError("file", f"Failed to read SKILL.md: {e}"))
|
|
323
|
+
return ValidationResult(valid=False, errors=errors, warnings=warnings)
|
|
324
|
+
|
|
325
|
+
# Extract and parse frontmatter
|
|
326
|
+
frontmatter, body, fm_errors = SkillValidator.extract_frontmatter(content)
|
|
327
|
+
errors.extend(fm_errors)
|
|
328
|
+
|
|
329
|
+
if not frontmatter:
|
|
330
|
+
return ValidationResult(valid=False, errors=errors, warnings=warnings)
|
|
331
|
+
|
|
332
|
+
# Validate frontmatter fields
|
|
333
|
+
errors.extend(SkillValidator.validate_frontmatter_fields(frontmatter))
|
|
334
|
+
|
|
335
|
+
# Validate individual fields
|
|
336
|
+
if "name" in frontmatter:
|
|
337
|
+
errors.extend(SkillValidator.validate_name(frontmatter["name"]))
|
|
338
|
+
|
|
339
|
+
# Validate directory name matches skill name
|
|
340
|
+
errors.extend(
|
|
341
|
+
SkillValidator.validate_directory_name(skill_path, frontmatter["name"])
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if "description" in frontmatter:
|
|
345
|
+
errors.extend(
|
|
346
|
+
SkillValidator.validate_description(frontmatter["description"])
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if "compatibility" in frontmatter:
|
|
350
|
+
errors.extend(
|
|
351
|
+
SkillValidator.validate_compatibility(frontmatter["compatibility"])
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Check for body content (warning if empty)
|
|
355
|
+
if body and not body.strip():
|
|
356
|
+
warnings.append(
|
|
357
|
+
ValidationError(
|
|
358
|
+
"body", "SKILL.md body is empty. Consider adding instructions."
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
return ValidationResult(
|
|
363
|
+
valid=len(errors) == 0,
|
|
364
|
+
errors=errors,
|
|
365
|
+
warnings=warnings,
|
|
366
|
+
metadata=frontmatter if len(errors) == 0 else None,
|
|
367
|
+
)
|
uacs/utils/__init__.py
ADDED
uacs/utils/paths.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Path utilities for UACS."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_project_root() -> Path:
|
|
8
|
+
"""Get the effective project root directory.
|
|
9
|
+
|
|
10
|
+
Prioritizes PWD environment variable to handle cases where the tool
|
|
11
|
+
is invoked via 'uv run --directory ...' which changes the process CWD.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Path to the project root directory
|
|
15
|
+
"""
|
|
16
|
+
# Check if PWD is set and valid
|
|
17
|
+
pwd = os.environ.get("PWD")
|
|
18
|
+
if pwd:
|
|
19
|
+
path = Path(pwd)
|
|
20
|
+
if path.exists() and path.is_dir():
|
|
21
|
+
return path
|
|
22
|
+
|
|
23
|
+
# Fallback to current working directory
|
|
24
|
+
return Path.cwd()
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# UACS Visualization Module
|
|
2
|
+
|
|
3
|
+
This module provides real-time web-based visualization of UACS context graphs.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
visualization/
|
|
9
|
+
├── __init__.py # Module exports
|
|
10
|
+
├── visualization.py # Terminal-based Rich visualizations
|
|
11
|
+
├── web_server.py # FastAPI web server for browser UI
|
|
12
|
+
├── static/ # Static web assets
|
|
13
|
+
│ └── index.html # Single-page web application
|
|
14
|
+
└── README.md # This file
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### From CLI
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Start MCP server with web UI
|
|
23
|
+
uacs serve --with-ui
|
|
24
|
+
|
|
25
|
+
# Custom port
|
|
26
|
+
uacs serve --with-ui --ui-port 3000
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### From Python
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from uacs.context.shared_context import SharedContextManager
|
|
34
|
+
from uacs.visualization.web_server import VisualizationServer
|
|
35
|
+
import uvicorn
|
|
36
|
+
|
|
37
|
+
# Initialize
|
|
38
|
+
manager = SharedContextManager(Path(".state/context"))
|
|
39
|
+
viz_server = VisualizationServer(manager, host="localhost", port=8081)
|
|
40
|
+
|
|
41
|
+
# Run server
|
|
42
|
+
config = uvicorn.Config(viz_server.app, host="localhost", port=8081)
|
|
43
|
+
server = uvicorn.Server(config)
|
|
44
|
+
await server.serve()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Components
|
|
48
|
+
|
|
49
|
+
### Terminal Visualization (`visualization.py`)
|
|
50
|
+
|
|
51
|
+
Rich-based terminal visualizations for CLI usage:
|
|
52
|
+
- Context graph as tree structure
|
|
53
|
+
- Token usage meters
|
|
54
|
+
- Agent interaction flow
|
|
55
|
+
- Live dashboard with auto-refresh
|
|
56
|
+
|
|
57
|
+
### Web Visualization (`web_server.py`)
|
|
58
|
+
|
|
59
|
+
FastAPI server providing:
|
|
60
|
+
- REST API endpoints for context data
|
|
61
|
+
- WebSocket support for real-time updates
|
|
62
|
+
- Static file serving for web UI
|
|
63
|
+
|
|
64
|
+
### Web UI (`static/index.html`)
|
|
65
|
+
|
|
66
|
+
Single-page application with:
|
|
67
|
+
- D3.js for interactive graphs
|
|
68
|
+
- Chart.js for statistics
|
|
69
|
+
- 5 visualization modes
|
|
70
|
+
- Real-time WebSocket updates
|
|
71
|
+
|
|
72
|
+
## API Endpoints
|
|
73
|
+
|
|
74
|
+
| Endpoint | Description |
|
|
75
|
+
|----------|-------------|
|
|
76
|
+
| `GET /` | Main visualization page |
|
|
77
|
+
| `GET /api/graph` | Context graph data |
|
|
78
|
+
| `GET /api/stats` | Token statistics |
|
|
79
|
+
| `GET /api/topics` | Topic clusters |
|
|
80
|
+
| `GET /api/deduplication` | Deduplication data |
|
|
81
|
+
| `GET /api/quality` | Quality distribution |
|
|
82
|
+
| `WS /ws` | WebSocket for real-time updates |
|
|
83
|
+
| `GET /health` | Health check |
|
|
84
|
+
|
|
85
|
+
## Visualization Modes
|
|
86
|
+
|
|
87
|
+
1. **Conversation Flow** - Interactive D3.js force-directed graph
|
|
88
|
+
2. **Token Dashboard** - Real-time token usage charts
|
|
89
|
+
3. **Deduplication** - Duplicate content analysis
|
|
90
|
+
4. **Quality Distribution** - Content quality metrics
|
|
91
|
+
5. **Topic Clusters** - Topic network visualization
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
For complete documentation, see: [docs/VISUALIZATION.md](../../../docs/VISUALIZATION.md)
|
|
96
|
+
|
|
97
|
+
## Testing
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Run tests
|
|
101
|
+
pytest tests/test_visualization_server.py -v
|
|
102
|
+
|
|
103
|
+
# Run demo
|
|
104
|
+
python examples/visualization_demo.py
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
### Adding New Endpoints
|
|
110
|
+
|
|
111
|
+
1. Add method to `VisualizationServer._setup_routes()`
|
|
112
|
+
2. Implement data processing method
|
|
113
|
+
3. Add frontend update function in `index.html`
|
|
114
|
+
|
|
115
|
+
### Modifying Visualizations
|
|
116
|
+
|
|
117
|
+
Edit `static/index.html`:
|
|
118
|
+
- CSS for styling (in `<style>` section)
|
|
119
|
+
- JavaScript for behavior (in `<script>` section)
|
|
120
|
+
- D3.js/Chart.js configuration for visualizations
|
|
121
|
+
|
|
122
|
+
## Dependencies
|
|
123
|
+
|
|
124
|
+
- FastAPI - Web framework
|
|
125
|
+
- Uvicorn - ASGI server
|
|
126
|
+
- WebSockets - Real-time communication
|
|
127
|
+
- D3.js (CDN) - Graph visualization
|
|
128
|
+
- Chart.js (CDN) - Statistical charts
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT License (same as UACS)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Visualization module for UACS context graphs and trace visualization."""
|
|
2
|
+
|
|
3
|
+
from uacs.visualization.visualization import ContextVisualizer
|
|
4
|
+
from uacs.visualization.web_server import VisualizationServer, start_visualization_server
|
|
5
|
+
from uacs.visualization.models import (
|
|
6
|
+
Event,
|
|
7
|
+
EventType,
|
|
8
|
+
Session,
|
|
9
|
+
SessionList,
|
|
10
|
+
EventList,
|
|
11
|
+
TokenAnalytics,
|
|
12
|
+
CompressionAnalytics,
|
|
13
|
+
TopicAnalytics,
|
|
14
|
+
SearchRequest,
|
|
15
|
+
SearchResults,
|
|
16
|
+
CompressionTrigger,
|
|
17
|
+
)
|
|
18
|
+
from uacs.visualization.storage import TraceStorage
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ContextVisualizer",
|
|
22
|
+
"VisualizationServer",
|
|
23
|
+
"start_visualization_server",
|
|
24
|
+
"Event",
|
|
25
|
+
"EventType",
|
|
26
|
+
"Session",
|
|
27
|
+
"SessionList",
|
|
28
|
+
"EventList",
|
|
29
|
+
"TokenAnalytics",
|
|
30
|
+
"CompressionAnalytics",
|
|
31
|
+
"TopicAnalytics",
|
|
32
|
+
"SearchRequest",
|
|
33
|
+
"SearchResults",
|
|
34
|
+
"CompressionTrigger",
|
|
35
|
+
"TraceStorage",
|
|
36
|
+
]
|