devsquad 3.6.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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
ContextCompressor - 3-Level Context Compression Strategy
|
|
5
|
+
|
|
6
|
+
Based on Claude Code's context management approach:
|
|
7
|
+
Level 1: SNIP - Fine-grained pruning of old conversation segments
|
|
8
|
+
Level 2: SessionMemory - Extract key info to structured memory, clear window
|
|
9
|
+
Level 3: FullCompact - LLM-style summary generation (simulated)
|
|
10
|
+
|
|
11
|
+
Trigger thresholds:
|
|
12
|
+
< 60K tokens → No compression
|
|
13
|
+
60K - 80K → Level 1 SNIP
|
|
14
|
+
80K - 100K → Level 2 SessionMemory
|
|
15
|
+
> 100K → Level 3 FullCompact
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
import json
|
|
20
|
+
import hashlib
|
|
21
|
+
import threading
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
25
|
+
from enum import Enum
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class CompressionLevel(Enum):
|
|
29
|
+
NONE = 0
|
|
30
|
+
SNIP = 1
|
|
31
|
+
SESSION_MEMORY = 2
|
|
32
|
+
FULL_COMPACT = 3
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class MessageType(Enum):
|
|
36
|
+
SYSTEM = "system"
|
|
37
|
+
USER = "user"
|
|
38
|
+
ASSISTANT = "assistant"
|
|
39
|
+
TOOL_CALL = "tool_call"
|
|
40
|
+
TOOL_RESULT = "tool_result"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class MemoryCategory(Enum):
|
|
44
|
+
DECISION = "decision"
|
|
45
|
+
FINDING = "finding"
|
|
46
|
+
DELIVERABLE = "deliverable"
|
|
47
|
+
TODO = "todo"
|
|
48
|
+
ERROR = "error"
|
|
49
|
+
CONSTRAINT = "constraint"
|
|
50
|
+
QUESTION = "question"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class Message:
|
|
55
|
+
message_id: str = field(default_factory=lambda: f"msg-{hashlib.md5(str(datetime.now()).encode()).hexdigest()[:12]}")
|
|
56
|
+
role: str = "user"
|
|
57
|
+
content: str = ""
|
|
58
|
+
msg_type: MessageType = MessageType.USER
|
|
59
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
60
|
+
token_count: int = 0
|
|
61
|
+
importance_score: float = 0.5
|
|
62
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
63
|
+
|
|
64
|
+
def to_dict(self) -> Dict:
|
|
65
|
+
return {
|
|
66
|
+
"message_id": self.message_id,
|
|
67
|
+
"role": self.role,
|
|
68
|
+
"content": self.content,
|
|
69
|
+
"msg_type": self.msg_type.value,
|
|
70
|
+
"timestamp": self.timestamp.isoformat(),
|
|
71
|
+
"token_count": self.token_count,
|
|
72
|
+
"importance_score": self.importance_score,
|
|
73
|
+
"metadata": self.metadata,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_dict(cls, d: Dict) -> "Message":
|
|
78
|
+
ts = d.get("timestamp")
|
|
79
|
+
return cls(
|
|
80
|
+
message_id=d.get("message_id", ""),
|
|
81
|
+
role=d.get("role", "user"),
|
|
82
|
+
content=d.get("content", ""),
|
|
83
|
+
msg_type=MessageType(d.get("msg_type", "user")),
|
|
84
|
+
timestamp=datetime.fromisoformat(ts) if ts else datetime.now(),
|
|
85
|
+
token_count=d.get("token_count", 0),
|
|
86
|
+
importance_score=d.get("importance_score", 0.5),
|
|
87
|
+
metadata=d.get("metadata", {}),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class MemoryEntry:
|
|
93
|
+
entry_id: str = field(default_factory=lambda: f"mem-{hashlib.md5(str(datetime.now()).encode()).hexdigest()[:12]}")
|
|
94
|
+
category: MemoryCategory = MemoryCategory.FINDING
|
|
95
|
+
content: str = ""
|
|
96
|
+
source_message_ids: List[str] = field(default_factory=list)
|
|
97
|
+
confidence: float = 0.8
|
|
98
|
+
tags: List[str] = field(default_factory=list)
|
|
99
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
100
|
+
last_accessed: datetime = field(default_factory=datetime.now)
|
|
101
|
+
|
|
102
|
+
def to_dict(self) -> Dict:
|
|
103
|
+
return {
|
|
104
|
+
"entry_id": self.entry_id,
|
|
105
|
+
"category": self.category.value,
|
|
106
|
+
"content": self.content,
|
|
107
|
+
"source_message_ids": self.source_message_ids,
|
|
108
|
+
"confidence": self.confidence,
|
|
109
|
+
"tags": self.tags,
|
|
110
|
+
"created_at": self.created_at.isoformat(),
|
|
111
|
+
"last_accessed": self.last_accessed.isoformat(),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_dict(cls, d: Dict) -> "MemoryEntry":
|
|
116
|
+
ca = d.get("created_at")
|
|
117
|
+
la = d.get("last_accessed")
|
|
118
|
+
return cls(
|
|
119
|
+
entry_id=d.get("entry_id", ""),
|
|
120
|
+
category=MemoryCategory(d.get("category", "finding")),
|
|
121
|
+
content=d.get("content", ""),
|
|
122
|
+
source_message_ids=d.get("source_message_ids", []),
|
|
123
|
+
confidence=d.get("confidence", 0.8),
|
|
124
|
+
tags=d.get("tags", []),
|
|
125
|
+
created_at=datetime.fromisoformat(ca) if ca else datetime.now(),
|
|
126
|
+
last_accessed=datetime.fromisoformat(la) if la else datetime.now(),
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class CompressedContext:
|
|
132
|
+
original_token_count: int = 0
|
|
133
|
+
compressed_token_count: int = 0
|
|
134
|
+
compression_level: CompressionLevel = CompressionLevel.NONE
|
|
135
|
+
messages: List[Message] = field(default_factory=list)
|
|
136
|
+
session_memory: List[MemoryEntry] = field(default_factory=list)
|
|
137
|
+
summary: str = ""
|
|
138
|
+
compression_ratio: float = 0.0
|
|
139
|
+
compressed_at: datetime = field(default_factory=datetime.now)
|
|
140
|
+
stats: Dict[str, Any] = field(default_factory=dict)
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def reduction_percent(self) -> float:
|
|
144
|
+
if self.original_token_count == 0:
|
|
145
|
+
return 0.0
|
|
146
|
+
return (1.0 - self.compressed_token_count / self.original_token_count) * 100
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Token estimation: ~4 chars per token for English, ~1.5 for Chinese mixed
|
|
150
|
+
_TOKEN_CHARS_RATIO = {
|
|
151
|
+
"default": 4.0,
|
|
152
|
+
"chinese": 1.5,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Importance keywords that boost score
|
|
156
|
+
_HIGH_IMPORTANCE_KEYWORDS = [
|
|
157
|
+
"决定", "结论", "方案", "架构", "设计", "关键",
|
|
158
|
+
"decision", "conclusion", "architecture", "design", "critical",
|
|
159
|
+
"错误", "修复", "问题", "bug", "error", "fix", "issue",
|
|
160
|
+
"必须", "要求", "验收", "标准", "must", "requirement", "acceptance",
|
|
161
|
+
"TODO", "待办", "下一步", "action item", "next step",
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
_LOW_IMPORTANCE_PATTERNS = [
|
|
165
|
+
r"^(好的|OK|明白|了解|收到|嗯|哦|啊|哈|呵呵)",
|
|
166
|
+
r"^((是的|对|没错|正确|确实|确实如此)[,。!?]?)$",
|
|
167
|
+
r"^((谢谢|感谢|多谢|辛苦了|好的吧|行吧)[,。!?])?$",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class ContextCompressor:
|
|
172
|
+
"""3-Level Context Compressor"""
|
|
173
|
+
|
|
174
|
+
DEFAULT_THRESHOLDS = {
|
|
175
|
+
CompressionLevel.SNIP: 60000,
|
|
176
|
+
CompressionLevel.SESSION_MEMORY: 80000,
|
|
177
|
+
CompressionLevel.FULL_COMPACT: 100000,
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
def __init__(self, token_threshold: int = 100000,
|
|
181
|
+
thresholds: Optional[Dict[int, int]] = None):
|
|
182
|
+
self.token_threshold = token_threshold
|
|
183
|
+
self.thresholds = thresholds or self.DEFAULT_THRESHOLDS
|
|
184
|
+
self._session_memory: List[MemoryEntry] = []
|
|
185
|
+
self._compression_log: List[Dict] = []
|
|
186
|
+
self._lock = threading.RLock()
|
|
187
|
+
|
|
188
|
+
def estimate_tokens(self, text: str) -> int:
|
|
189
|
+
if not text:
|
|
190
|
+
return 0
|
|
191
|
+
chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text))
|
|
192
|
+
other_chars = len(text) - chinese_chars
|
|
193
|
+
chinese_tokens = int(chinese_chars / _TOKEN_CHARS_RATIO["chinese"])
|
|
194
|
+
other_tokens = int(other_chars / _TOKEN_CHARS_RATIO["default"])
|
|
195
|
+
return chinese_tokens + other_tokens
|
|
196
|
+
|
|
197
|
+
def estimate_messages_tokens(self, messages: List[Message]) -> int:
|
|
198
|
+
total = 0
|
|
199
|
+
for m in messages:
|
|
200
|
+
if hasattr(m, 'token_count') and getattr(m, 'token_count', 0) > 0:
|
|
201
|
+
total += m.token_count
|
|
202
|
+
elif hasattr(m, 'content'):
|
|
203
|
+
est = self.estimate_tokens(m.content)
|
|
204
|
+
m.token_count = est
|
|
205
|
+
total += est
|
|
206
|
+
else:
|
|
207
|
+
total += self.estimate_tokens(str(m))
|
|
208
|
+
return total
|
|
209
|
+
|
|
210
|
+
def check_and_compress(self, messages: List[Message],
|
|
211
|
+
force_level: Optional[CompressionLevel] = None) -> CompressedContext:
|
|
212
|
+
with self._lock:
|
|
213
|
+
total_tokens = self.estimate_messages_tokens(messages)
|
|
214
|
+
|
|
215
|
+
if force_level is not None:
|
|
216
|
+
level = force_level
|
|
217
|
+
elif total_tokens >= self.thresholds.get(CompressionLevel.FULL_COMPACT, 100000):
|
|
218
|
+
level = CompressionLevel.FULL_COMPACT
|
|
219
|
+
elif total_tokens >= self.thresholds.get(CompressionLevel.SESSION_MEMORY, 80000):
|
|
220
|
+
level = CompressionLevel.SESSION_MEMORY
|
|
221
|
+
elif total_tokens >= self.thresholds.get(CompressionLevel.SNIP, 60000):
|
|
222
|
+
level = CompressionLevel.SNIP
|
|
223
|
+
else:
|
|
224
|
+
level = CompressionLevel.NONE
|
|
225
|
+
|
|
226
|
+
result = CompressedContext(
|
|
227
|
+
original_token_count=total_tokens,
|
|
228
|
+
compression_level=level,
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
if level == CompressionLevel.NONE:
|
|
232
|
+
result.messages = list(messages)
|
|
233
|
+
result.compressed_token_count = total_tokens
|
|
234
|
+
result.stats["reason"] = "under_threshold"
|
|
235
|
+
elif level == CompressionLevel.SNIP:
|
|
236
|
+
result = self._level1_snip(messages, result)
|
|
237
|
+
elif level == CompressionLevel.SESSION_MEMORY:
|
|
238
|
+
result = self._level2_session_memory(messages, result)
|
|
239
|
+
elif level == CompressionLevel.FULL_COMPACT:
|
|
240
|
+
result = self._level3_full_compact(messages, result)
|
|
241
|
+
|
|
242
|
+
result.session_memory = list(self._session_memory)
|
|
243
|
+
result.stats["original_message_count"] = len(messages)
|
|
244
|
+
result.stats["remaining_message_count"] = len(result.messages)
|
|
245
|
+
result.stats["memory_entries"] = len(result.session_memory)
|
|
246
|
+
|
|
247
|
+
self._log_compression(result)
|
|
248
|
+
return result
|
|
249
|
+
|
|
250
|
+
def _score_importance(self, message: Message) -> float:
|
|
251
|
+
score = 0.5
|
|
252
|
+
content_lower = message.content.lower()
|
|
253
|
+
|
|
254
|
+
for kw in _HIGH_IMPORTANCE_KEYWORDS:
|
|
255
|
+
if kw in content_lower:
|
|
256
|
+
score += 0.15
|
|
257
|
+
if kw in message.content:
|
|
258
|
+
score += 0.10
|
|
259
|
+
|
|
260
|
+
for pattern in _LOW_IMPORTANCE_PATTERNS:
|
|
261
|
+
if re.match(pattern, message.content.strip()):
|
|
262
|
+
score -= 0.30
|
|
263
|
+
|
|
264
|
+
if message.msg_type == MessageType.SYSTEM:
|
|
265
|
+
score += 0.20
|
|
266
|
+
elif message.msg_type == MessageType.ASSISTANT:
|
|
267
|
+
lines = message.content.strip().split('\n')
|
|
268
|
+
if len(lines) <= 2:
|
|
269
|
+
score -= 0.10
|
|
270
|
+
else:
|
|
271
|
+
has_structure = any(c in message.content for c in ['#', '*', '-', '1.', '2.'])
|
|
272
|
+
if has_structure:
|
|
273
|
+
score += 0.15
|
|
274
|
+
|
|
275
|
+
if message.metadata.get("is_error") or message.metadata.get("error"):
|
|
276
|
+
score += 0.20
|
|
277
|
+
if message.metadata.get("is_decision"):
|
|
278
|
+
score += 0.25
|
|
279
|
+
if message.metadata.get("is_deliverable"):
|
|
280
|
+
score += 0.20
|
|
281
|
+
|
|
282
|
+
return max(0.0, min(1.0, score))
|
|
283
|
+
|
|
284
|
+
def _level1_snip(self, messages: List[Message],
|
|
285
|
+
ctx: CompressedContext) -> CompressedContext:
|
|
286
|
+
scored = [(m, self._score_importance(m)) for m in messages]
|
|
287
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
288
|
+
|
|
289
|
+
target_ratio = 0.65
|
|
290
|
+
total_tokens = sum(m.token_count or self.estimate_tokens(m.content) for m, _ in scored)
|
|
291
|
+
keep_budget = int(total_tokens * target_ratio)
|
|
292
|
+
|
|
293
|
+
kept = []
|
|
294
|
+
kept_tokens = 0
|
|
295
|
+
snipped = []
|
|
296
|
+
snipped_tokens = 0
|
|
297
|
+
|
|
298
|
+
for msg, score in scored:
|
|
299
|
+
tokens = msg.token_count or self.estimate_tokens(msg.content)
|
|
300
|
+
if kept_tokens + tokens <= keep_budget or score >= 0.6:
|
|
301
|
+
kept.append(msg)
|
|
302
|
+
kept_tokens += tokens
|
|
303
|
+
else:
|
|
304
|
+
snipped.append(msg)
|
|
305
|
+
snipped_tokens += tokens
|
|
306
|
+
|
|
307
|
+
mem_entry = self._extract_memory_from_message(msg)
|
|
308
|
+
if mem_entry:
|
|
309
|
+
self._session_memory.append(mem_entry)
|
|
310
|
+
|
|
311
|
+
ctx.messages = kept
|
|
312
|
+
ctx.compressed_token_count = kept_tokens
|
|
313
|
+
ctx.summary = f"SNIP: Kept {len(kept)}/{len(messages)} msgs ({kept_tokens}/{total_tokens} tokens)"
|
|
314
|
+
ctx.stats["snipped_count"] = len(snipped)
|
|
315
|
+
ctx.stats["snipped_tokens"] = snipped_tokens
|
|
316
|
+
ctx.stats["avg_kept_importance"] = (
|
|
317
|
+
sum(s for _, s in scored[:len(kept)]) / max(1, len(kept))
|
|
318
|
+
if kept else 0
|
|
319
|
+
)
|
|
320
|
+
return ctx
|
|
321
|
+
|
|
322
|
+
def _level2_session_memory(self, messages: List[Message],
|
|
323
|
+
ctx: CompressedContext) -> CompressedContext:
|
|
324
|
+
for msg in messages:
|
|
325
|
+
mem = self._extract_memory_from_message(msg)
|
|
326
|
+
if mem:
|
|
327
|
+
self._session_memory.append(mem)
|
|
328
|
+
|
|
329
|
+
recent_msgs = messages[-3:] if len(messages) > 3 else list(messages)
|
|
330
|
+
ctx.messages = recent_msgs
|
|
331
|
+
ctx.compressed_token_count = self.estimate_messages_tokens(recent_msgs)
|
|
332
|
+
ctx.summary = (
|
|
333
|
+
f"SessionMemory: Extracted {len(self._session_memory)} memory entries, "
|
|
334
|
+
f"kept {len(recent_msgs)} recent messages"
|
|
335
|
+
)
|
|
336
|
+
ctx.stats["memory_extracted"] = len(self._session_memory)
|
|
337
|
+
ctx.stats["categories"] = self._get_memory_category_counts()
|
|
338
|
+
return ctx
|
|
339
|
+
|
|
340
|
+
def _level3_full_compact(self, messages: List[Message],
|
|
341
|
+
ctx: CompressedContext) -> CompressedContext:
|
|
342
|
+
for msg in messages:
|
|
343
|
+
mem = self._extract_memory_from_message(msg)
|
|
344
|
+
if mem:
|
|
345
|
+
self._session_memory.append(mem)
|
|
346
|
+
|
|
347
|
+
decisions = [m for m in self._session_memory if m.category == MemoryCategory.DECISION]
|
|
348
|
+
findings = [m for m in self._session_memory if m.category == MemoryCategory.FINDING]
|
|
349
|
+
todos = [m for m in self._session_memory if m.category == MemoryCategory.TODO]
|
|
350
|
+
errors = [m for m in self._session_memory if m.category == MemoryCategory.ERROR]
|
|
351
|
+
deliverables = [m for m in self._session_memory if m.category == MemoryCategory.DELIVERABLE]
|
|
352
|
+
|
|
353
|
+
summary_lines = ["=== FullCompact Summary ==="]
|
|
354
|
+
summary_lines.append(f"Total messages processed: {len(messages)}")
|
|
355
|
+
summary_lines.append(f"Memory entries extracted: {len(self._session_memory)}")
|
|
356
|
+
|
|
357
|
+
if decisions:
|
|
358
|
+
summary_lines.append(f"\n## Key Decisions ({len(decisions)})")
|
|
359
|
+
for d in decisions[:8]:
|
|
360
|
+
summary_lines.append(f"- [{d.category.value}] {d.content[:120]}")
|
|
361
|
+
|
|
362
|
+
if deliverables:
|
|
363
|
+
summary_lines.append(f"\n## Deliverables ({len(deliverables)})")
|
|
364
|
+
for d in deliverables[:6]:
|
|
365
|
+
summary_lines.append(f"- {d.content[:120]}")
|
|
366
|
+
|
|
367
|
+
if todos:
|
|
368
|
+
summary_lines.append(f"\n## Action Items ({len(todos)})")
|
|
369
|
+
for t in todos[:6]:
|
|
370
|
+
summary_lines.append(f"- [ ] {t.content[:120]}")
|
|
371
|
+
|
|
372
|
+
if errors:
|
|
373
|
+
summary_lines.append(f"\n## Errors/Issues ({len(errors)})")
|
|
374
|
+
for e in errors[:4]:
|
|
375
|
+
summary_lines.append(f"- {e.content[:120]}")
|
|
376
|
+
|
|
377
|
+
if findings:
|
|
378
|
+
summary_lines.append(f"\n## Key Findings ({len(findings)})")
|
|
379
|
+
for f in findings[:5]:
|
|
380
|
+
summary_lines.append(f"- {f.content[:120]}")
|
|
381
|
+
|
|
382
|
+
ctx.messages = []
|
|
383
|
+
ctx.compressed_token_count = self.estimate_tokens("\n".join(summary_lines))
|
|
384
|
+
ctx.summary = "\n".join(summary_lines)
|
|
385
|
+
ctx.stats["decisions"] = len(decisions)
|
|
386
|
+
ctx.stats["findings"] = len(findings)
|
|
387
|
+
ctx.stats["todos"] = len(todos)
|
|
388
|
+
ctx.stats["errors"] = len(errors)
|
|
389
|
+
ctx.stats["deliverables"] = len(deliverables)
|
|
390
|
+
return ctx
|
|
391
|
+
|
|
392
|
+
def _extract_memory_from_message(self, msg: Message) -> Optional[MemoryEntry]:
|
|
393
|
+
content = msg.content.strip()
|
|
394
|
+
if not content or len(content) < 10:
|
|
395
|
+
return None
|
|
396
|
+
|
|
397
|
+
score = self._score_importance(msg)
|
|
398
|
+
if score < 0.35:
|
|
399
|
+
return None
|
|
400
|
+
|
|
401
|
+
category = self._classify_content(content)
|
|
402
|
+
if category is None:
|
|
403
|
+
return None
|
|
404
|
+
|
|
405
|
+
tags = self._extract_tags(content)
|
|
406
|
+
|
|
407
|
+
return MemoryEntry(
|
|
408
|
+
category=category,
|
|
409
|
+
content=content[:500],
|
|
410
|
+
source_message_ids=[msg.message_id],
|
|
411
|
+
confidence=min(1.0, score),
|
|
412
|
+
tags=tags,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def _classify_content(self, content: str) -> Optional[MemoryCategory]:
|
|
416
|
+
content_lower = content.lower()
|
|
417
|
+
|
|
418
|
+
decision_patterns = ["决定", "选择", "采用", "确认", "批准", "方案",
|
|
419
|
+
"decision", "choose", "adopt", "confirm", "approve"]
|
|
420
|
+
todo_patterns = ["todo", "待办", "需要", "下一步", "计划", "后续",
|
|
421
|
+
"need to", "next step", "plan", "follow-up"]
|
|
422
|
+
error_patterns = ["错误", "失败", "异常", "bug", "问题", "无法",
|
|
423
|
+
"error", "fail", "exception", "issue", "cannot"]
|
|
424
|
+
deliverable_patterns = ["交付", "产出", "完成", "实现", "输出", "结果",
|
|
425
|
+
"deliverable", "output", "complete", "implement", "result"]
|
|
426
|
+
question_patterns = ["?", "是否", "如何", "为什么", "什么", "能否"]
|
|
427
|
+
|
|
428
|
+
for p in decision_patterns:
|
|
429
|
+
if p in content_lower:
|
|
430
|
+
return MemoryCategory.DECISION
|
|
431
|
+
for p in todo_patterns:
|
|
432
|
+
if p in content_lower:
|
|
433
|
+
return MemoryCategory.TODO
|
|
434
|
+
for p in error_patterns:
|
|
435
|
+
if p in content_lower:
|
|
436
|
+
return MemoryCategory.ERROR
|
|
437
|
+
for p in deliverable_patterns:
|
|
438
|
+
if p in content_lower:
|
|
439
|
+
return MemoryCategory.DELIVERABLE
|
|
440
|
+
for p in question_patterns:
|
|
441
|
+
if p in content:
|
|
442
|
+
return MemoryCategory.QUESTION
|
|
443
|
+
|
|
444
|
+
if msg_type_hint := getattr(self, '_last_msg_type', None):
|
|
445
|
+
pass
|
|
446
|
+
|
|
447
|
+
return MemoryCategory.FINDING
|
|
448
|
+
|
|
449
|
+
def _extract_tags(self, content: str) -> List[str]:
|
|
450
|
+
tags = []
|
|
451
|
+
tag_patterns = {
|
|
452
|
+
"security": ["安全", "安全漏洞", "注入", "xss", "csrf", "权限"],
|
|
453
|
+
"performance": ["性能", "优化", "慢", "延迟", "瓶颈", "缓存"],
|
|
454
|
+
"architecture": ["架构", "模块", "设计", "接口", "组件"],
|
|
455
|
+
"testing": ["测试", "用例", "覆盖", "验证"],
|
|
456
|
+
"data": ["数据", "数据库", "schema", "模型"],
|
|
457
|
+
"api": ["api", "接口", "endpoint", "rest", "graphql"],
|
|
458
|
+
}
|
|
459
|
+
for tag_name, patterns in tag_patterns.items():
|
|
460
|
+
for p in patterns:
|
|
461
|
+
if p in content.lower():
|
|
462
|
+
tags.append(tag_name)
|
|
463
|
+
break
|
|
464
|
+
return tags
|
|
465
|
+
|
|
466
|
+
def get_session_memory(self, category: Optional[MemoryCategory] = None,
|
|
467
|
+
limit: int = 50) -> List[MemoryEntry]:
|
|
468
|
+
with self._lock:
|
|
469
|
+
if category:
|
|
470
|
+
return [m for m in self._session_memory if m.category == category][:limit]
|
|
471
|
+
return list(self._session_memory)[:limit]
|
|
472
|
+
|
|
473
|
+
def query_memory(self, query: str, limit: int = 20) -> List[MemoryEntry]:
|
|
474
|
+
query_lower = query.lower()
|
|
475
|
+
results = []
|
|
476
|
+
for entry in self._session_memory:
|
|
477
|
+
if query_lower in entry.content.lower() or query_lower in " ".join(entry.tags):
|
|
478
|
+
entry.last_accessed = datetime.now()
|
|
479
|
+
results.append(entry)
|
|
480
|
+
results.sort(key=lambda e: e.last_accessed, reverse=True)
|
|
481
|
+
return results[:limit]
|
|
482
|
+
|
|
483
|
+
def clear_session_memory(self) -> int:
|
|
484
|
+
with self._lock:
|
|
485
|
+
count = len(self._session_memory)
|
|
486
|
+
self._session_memory.clear()
|
|
487
|
+
return count
|
|
488
|
+
|
|
489
|
+
def get_compression_stats(self) -> Dict[str, Any]:
|
|
490
|
+
with self._lock:
|
|
491
|
+
return {
|
|
492
|
+
"total_compressions": len(self._compression_log),
|
|
493
|
+
"memory_entries": len(self._session_memory),
|
|
494
|
+
"memory_by_category": self._get_memory_category_counts(),
|
|
495
|
+
"recent_compressions": self._compression_log[-5:] if self._compression_log else [],
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
def _get_memory_category_counts(self) -> Dict[str, int]:
|
|
499
|
+
counts: Dict[str, int] = {}
|
|
500
|
+
for m in self._session_memory:
|
|
501
|
+
cat = m.category.value
|
|
502
|
+
counts[cat] = counts.get(cat, 0) + 1
|
|
503
|
+
return counts
|
|
504
|
+
|
|
505
|
+
def _log_compression(self, result: CompressedContext):
|
|
506
|
+
self._compression_log.append({
|
|
507
|
+
"timestamp": datetime.now().isoformat(),
|
|
508
|
+
"level": result.compression_level.value,
|
|
509
|
+
"original_tokens": result.original_token_count,
|
|
510
|
+
"compressed_tokens": result.compressed_token_count,
|
|
511
|
+
"reduction_pct": round(result.reduction_percent, 1),
|
|
512
|
+
"summary": result.summary[:200] if result.summary else "",
|
|
513
|
+
})
|
|
514
|
+
if len(self._compression_log) > 100:
|
|
515
|
+
self._compression_log = self._compression_log[-50:]
|
|
516
|
+
|
|
517
|
+
def export_state(self) -> Dict:
|
|
518
|
+
with self._lock:
|
|
519
|
+
return {
|
|
520
|
+
"session_memory": [m.to_dict() for m in self._session_memory],
|
|
521
|
+
"compression_log": self._compression_log[-20:],
|
|
522
|
+
"thresholds": self.thresholds,
|
|
523
|
+
"token_threshold": self.token_threshold,
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
def import_state(self, state: Dict):
|
|
527
|
+
with self._lock:
|
|
528
|
+
self._session_memory = [
|
|
529
|
+
MemoryEntry.from_dict(m) for m in state.get("session_memory", [])
|
|
530
|
+
]
|
|
531
|
+
self._compression_log = state.get("compression_log", [])
|
|
532
|
+
self.thresholds = state.get("thresholds", self.DEFAULT_THRESHOLDS)
|
|
533
|
+
self.token_threshold = state.get("token_threshold", 100000)
|