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.
Files changed (95) hide show
  1. devsquad-3.6.0.dist-info/METADATA +944 -0
  2. devsquad-3.6.0.dist-info/RECORD +95 -0
  3. devsquad-3.6.0.dist-info/WHEEL +5 -0
  4. devsquad-3.6.0.dist-info/entry_points.txt +2 -0
  5. devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
  6. devsquad-3.6.0.dist-info/top_level.txt +2 -0
  7. scripts/__init__.py +0 -0
  8. scripts/ai_semantic_matcher.py +512 -0
  9. scripts/alert_manager.py +505 -0
  10. scripts/api/__init__.py +43 -0
  11. scripts/api/models.py +386 -0
  12. scripts/api/routes/__init__.py +20 -0
  13. scripts/api/routes/dispatch.py +348 -0
  14. scripts/api/routes/lifecycle.py +330 -0
  15. scripts/api/routes/metrics_gates.py +347 -0
  16. scripts/api_server.py +318 -0
  17. scripts/auth.py +451 -0
  18. scripts/cli/__init__.py +1 -0
  19. scripts/cli/cli_visual.py +642 -0
  20. scripts/cli.py +1094 -0
  21. scripts/collaboration/__init__.py +212 -0
  22. scripts/collaboration/_version.py +1 -0
  23. scripts/collaboration/agent_briefing.py +656 -0
  24. scripts/collaboration/ai_semantic_matcher.py +260 -0
  25. scripts/collaboration/anchor_checker.py +281 -0
  26. scripts/collaboration/anti_rationalization.py +470 -0
  27. scripts/collaboration/async_integration_example.py +255 -0
  28. scripts/collaboration/batch_scheduler.py +149 -0
  29. scripts/collaboration/checkpoint_manager.py +561 -0
  30. scripts/collaboration/ci_feedback_adapter.py +351 -0
  31. scripts/collaboration/code_map_generator.py +247 -0
  32. scripts/collaboration/concern_pack_loader.py +352 -0
  33. scripts/collaboration/confidence_score.py +496 -0
  34. scripts/collaboration/config_loader.py +188 -0
  35. scripts/collaboration/consensus.py +244 -0
  36. scripts/collaboration/context_compressor.py +533 -0
  37. scripts/collaboration/coordinator.py +668 -0
  38. scripts/collaboration/dispatcher.py +1636 -0
  39. scripts/collaboration/dual_layer_context.py +128 -0
  40. scripts/collaboration/enhanced_worker.py +539 -0
  41. scripts/collaboration/feature_usage_tracker.py +206 -0
  42. scripts/collaboration/five_axis_consensus.py +334 -0
  43. scripts/collaboration/input_validator.py +401 -0
  44. scripts/collaboration/integration_example.py +287 -0
  45. scripts/collaboration/intent_workflow_mapper.py +350 -0
  46. scripts/collaboration/language_parsers.py +269 -0
  47. scripts/collaboration/lifecycle_protocol.py +1446 -0
  48. scripts/collaboration/llm_backend.py +453 -0
  49. scripts/collaboration/llm_cache.py +448 -0
  50. scripts/collaboration/llm_cache_async.py +347 -0
  51. scripts/collaboration/llm_retry.py +387 -0
  52. scripts/collaboration/llm_retry_async.py +389 -0
  53. scripts/collaboration/mce_adapter.py +597 -0
  54. scripts/collaboration/memory_bridge.py +1607 -0
  55. scripts/collaboration/models.py +537 -0
  56. scripts/collaboration/null_providers.py +297 -0
  57. scripts/collaboration/operation_classifier.py +289 -0
  58. scripts/collaboration/output_slicer.py +225 -0
  59. scripts/collaboration/performance_monitor.py +462 -0
  60. scripts/collaboration/permission_guard.py +865 -0
  61. scripts/collaboration/prompt_assembler.py +756 -0
  62. scripts/collaboration/prompt_variant_generator.py +483 -0
  63. scripts/collaboration/protocols.py +267 -0
  64. scripts/collaboration/report_formatter.py +352 -0
  65. scripts/collaboration/retrospective.py +279 -0
  66. scripts/collaboration/role_matcher.py +92 -0
  67. scripts/collaboration/role_template_market.py +352 -0
  68. scripts/collaboration/rule_collector.py +678 -0
  69. scripts/collaboration/scratchpad.py +346 -0
  70. scripts/collaboration/skill_registry.py +151 -0
  71. scripts/collaboration/skillifier.py +878 -0
  72. scripts/collaboration/standardized_role_template.py +317 -0
  73. scripts/collaboration/task_completion_checker.py +237 -0
  74. scripts/collaboration/test_quality_guard.py +695 -0
  75. scripts/collaboration/unified_gate_engine.py +598 -0
  76. scripts/collaboration/usage_tracker.py +309 -0
  77. scripts/collaboration/user_friendly_error.py +176 -0
  78. scripts/collaboration/verification_gate.py +312 -0
  79. scripts/collaboration/warmup_manager.py +635 -0
  80. scripts/collaboration/worker.py +513 -0
  81. scripts/collaboration/workflow_engine.py +684 -0
  82. scripts/dashboard.py +1088 -0
  83. scripts/generate_benchmark_report.py +786 -0
  84. scripts/history_manager.py +604 -0
  85. scripts/mcp_server.py +289 -0
  86. skills/__init__.py +32 -0
  87. skills/dispatch/handler.py +52 -0
  88. skills/intent/handler.py +59 -0
  89. skills/registry.py +67 -0
  90. skills/retrospective/__init__.py +0 -0
  91. skills/retrospective/handler.py +125 -0
  92. skills/review/handler.py +356 -0
  93. skills/security/handler.py +454 -0
  94. skills/test/__init__.py +0 -0
  95. 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)