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/adapters/base.py
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
"""Base adapter class for format translation."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Skill:
|
|
11
|
+
"""Skill information."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
instructions: str
|
|
15
|
+
triggers: list[str] = field(default_factory=list)
|
|
16
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
17
|
+
description: str | None = None
|
|
18
|
+
examples: list[str] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ParsedContent:
|
|
22
|
+
"""Parsed content from format."""
|
|
23
|
+
|
|
24
|
+
name: str | None = None
|
|
25
|
+
description: str | None = None
|
|
26
|
+
instructions: str | None = None
|
|
27
|
+
metadata: dict[str, Any] | None = None
|
|
28
|
+
rules: str | None = None
|
|
29
|
+
|
|
30
|
+
def __init__(self, **kwargs):
|
|
31
|
+
self.__dict__.update(kwargs)
|
|
32
|
+
# Set attributes from kwargs for type safety
|
|
33
|
+
for k, v in kwargs.items():
|
|
34
|
+
if hasattr(self, k):
|
|
35
|
+
setattr(self, k, v)
|
|
36
|
+
|
|
37
|
+
def to_dict(self) -> dict[str, Any]:
|
|
38
|
+
"""Convert to dictionary."""
|
|
39
|
+
return self.__dict__
|
|
40
|
+
|
|
41
|
+
def __repr__(self) -> str:
|
|
42
|
+
return f"ParsedContent({self.__dict__})"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class BaseFormatAdapter(ABC):
|
|
46
|
+
"""Base class for all format adapters."""
|
|
47
|
+
|
|
48
|
+
FORMAT_NAME: str = "base"
|
|
49
|
+
SUPPORTED_FILES: list[str] = []
|
|
50
|
+
|
|
51
|
+
def __init__(self, file_path: Path | None = None):
|
|
52
|
+
self.file_path = file_path
|
|
53
|
+
self.content = file_path.read_text() if file_path and file_path.exists() else ""
|
|
54
|
+
self.parsed = self.parse(self.content) if self.content else None
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
def parse(self, content: str) -> ParsedContent:
|
|
58
|
+
"""Parse format-specific content.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
content: Raw file content
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Parsed content object
|
|
65
|
+
"""
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
@abstractmethod
|
|
69
|
+
def to_system_prompt(self) -> str:
|
|
70
|
+
"""Convert to system prompt format.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Formatted system prompt string
|
|
74
|
+
"""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
def to_adk_capabilities(self) -> dict[str, Any]:
|
|
78
|
+
"""Convert to ADK agent card format (optional).
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
ADK capabilities dictionary
|
|
82
|
+
"""
|
|
83
|
+
return {}
|
|
84
|
+
|
|
85
|
+
def exists(self) -> bool:
|
|
86
|
+
"""Check if file exists.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if file exists
|
|
90
|
+
"""
|
|
91
|
+
return self.file_path and self.file_path.exists()
|
|
92
|
+
|
|
93
|
+
def get_stats(self) -> dict[str, Any]:
|
|
94
|
+
"""Get adapter statistics.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Dictionary with adapter stats
|
|
98
|
+
"""
|
|
99
|
+
return {
|
|
100
|
+
"format": self.FORMAT_NAME,
|
|
101
|
+
"file": str(self.file_path),
|
|
102
|
+
"exists": self.exists(),
|
|
103
|
+
"size": len(self.content),
|
|
104
|
+
"parsed": self.parsed.to_dict() if self.parsed else None,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
def find_skill_by_trigger(self, query: str) -> Skill | None:
|
|
108
|
+
"""Find skill that matches query trigger.
|
|
109
|
+
|
|
110
|
+
Default implementation: no matching.
|
|
111
|
+
Subclasses can override for trigger-based matching.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
query: User query to match against triggers
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
Matching skill or None
|
|
118
|
+
"""
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def supports_file(cls, file_path: Path) -> bool:
|
|
123
|
+
"""Check if this adapter supports the file.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
file_path: Path to check
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
True if adapter supports this file
|
|
130
|
+
"""
|
|
131
|
+
return file_path.name in cls.SUPPORTED_FILES
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class FormatAdapterRegistry:
|
|
135
|
+
"""Registry for format adapters."""
|
|
136
|
+
|
|
137
|
+
_adapters: dict[str, type["BaseFormatAdapter"]] = {}
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def register(
|
|
141
|
+
cls, adapter_class: type["BaseFormatAdapter"]
|
|
142
|
+
) -> type["BaseFormatAdapter"]:
|
|
143
|
+
"""Register adapter class.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
adapter_class: Adapter class to register
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
The adapter class (for decorator usage)
|
|
150
|
+
"""
|
|
151
|
+
cls._adapters[adapter_class.FORMAT_NAME] = adapter_class
|
|
152
|
+
return adapter_class
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def detect_and_load(
|
|
156
|
+
cls, project_path: Path, search_parents: bool = True
|
|
157
|
+
) -> BaseFormatAdapter | None:
|
|
158
|
+
"""Auto-detect format and return adapter.
|
|
159
|
+
|
|
160
|
+
Searches for supported files in:
|
|
161
|
+
1. Project directory (project-specific config)
|
|
162
|
+
2. Parent directories up to root (following AGENTS.md spec)
|
|
163
|
+
3. User home directory (personal/global config)
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
project_path: Path to project directory
|
|
167
|
+
search_parents: Whether to search parent directories
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Adapter instance or None if no format detected
|
|
171
|
+
"""
|
|
172
|
+
search_paths = [project_path]
|
|
173
|
+
|
|
174
|
+
# Add parent directories
|
|
175
|
+
if search_parents:
|
|
176
|
+
current = project_path
|
|
177
|
+
while current != current.parent:
|
|
178
|
+
current = current.parent
|
|
179
|
+
search_paths.append(current)
|
|
180
|
+
|
|
181
|
+
# Add user home directory for personal skills
|
|
182
|
+
home_config_paths = [
|
|
183
|
+
Path.home() / ".claude", # Claude Code personal skills
|
|
184
|
+
Path.home() / ".config" / "uacs", # UACS config directory
|
|
185
|
+
]
|
|
186
|
+
search_paths.extend(home_config_paths)
|
|
187
|
+
|
|
188
|
+
# Search all paths for supported files
|
|
189
|
+
for adapter_class in cls._adapters.values():
|
|
190
|
+
for filename in adapter_class.SUPPORTED_FILES:
|
|
191
|
+
for search_path in search_paths:
|
|
192
|
+
file_path = search_path / filename
|
|
193
|
+
if file_path.exists():
|
|
194
|
+
return adapter_class(file_path)
|
|
195
|
+
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
@classmethod
|
|
199
|
+
def detect_and_load_all(cls, project_path: Path) -> list[BaseFormatAdapter]:
|
|
200
|
+
"""Detect and load all available format adapters.
|
|
201
|
+
|
|
202
|
+
Finds all supported files in project and personal directories.
|
|
203
|
+
Also discovers multiple SKILL.md files from .claude/skills/*
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
project_path: Path to project directory
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
List of adapter instances
|
|
210
|
+
"""
|
|
211
|
+
adapters = []
|
|
212
|
+
found_files = set()
|
|
213
|
+
|
|
214
|
+
search_paths = [
|
|
215
|
+
project_path,
|
|
216
|
+
Path.home() / ".claude",
|
|
217
|
+
Path.home() / ".config" / "uacs",
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
for adapter_class in cls._adapters.values():
|
|
221
|
+
for filename in adapter_class.SUPPORTED_FILES:
|
|
222
|
+
for search_path in search_paths:
|
|
223
|
+
file_path = search_path / filename
|
|
224
|
+
if file_path.exists() and str(file_path) not in found_files:
|
|
225
|
+
adapters.append(adapter_class(file_path))
|
|
226
|
+
found_files.add(str(file_path))
|
|
227
|
+
|
|
228
|
+
# Special case: Discover multiple SKILL.md files from .agent/skills/* and .claude/skills/*
|
|
229
|
+
from .agent_skill_adapter import AgentSkillAdapter
|
|
230
|
+
|
|
231
|
+
agent_skills = AgentSkillAdapter.discover_skills(project_path)
|
|
232
|
+
for skill in agent_skills:
|
|
233
|
+
if str(skill.file_path) not in found_files:
|
|
234
|
+
adapters.append(skill)
|
|
235
|
+
found_files.add(str(skill.file_path))
|
|
236
|
+
|
|
237
|
+
return adapters
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def list_formats(cls) -> list[str]:
|
|
241
|
+
"""List registered formats.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
List of format names
|
|
245
|
+
"""
|
|
246
|
+
return list(cls._adapters.keys())
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def get_adapter(cls, format_name: str) -> type | None:
|
|
250
|
+
"""Get adapter class by format name.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
format_name: Name of the format
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Adapter class or None if not found
|
|
257
|
+
"""
|
|
258
|
+
return cls._adapters.get(format_name)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
__all__ = ["BaseFormatAdapter", "FormatAdapterRegistry", "ParsedContent", "Skill"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Adapter for Cline .clinerules format."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseFormatAdapter, FormatAdapterRegistry, ParsedContent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@FormatAdapterRegistry.register
|
|
7
|
+
class ClineRulesAdapter(BaseFormatAdapter):
|
|
8
|
+
"""Cline .clinerules format adapter.
|
|
9
|
+
|
|
10
|
+
Cline rules are typically plain text instructions for the Cline AI assistant.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
FORMAT_NAME = "clinerules"
|
|
14
|
+
SUPPORTED_FILES = [".clinerules"]
|
|
15
|
+
|
|
16
|
+
def parse(self, content: str) -> ParsedContent:
|
|
17
|
+
"""Parse .clinerules format.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
content: Raw .clinerules content
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
ParsedContent with rules
|
|
24
|
+
"""
|
|
25
|
+
return ParsedContent(rules=content.strip())
|
|
26
|
+
|
|
27
|
+
def to_system_prompt(self) -> str:
|
|
28
|
+
"""Convert to system prompt.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Formatted system prompt
|
|
32
|
+
"""
|
|
33
|
+
if not self.parsed or not self.parsed.rules:
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
return f"# PROJECT RULES\n\n{self.parsed.rules}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ["ClineRulesAdapter"]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Adapter for Cursor .cursorrules format."""
|
|
2
|
+
|
|
3
|
+
from .base import BaseFormatAdapter, FormatAdapterRegistry, ParsedContent
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@FormatAdapterRegistry.register
|
|
7
|
+
class CursorRulesAdapter(BaseFormatAdapter):
|
|
8
|
+
"""Cursor .cursorrules format adapter.
|
|
9
|
+
|
|
10
|
+
Cursor rules are typically plain text instructions for the Cursor editor.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
FORMAT_NAME = "cursorrules"
|
|
14
|
+
SUPPORTED_FILES = [".cursorrules"]
|
|
15
|
+
|
|
16
|
+
def parse(self, content: str) -> ParsedContent:
|
|
17
|
+
"""Parse .cursorrules format (usually plain text).
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
content: Raw .cursorrules content
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
ParsedContent with rules
|
|
24
|
+
"""
|
|
25
|
+
return ParsedContent(rules=content.strip())
|
|
26
|
+
|
|
27
|
+
def to_system_prompt(self) -> str:
|
|
28
|
+
"""Convert to system prompt.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Formatted system prompt
|
|
32
|
+
"""
|
|
33
|
+
if not self.parsed or not self.parsed.rules:
|
|
34
|
+
return ""
|
|
35
|
+
|
|
36
|
+
return f"# PROJECT RULES\n\n{self.parsed.rules}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ["CursorRulesAdapter"]
|
uacs/api.py
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"""Universal Agent Context System - Main API
|
|
2
|
+
|
|
3
|
+
See docs/uacs/README.md for details.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from uacs.adapters.agent_skill_adapter import AgentSkillAdapter
|
|
10
|
+
from uacs.adapters.agents_md_adapter import AgentsMDAdapter
|
|
11
|
+
from uacs.context.shared_context import SharedContextManager
|
|
12
|
+
from uacs.context.unified_context import UnifiedContextAdapter
|
|
13
|
+
from uacs.packages import PackageManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class UACS:
|
|
17
|
+
"""Universal Agent Context System
|
|
18
|
+
|
|
19
|
+
Provides unified context management across:
|
|
20
|
+
- Packages (Local package management)
|
|
21
|
+
- Adapters (Format translation)
|
|
22
|
+
- Context (Shared memory + compression)
|
|
23
|
+
- Project metadata (AGENTS.md)
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> uacs = UACS(project_path=Path("."))
|
|
27
|
+
>>> uacs.install_package("owner/repo")
|
|
28
|
+
>>> context = uacs.build_context(query="...", agent="claude")
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
project_path: Path,
|
|
34
|
+
):
|
|
35
|
+
"""Initialize UACS.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
project_path: Path to the project root
|
|
39
|
+
"""
|
|
40
|
+
self.project_path = project_path
|
|
41
|
+
|
|
42
|
+
# Initialize components
|
|
43
|
+
self.packages = PackageManager(project_path)
|
|
44
|
+
|
|
45
|
+
# Detect and load project format adapters
|
|
46
|
+
agents_md_path = project_path / "AGENTS.md"
|
|
47
|
+
|
|
48
|
+
# Modern Agent Skills support (.agent/skills/)
|
|
49
|
+
self.agent_skills = AgentSkillAdapter.discover_skills(project_path)
|
|
50
|
+
|
|
51
|
+
self.agents_md = (
|
|
52
|
+
AgentsMDAdapter(agents_md_path) if agents_md_path.exists() else None
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Initialize shared context and unified adapter
|
|
56
|
+
self.shared_context = SharedContextManager(project_path / ".state" / "context")
|
|
57
|
+
self.unified_context = UnifiedContextAdapter(
|
|
58
|
+
agents_md_path=agents_md_path if agents_md_path.exists() else None,
|
|
59
|
+
context_storage=project_path / ".state" / "context",
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
def install_package(
|
|
63
|
+
self,
|
|
64
|
+
source: str,
|
|
65
|
+
validate: bool = True,
|
|
66
|
+
force: bool = False,
|
|
67
|
+
) -> Any:
|
|
68
|
+
"""Install a package from GitHub, Git URL, or local path.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
source: Package source (owner/repo, git URL, or local path)
|
|
72
|
+
validate: Whether to validate before installing
|
|
73
|
+
force: Whether to overwrite existing package
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Installed package information
|
|
77
|
+
"""
|
|
78
|
+
return self.packages.install(source, validate=validate, force=force)
|
|
79
|
+
|
|
80
|
+
def list_packages(self) -> list[Any]:
|
|
81
|
+
"""List installed packages.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
List of installed packages
|
|
85
|
+
"""
|
|
86
|
+
return self.packages.list_installed()
|
|
87
|
+
|
|
88
|
+
def get_capabilities(self, agent: str | None = None) -> dict[str, Any]:
|
|
89
|
+
"""Get available capabilities for an agent.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
agent: Optional agent name to filter capabilities
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Dictionary of capabilities
|
|
96
|
+
"""
|
|
97
|
+
capabilities = self.unified_context.get_capabilities(agent_name=agent)
|
|
98
|
+
|
|
99
|
+
# Add discovered Agent Skills
|
|
100
|
+
if self.agent_skills:
|
|
101
|
+
capabilities["agent_skills"] = [
|
|
102
|
+
{
|
|
103
|
+
"name": skill.parsed.name,
|
|
104
|
+
"description": skill.parsed.description,
|
|
105
|
+
"triggers": skill.parsed.triggers,
|
|
106
|
+
"source": skill.source_directory or "local",
|
|
107
|
+
}
|
|
108
|
+
for skill in self.agent_skills
|
|
109
|
+
if skill.parsed
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
return capabilities
|
|
113
|
+
|
|
114
|
+
def build_context(
|
|
115
|
+
self,
|
|
116
|
+
query: str,
|
|
117
|
+
agent: str,
|
|
118
|
+
max_tokens: int | None = None,
|
|
119
|
+
topics: list[str] | None = None,
|
|
120
|
+
) -> str:
|
|
121
|
+
"""Build context for an agent query.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
query: The query or task
|
|
125
|
+
agent: Agent name (claude, gemini, etc.)
|
|
126
|
+
max_tokens: Optional token limit
|
|
127
|
+
topics: Optional topics to filter and prioritize context
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Formatted context string
|
|
131
|
+
"""
|
|
132
|
+
# If topics are provided, use focused context
|
|
133
|
+
if topics:
|
|
134
|
+
focused_context = self.shared_context.get_focused_context(
|
|
135
|
+
topics=topics, agent=agent, max_tokens=max_tokens or 4000
|
|
136
|
+
)
|
|
137
|
+
# Combine with unified context
|
|
138
|
+
unified = self.unified_context.build_context(
|
|
139
|
+
query=query, agent_name=agent, max_tokens=max_tokens
|
|
140
|
+
)
|
|
141
|
+
return f"{focused_context}\n\n{unified}" if focused_context else unified
|
|
142
|
+
|
|
143
|
+
return self.unified_context.build_context(
|
|
144
|
+
query=query, agent_name=agent, max_tokens=max_tokens
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def add_to_context(
|
|
148
|
+
self,
|
|
149
|
+
key: str,
|
|
150
|
+
content: str,
|
|
151
|
+
metadata: dict[str, Any] | None = None,
|
|
152
|
+
topics: list[str] | None = None,
|
|
153
|
+
):
|
|
154
|
+
"""Add content to shared context.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
key: Context key (used as agent name)
|
|
158
|
+
content: Content to store
|
|
159
|
+
metadata: Optional metadata
|
|
160
|
+
topics: Optional topics for semantic filtering
|
|
161
|
+
|
|
162
|
+
Example:
|
|
163
|
+
uacs.add_to_context(
|
|
164
|
+
"claude",
|
|
165
|
+
"Reviewed auth.py, found SQL injection",
|
|
166
|
+
topics=["code-review", "security"]
|
|
167
|
+
)
|
|
168
|
+
"""
|
|
169
|
+
# Add to shared context with topics
|
|
170
|
+
self.shared_context.add_entry(
|
|
171
|
+
content=content,
|
|
172
|
+
agent=key,
|
|
173
|
+
metadata=metadata,
|
|
174
|
+
topics=topics,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def get_token_stats(self) -> dict[str, int]:
|
|
178
|
+
"""Get token usage statistics.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dictionary of token counts
|
|
182
|
+
"""
|
|
183
|
+
return self.unified_context.get_token_stats()
|
|
184
|
+
|
|
185
|
+
def export_config(self, output_path: Path):
|
|
186
|
+
"""Export UACS configuration.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
output_path: Path to save configuration
|
|
190
|
+
"""
|
|
191
|
+
self.unified_context.export_unified_config(output_path)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def visualize_context(self, output_path: Path | None = None) -> str:
|
|
195
|
+
"""Visualize context structure.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
output_path: Optional path to save visualization
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Visualization string (ASCII art or HTML)
|
|
202
|
+
"""
|
|
203
|
+
from uacs.visualization import ContextVisualizer
|
|
204
|
+
|
|
205
|
+
visualizer = ContextVisualizer()
|
|
206
|
+
# Get graph and stats from shared context
|
|
207
|
+
graph = self.shared_context.get_context_graph()
|
|
208
|
+
stats = self.shared_context.get_stats()
|
|
209
|
+
|
|
210
|
+
# Render the visualizations
|
|
211
|
+
graph_panel = visualizer.render_context_graph(graph)
|
|
212
|
+
stats_table = visualizer.render_stats_table(stats)
|
|
213
|
+
|
|
214
|
+
# Print to console
|
|
215
|
+
visualizer.console.print(graph_panel)
|
|
216
|
+
visualizer.console.print(stats_table)
|
|
217
|
+
|
|
218
|
+
# Optionally save to file
|
|
219
|
+
if output_path:
|
|
220
|
+
output_path.write_text(f"Context Graph: {graph}\nStats: {stats}")
|
|
221
|
+
return f"Visualization saved to {output_path}"
|
|
222
|
+
|
|
223
|
+
return "Visualization rendered to console"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def get_stats(self) -> dict[str, Any]:
|
|
227
|
+
"""Get comprehensive UACS statistics.
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
Dictionary with statistics from all components
|
|
231
|
+
"""
|
|
232
|
+
# Merge token stats with shared context stats
|
|
233
|
+
token_stats = self.get_token_stats()
|
|
234
|
+
context_stats = self.shared_context.get_stats()
|
|
235
|
+
token_stats.update(context_stats)
|
|
236
|
+
|
|
237
|
+
stats = {
|
|
238
|
+
"project_path": str(self.project_path),
|
|
239
|
+
"adapters": {
|
|
240
|
+
"agent_skills": {
|
|
241
|
+
"count": len(self.agent_skills),
|
|
242
|
+
"paths": [str(s.file_path) for s in self.agent_skills],
|
|
243
|
+
},
|
|
244
|
+
"agents_md": {
|
|
245
|
+
"loaded": self.agents_md is not None,
|
|
246
|
+
"path": str(self.project_path / "AGENTS.md")
|
|
247
|
+
if self.agents_md
|
|
248
|
+
else None,
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
"packages": {
|
|
252
|
+
"installed_count": len(self.packages.list_installed()),
|
|
253
|
+
"skills_dir": str(self.project_path / ".agent" / "skills"),
|
|
254
|
+
},
|
|
255
|
+
"context": token_stats,
|
|
256
|
+
"capabilities": self.get_capabilities(),
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return stats
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
__all__ = ["UACS"]
|