loom-agent 0.0.1__py3-none-any.whl → 0.0.3__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.
Potentially problematic release.
This version of loom-agent might be problematic. Click here for more details.
- loom/builtin/tools/calculator.py +4 -0
- loom/builtin/tools/document_search.py +5 -0
- loom/builtin/tools/glob.py +4 -0
- loom/builtin/tools/grep.py +4 -0
- loom/builtin/tools/http_request.py +5 -0
- loom/builtin/tools/python_repl.py +5 -0
- loom/builtin/tools/read_file.py +4 -0
- loom/builtin/tools/task.py +105 -0
- loom/builtin/tools/web_search.py +4 -0
- loom/builtin/tools/write_file.py +4 -0
- loom/components/agent.py +121 -5
- loom/core/agent_executor.py +777 -321
- loom/core/compression_manager.py +17 -10
- loom/core/context_assembly.py +437 -0
- loom/core/events.py +660 -0
- loom/core/execution_context.py +119 -0
- loom/core/tool_orchestrator.py +383 -0
- loom/core/turn_state.py +188 -0
- loom/core/types.py +15 -4
- loom/core/unified_coordination.py +389 -0
- loom/interfaces/event_producer.py +172 -0
- loom/interfaces/tool.py +22 -1
- loom/security/__init__.py +13 -0
- loom/security/models.py +85 -0
- loom/security/path_validator.py +128 -0
- loom/security/validator.py +346 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.1_agent_events.md +121 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.2_streaming_api.md +521 -0
- loom/tasks/PHASE_1_FOUNDATION/task_1.3_context_assembler.md +606 -0
- loom/tasks/PHASE_2_CORE_FEATURES/task_2.1_tool_orchestrator.md +743 -0
- loom/tasks/PHASE_2_CORE_FEATURES/task_2.2_security_validator.md +676 -0
- loom/tasks/README.md +109 -0
- loom/tasks/__init__.py +11 -0
- loom/tasks/sql_placeholder.py +100 -0
- loom_agent-0.0.3.dist-info/METADATA +292 -0
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/RECORD +38 -19
- loom_agent-0.0.1.dist-info/METADATA +0 -457
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/WHEEL +0 -0
- {loom_agent-0.0.1.dist-info → loom_agent-0.0.3.dist-info}/licenses/LICENSE +0 -0
loom/core/compression_manager.py
CHANGED
|
@@ -151,8 +151,8 @@ Now compress the conversation above following this exact structure:"""
|
|
|
151
151
|
return messages, CompressionMetadata(
|
|
152
152
|
original_message_count=0,
|
|
153
153
|
compressed_message_count=0,
|
|
154
|
-
|
|
155
|
-
|
|
154
|
+
original_token_count=0,
|
|
155
|
+
compressed_token_count=0,
|
|
156
156
|
compression_ratio=0.0,
|
|
157
157
|
key_topics=[],
|
|
158
158
|
)
|
|
@@ -163,11 +163,12 @@ Now compress the conversation above following this exact structure:"""
|
|
|
163
163
|
|
|
164
164
|
if not compressible:
|
|
165
165
|
# No messages to compress, return as-is
|
|
166
|
+
token_count = count_messages_tokens(messages)
|
|
166
167
|
return messages, CompressionMetadata(
|
|
167
168
|
original_message_count=len(messages),
|
|
168
169
|
compressed_message_count=len(messages),
|
|
169
|
-
|
|
170
|
-
|
|
170
|
+
original_token_count=token_count,
|
|
171
|
+
compressed_token_count=token_count,
|
|
171
172
|
compression_ratio=1.0,
|
|
172
173
|
key_topics=[],
|
|
173
174
|
)
|
|
@@ -201,13 +202,16 @@ Now compress the conversation above following this exact structure:"""
|
|
|
201
202
|
windowed_messages = self.sliding_window_fallback(compressible, self.sliding_window_size)
|
|
202
203
|
final_messages = system_messages + windowed_messages
|
|
203
204
|
compressed_tokens = count_messages_tokens(windowed_messages)
|
|
205
|
+
ratio = compressed_tokens / original_tokens if original_tokens > 0 else 0.0
|
|
206
|
+
# Ensure ratio is clamped to [0.0, 1.0]
|
|
207
|
+
ratio = min(max(ratio, 0.0), 1.0)
|
|
204
208
|
|
|
205
209
|
metadata = CompressionMetadata(
|
|
206
210
|
original_message_count=len(compressible),
|
|
207
211
|
compressed_message_count=len(windowed_messages),
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
compression_ratio=
|
|
212
|
+
original_token_count=original_tokens,
|
|
213
|
+
compressed_token_count=compressed_tokens,
|
|
214
|
+
compression_ratio=ratio,
|
|
211
215
|
key_topics=["fallback"],
|
|
212
216
|
)
|
|
213
217
|
return final_messages, metadata
|
|
@@ -223,13 +227,16 @@ Now compress the conversation above following this exact structure:"""
|
|
|
223
227
|
|
|
224
228
|
# Combine system messages + compressed summary
|
|
225
229
|
final_messages = system_messages + [compressed_message]
|
|
230
|
+
ratio = compressed_tokens / original_tokens if original_tokens > 0 else 0.0
|
|
231
|
+
# Ensure ratio is clamped to [0.0, 1.0]
|
|
232
|
+
ratio = min(max(ratio, 0.0), 1.0)
|
|
226
233
|
|
|
227
234
|
metadata = CompressionMetadata(
|
|
228
235
|
original_message_count=len(compressible),
|
|
229
236
|
compressed_message_count=1,
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
compression_ratio=
|
|
237
|
+
original_token_count=original_tokens,
|
|
238
|
+
compressed_token_count=compressed_tokens,
|
|
239
|
+
compression_ratio=ratio,
|
|
233
240
|
key_topics=key_topics,
|
|
234
241
|
)
|
|
235
242
|
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Context Assembly Module
|
|
3
|
+
|
|
4
|
+
Provides intelligent context assembly with priority-based component management
|
|
5
|
+
and token budget constraints.
|
|
6
|
+
|
|
7
|
+
This module fixes the RAG Context Bug where retrieved documents were being
|
|
8
|
+
overwritten by system prompts.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from typing import Dict, List, Optional, Callable, Any
|
|
16
|
+
from enum import IntEnum
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ComponentPriority(IntEnum):
|
|
20
|
+
"""
|
|
21
|
+
Component priority levels for context assembly.
|
|
22
|
+
|
|
23
|
+
Higher values = higher priority = less likely to be truncated.
|
|
24
|
+
"""
|
|
25
|
+
CRITICAL = 100 # Base instructions (must be included)
|
|
26
|
+
HIGH = 90 # RAG context, important configurations
|
|
27
|
+
MEDIUM = 70 # Tool definitions
|
|
28
|
+
LOW = 50 # Examples, additional hints
|
|
29
|
+
OPTIONAL = 30 # Optional content
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ContextComponent:
|
|
34
|
+
"""
|
|
35
|
+
A single component of the context.
|
|
36
|
+
|
|
37
|
+
Attributes:
|
|
38
|
+
name: Component identifier (e.g., "base_instructions", "retrieved_docs")
|
|
39
|
+
content: The actual content text
|
|
40
|
+
priority: Priority level (0-100)
|
|
41
|
+
token_count: Estimated number of tokens
|
|
42
|
+
truncatable: Whether this component can be truncated
|
|
43
|
+
"""
|
|
44
|
+
name: str
|
|
45
|
+
content: str
|
|
46
|
+
priority: int
|
|
47
|
+
token_count: int
|
|
48
|
+
truncatable: bool = True
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ContextAssembler:
|
|
52
|
+
"""
|
|
53
|
+
Intelligent context assembler with priority-based management.
|
|
54
|
+
|
|
55
|
+
Features:
|
|
56
|
+
- Priority-based component ordering
|
|
57
|
+
- Token budget management
|
|
58
|
+
- Smart truncation of low-priority components
|
|
59
|
+
- Guarantee high-priority component integrity
|
|
60
|
+
- Component caching for performance
|
|
61
|
+
- Dynamic priority adjustment
|
|
62
|
+
- Context reuse optimization
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
```python
|
|
66
|
+
assembler = ContextAssembler(max_tokens=4000)
|
|
67
|
+
|
|
68
|
+
# Add components with priorities
|
|
69
|
+
assembler.add_component(
|
|
70
|
+
"base_instructions",
|
|
71
|
+
"You are a helpful assistant.",
|
|
72
|
+
priority=ComponentPriority.CRITICAL,
|
|
73
|
+
truncatable=False
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
assembler.add_component(
|
|
77
|
+
"retrieved_docs",
|
|
78
|
+
doc_context,
|
|
79
|
+
priority=ComponentPriority.HIGH,
|
|
80
|
+
truncatable=True
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Assemble final context
|
|
84
|
+
final_prompt = assembler.assemble()
|
|
85
|
+
```
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def __init__(
|
|
89
|
+
self,
|
|
90
|
+
max_tokens: int = 16000,
|
|
91
|
+
token_counter: Optional[Callable[[str], int]] = None,
|
|
92
|
+
token_buffer: float = 0.9, # Use 90% of budget for safety
|
|
93
|
+
enable_caching: bool = True,
|
|
94
|
+
cache_size: int = 100
|
|
95
|
+
):
|
|
96
|
+
"""
|
|
97
|
+
Initialize the context assembler.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
max_tokens: Maximum token budget
|
|
101
|
+
token_counter: Custom token counting function (defaults to simple estimation)
|
|
102
|
+
token_buffer: Safety buffer ratio (0.9 = use 90% of max_tokens)
|
|
103
|
+
enable_caching: Enable component caching for performance
|
|
104
|
+
cache_size: Maximum number of cached components
|
|
105
|
+
"""
|
|
106
|
+
self.max_tokens = int(max_tokens * token_buffer)
|
|
107
|
+
self.token_counter = token_counter or self._estimate_tokens
|
|
108
|
+
self.components: List[ContextComponent] = []
|
|
109
|
+
|
|
110
|
+
# Performance optimizations
|
|
111
|
+
self.enable_caching = enable_caching
|
|
112
|
+
self._component_cache: Dict[str, ContextComponent] = {}
|
|
113
|
+
self._cache_size = cache_size
|
|
114
|
+
self._assembly_cache: Optional[str] = None
|
|
115
|
+
self._last_components_hash: Optional[str] = None
|
|
116
|
+
|
|
117
|
+
def add_component(
|
|
118
|
+
self,
|
|
119
|
+
name: str,
|
|
120
|
+
content: str,
|
|
121
|
+
priority: int,
|
|
122
|
+
truncatable: bool = True
|
|
123
|
+
) -> None:
|
|
124
|
+
"""
|
|
125
|
+
Add a context component.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
name: Component identifier (e.g., "base_instructions", "retrieved_docs")
|
|
129
|
+
content: Component content
|
|
130
|
+
priority: Priority level (0-100, higher = more important)
|
|
131
|
+
truncatable: Whether this component can be truncated
|
|
132
|
+
"""
|
|
133
|
+
if not content or not content.strip():
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
token_count = self.token_counter(content)
|
|
137
|
+
component = ContextComponent(
|
|
138
|
+
name=name,
|
|
139
|
+
content=content.strip(),
|
|
140
|
+
priority=priority,
|
|
141
|
+
token_count=token_count,
|
|
142
|
+
truncatable=truncatable
|
|
143
|
+
)
|
|
144
|
+
self.components.append(component)
|
|
145
|
+
|
|
146
|
+
# Update cache if enabled
|
|
147
|
+
if self.enable_caching:
|
|
148
|
+
self._component_cache[name] = component
|
|
149
|
+
# Maintain cache size limit
|
|
150
|
+
if len(self._component_cache) > self._cache_size:
|
|
151
|
+
# Remove oldest entries (simple LRU)
|
|
152
|
+
oldest_key = next(iter(self._component_cache))
|
|
153
|
+
del self._component_cache[oldest_key]
|
|
154
|
+
|
|
155
|
+
def assemble(self) -> str:
|
|
156
|
+
"""
|
|
157
|
+
Assemble the final context from all components.
|
|
158
|
+
|
|
159
|
+
Strategy:
|
|
160
|
+
1. Check cache for identical component configuration
|
|
161
|
+
2. Sort components by priority (descending)
|
|
162
|
+
3. Add components until budget is reached
|
|
163
|
+
4. Truncate low-priority components if needed
|
|
164
|
+
5. Merge all components into final string
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Assembled context string
|
|
168
|
+
"""
|
|
169
|
+
if not self.components:
|
|
170
|
+
return ""
|
|
171
|
+
|
|
172
|
+
# Check cache if enabled
|
|
173
|
+
if self.enable_caching:
|
|
174
|
+
current_hash = self._get_components_hash()
|
|
175
|
+
if (self._assembly_cache is not None and
|
|
176
|
+
self._last_components_hash == current_hash):
|
|
177
|
+
return self._assembly_cache
|
|
178
|
+
|
|
179
|
+
# Sort by priority (highest first)
|
|
180
|
+
sorted_components = sorted(
|
|
181
|
+
self.components,
|
|
182
|
+
key=lambda c: c.priority,
|
|
183
|
+
reverse=True
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Calculate total tokens
|
|
187
|
+
total_tokens = sum(c.token_count for c in sorted_components)
|
|
188
|
+
|
|
189
|
+
# Truncate if over budget
|
|
190
|
+
if total_tokens > self.max_tokens:
|
|
191
|
+
sorted_components = self._truncate_components(sorted_components)
|
|
192
|
+
|
|
193
|
+
# Merge components
|
|
194
|
+
sections = []
|
|
195
|
+
for component in sorted_components:
|
|
196
|
+
# Add section header and content
|
|
197
|
+
header = f"# {component.name.replace('_', ' ').upper()}"
|
|
198
|
+
sections.append(f"{header}\n{component.content}")
|
|
199
|
+
|
|
200
|
+
result = "\n\n".join(sections)
|
|
201
|
+
|
|
202
|
+
# Update cache if enabled
|
|
203
|
+
if self.enable_caching:
|
|
204
|
+
self._assembly_cache = result
|
|
205
|
+
self._last_components_hash = self._get_components_hash()
|
|
206
|
+
|
|
207
|
+
return result
|
|
208
|
+
|
|
209
|
+
def _get_components_hash(self) -> str:
|
|
210
|
+
"""
|
|
211
|
+
Generate hash for current component configuration
|
|
212
|
+
|
|
213
|
+
优化版本:
|
|
214
|
+
- 使用 blake2b 替代 MD5(更快)
|
|
215
|
+
- 直接update字节而非拼接字符串
|
|
216
|
+
- 移除不必要的排序
|
|
217
|
+
"""
|
|
218
|
+
# 使用 blake2b,比 MD5 更快且安全
|
|
219
|
+
hasher = hashlib.blake2b(digest_size=16)
|
|
220
|
+
|
|
221
|
+
# 直接更新hasher,避免字符串拼接
|
|
222
|
+
for comp in self.components:
|
|
223
|
+
hasher.update(comp.name.encode())
|
|
224
|
+
hasher.update(str(comp.priority).encode())
|
|
225
|
+
hasher.update(str(comp.token_count).encode())
|
|
226
|
+
hasher.update(b'1' if comp.truncatable else b'0')
|
|
227
|
+
|
|
228
|
+
return hasher.hexdigest()
|
|
229
|
+
|
|
230
|
+
def adjust_priority(self, component_name: str, new_priority: int) -> bool:
|
|
231
|
+
"""
|
|
232
|
+
Dynamically adjust component priority.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
component_name: Name of the component to adjust
|
|
236
|
+
new_priority: New priority value
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
True if component was found and adjusted, False otherwise
|
|
240
|
+
"""
|
|
241
|
+
for component in self.components:
|
|
242
|
+
if component.name == component_name:
|
|
243
|
+
component.priority = new_priority
|
|
244
|
+
# Clear cache since configuration changed
|
|
245
|
+
if self.enable_caching:
|
|
246
|
+
self._assembly_cache = None
|
|
247
|
+
self._last_components_hash = None
|
|
248
|
+
return True
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
def get_component_stats(self) -> Dict[str, Any]:
|
|
252
|
+
"""Get statistics about current components"""
|
|
253
|
+
if not self.components:
|
|
254
|
+
return {"total_components": 0, "total_tokens": 0}
|
|
255
|
+
|
|
256
|
+
total_tokens = sum(c.token_count for c in self.components)
|
|
257
|
+
priority_distribution = {}
|
|
258
|
+
|
|
259
|
+
for comp in self.components:
|
|
260
|
+
priority_distribution[comp.priority] = priority_distribution.get(comp.priority, 0) + 1
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
"total_components": len(self.components),
|
|
264
|
+
"total_tokens": total_tokens,
|
|
265
|
+
"budget_utilization": total_tokens / self.max_tokens if self.max_tokens > 0 else 0,
|
|
266
|
+
"priority_distribution": priority_distribution,
|
|
267
|
+
"cache_enabled": self.enable_caching,
|
|
268
|
+
"cache_size": len(self._component_cache) if self.enable_caching else 0
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
def clear_cache(self) -> None:
|
|
272
|
+
"""Clear all caches"""
|
|
273
|
+
if self.enable_caching:
|
|
274
|
+
self._component_cache.clear()
|
|
275
|
+
self._assembly_cache = None
|
|
276
|
+
self._last_components_hash = None
|
|
277
|
+
|
|
278
|
+
def _truncate_components(
|
|
279
|
+
self,
|
|
280
|
+
components: List[ContextComponent]
|
|
281
|
+
) -> List[ContextComponent]:
|
|
282
|
+
"""
|
|
283
|
+
Intelligently truncate components to fit token budget.
|
|
284
|
+
|
|
285
|
+
Strategy:
|
|
286
|
+
1. Always include non-truncatable components
|
|
287
|
+
2. Add truncatable components by priority
|
|
288
|
+
3. Truncate lower-priority components if needed
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
components: Sorted list of components (by priority, descending)
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
List of components that fit within budget
|
|
295
|
+
"""
|
|
296
|
+
budget_remaining = self.max_tokens
|
|
297
|
+
result = []
|
|
298
|
+
|
|
299
|
+
# Phase 1: Add all non-truncatable components
|
|
300
|
+
for comp in components:
|
|
301
|
+
if not comp.truncatable:
|
|
302
|
+
if comp.token_count <= budget_remaining:
|
|
303
|
+
result.append(comp)
|
|
304
|
+
budget_remaining -= comp.token_count
|
|
305
|
+
else:
|
|
306
|
+
# Non-truncatable component is too large
|
|
307
|
+
print(
|
|
308
|
+
f"Warning: Non-truncatable component '{comp.name}' "
|
|
309
|
+
f"({comp.token_count} tokens) exceeds remaining budget "
|
|
310
|
+
f"({budget_remaining} tokens). Skipping."
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
# Phase 2: Add truncatable components
|
|
314
|
+
truncatable = [c for c in components if c.truncatable]
|
|
315
|
+
|
|
316
|
+
for comp in truncatable:
|
|
317
|
+
if comp.token_count <= budget_remaining:
|
|
318
|
+
# Add complete component
|
|
319
|
+
result.append(comp)
|
|
320
|
+
budget_remaining -= comp.token_count
|
|
321
|
+
elif budget_remaining > 100: # Minimum 100 tokens to be useful
|
|
322
|
+
# Truncate and add
|
|
323
|
+
truncated_content = self._truncate_content(
|
|
324
|
+
comp.content,
|
|
325
|
+
budget_remaining - 20 # Reserve 20 tokens for "... (truncated)" marker
|
|
326
|
+
)
|
|
327
|
+
truncated_comp = ContextComponent(
|
|
328
|
+
name=comp.name,
|
|
329
|
+
content=truncated_content,
|
|
330
|
+
priority=comp.priority,
|
|
331
|
+
token_count=self.token_counter(truncated_content),
|
|
332
|
+
truncatable=comp.truncatable
|
|
333
|
+
)
|
|
334
|
+
result.append(truncated_comp)
|
|
335
|
+
budget_remaining = 0
|
|
336
|
+
break
|
|
337
|
+
else:
|
|
338
|
+
# Not enough budget left, skip remaining components
|
|
339
|
+
break
|
|
340
|
+
|
|
341
|
+
return result
|
|
342
|
+
|
|
343
|
+
def _truncate_content(self, content: str, max_tokens: int) -> str:
|
|
344
|
+
"""
|
|
345
|
+
Truncate content to fit within token limit.
|
|
346
|
+
|
|
347
|
+
Strategy: Proportional character truncation with conservative estimation
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
content: Content to truncate
|
|
351
|
+
max_tokens: Maximum tokens allowed
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
Truncated content with marker
|
|
355
|
+
"""
|
|
356
|
+
current_tokens = self.token_counter(content)
|
|
357
|
+
|
|
358
|
+
if current_tokens <= max_tokens:
|
|
359
|
+
return content
|
|
360
|
+
|
|
361
|
+
# Calculate target character count (conservative)
|
|
362
|
+
ratio = max_tokens / current_tokens
|
|
363
|
+
target_chars = int(len(content) * ratio * 0.95) # 5% safety margin
|
|
364
|
+
|
|
365
|
+
if target_chars < 100:
|
|
366
|
+
# Too small to be useful
|
|
367
|
+
return ""
|
|
368
|
+
|
|
369
|
+
# Truncate and add marker
|
|
370
|
+
truncated = content[:target_chars].rsplit(' ', 1)[0] # Truncate at word boundary
|
|
371
|
+
return f"{truncated}\n\n... (truncated due to token limit)"
|
|
372
|
+
|
|
373
|
+
def _estimate_tokens(self, text: str) -> int:
|
|
374
|
+
"""
|
|
375
|
+
Simple token estimation.
|
|
376
|
+
|
|
377
|
+
Rule of thumb: 1 token ≈ 4 characters for English text
|
|
378
|
+
This is a conservative estimate that works reasonably well.
|
|
379
|
+
|
|
380
|
+
For precise counting, use a model-specific tokenizer.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
text: Text to estimate
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Estimated token count
|
|
387
|
+
"""
|
|
388
|
+
return len(text) // 4
|
|
389
|
+
|
|
390
|
+
def get_summary(self) -> Dict:
|
|
391
|
+
"""
|
|
392
|
+
Get assembly summary for debugging and monitoring.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Dictionary containing:
|
|
396
|
+
- components: List of component info (name, priority, tokens, truncatable)
|
|
397
|
+
- total_tokens: Sum of all component tokens
|
|
398
|
+
- budget: Maximum token budget
|
|
399
|
+
- overflow: Tokens over budget (0 if within budget)
|
|
400
|
+
- utilization: Budget utilization percentage
|
|
401
|
+
"""
|
|
402
|
+
total_tokens = sum(c.token_count for c in self.components)
|
|
403
|
+
overflow = max(0, total_tokens - self.max_tokens)
|
|
404
|
+
utilization = (total_tokens / self.max_tokens * 100) if self.max_tokens > 0 else 0
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
"components": [
|
|
408
|
+
{
|
|
409
|
+
"name": c.name,
|
|
410
|
+
"priority": c.priority,
|
|
411
|
+
"tokens": c.token_count,
|
|
412
|
+
"truncatable": c.truncatable
|
|
413
|
+
}
|
|
414
|
+
for c in sorted(self.components, key=lambda x: x.priority, reverse=True)
|
|
415
|
+
],
|
|
416
|
+
"total_tokens": total_tokens,
|
|
417
|
+
"budget": self.max_tokens,
|
|
418
|
+
"overflow": overflow,
|
|
419
|
+
"utilization": round(utilization, 2)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
def clear(self) -> None:
|
|
423
|
+
"""Clear all components."""
|
|
424
|
+
self.components.clear()
|
|
425
|
+
|
|
426
|
+
def __len__(self) -> int:
|
|
427
|
+
"""Return number of components."""
|
|
428
|
+
return len(self.components)
|
|
429
|
+
|
|
430
|
+
def __repr__(self) -> str:
|
|
431
|
+
"""String representation."""
|
|
432
|
+
summary = self.get_summary()
|
|
433
|
+
return (
|
|
434
|
+
f"ContextAssembler(components={len(self.components)}, "
|
|
435
|
+
f"tokens={summary['total_tokens']}/{summary['budget']}, "
|
|
436
|
+
f"utilization={summary['utilization']}%)"
|
|
437
|
+
)
|