brainlayer 1.0.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.
- brainlayer/__init__.py +3 -0
- brainlayer/cli/__init__.py +1545 -0
- brainlayer/cli/wizard.py +132 -0
- brainlayer/cli_new.py +151 -0
- brainlayer/client.py +164 -0
- brainlayer/clustering.py +736 -0
- brainlayer/daemon.py +1105 -0
- brainlayer/dashboard/README.md +129 -0
- brainlayer/dashboard/__init__.py +5 -0
- brainlayer/dashboard/app.py +151 -0
- brainlayer/dashboard/search.py +229 -0
- brainlayer/dashboard/views.py +230 -0
- brainlayer/embeddings.py +131 -0
- brainlayer/engine.py +550 -0
- brainlayer/index_new.py +87 -0
- brainlayer/mcp/__init__.py +1558 -0
- brainlayer/migrate.py +205 -0
- brainlayer/paths.py +43 -0
- brainlayer/pipeline/__init__.py +47 -0
- brainlayer/pipeline/analyze_communication.py +508 -0
- brainlayer/pipeline/brain_graph.py +567 -0
- brainlayer/pipeline/chat_tags.py +63 -0
- brainlayer/pipeline/chunk.py +422 -0
- brainlayer/pipeline/classify.py +472 -0
- brainlayer/pipeline/cluster_sampling.py +73 -0
- brainlayer/pipeline/enrichment.py +810 -0
- brainlayer/pipeline/extract.py +66 -0
- brainlayer/pipeline/extract_claude_desktop.py +149 -0
- brainlayer/pipeline/extract_corrections.py +231 -0
- brainlayer/pipeline/extract_markdown.py +195 -0
- brainlayer/pipeline/extract_whatsapp.py +227 -0
- brainlayer/pipeline/git_overlay.py +301 -0
- brainlayer/pipeline/longitudinal_analyzer.py +568 -0
- brainlayer/pipeline/obsidian_export.py +455 -0
- brainlayer/pipeline/operation_grouping.py +486 -0
- brainlayer/pipeline/plan_linking.py +313 -0
- brainlayer/pipeline/sanitize.py +549 -0
- brainlayer/pipeline/semantic_style.py +574 -0
- brainlayer/pipeline/session_enrichment.py +472 -0
- brainlayer/pipeline/style_embed.py +67 -0
- brainlayer/pipeline/style_index.py +139 -0
- brainlayer/pipeline/temporal_chains.py +203 -0
- brainlayer/pipeline/time_batcher.py +248 -0
- brainlayer/pipeline/unified_timeline.py +569 -0
- brainlayer/storage.py +66 -0
- brainlayer/store.py +155 -0
- brainlayer/taxonomy.json +80 -0
- brainlayer/vector_store.py +1891 -0
- brainlayer-1.0.0.dist-info/METADATA +313 -0
- brainlayer-1.0.0.dist-info/RECORD +53 -0
- brainlayer-1.0.0.dist-info/WHEEL +4 -0
- brainlayer-1.0.0.dist-info/entry_points.txt +4 -0
- brainlayer-1.0.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
"""Stage 2: Classify content blocks by type and value.
|
|
2
|
+
|
|
3
|
+
Smart filtering to maximize search quality:
|
|
4
|
+
- Detects and skips tool JSON garbage (e.g., {'file_path': ...})
|
|
5
|
+
- Preserves questions regardless of length (ends with ?)
|
|
6
|
+
- Filters pure acknowledgments ("yes", "ok", "do it")
|
|
7
|
+
- Content-type-aware thresholds
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# SMART FILTERING CONFIGURATION
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
# Minimum length thresholds by content type
|
|
20
|
+
MIN_LENGTH_THRESHOLDS = {
|
|
21
|
+
"user_message": 15, # Short questions can be valuable
|
|
22
|
+
"assistant_text": 50, # Technical explanations can be concise
|
|
23
|
+
"ai_code": 30, # Code can be short but meaningful
|
|
24
|
+
"tool_result": 50, # Tool outputs need context
|
|
25
|
+
"stack_trace": 0, # Always keep stack traces
|
|
26
|
+
}
|
|
27
|
+
DEFAULT_MIN_LENGTH = 50
|
|
28
|
+
|
|
29
|
+
# Pure acknowledgments to skip (case-insensitive, exact match after strip)
|
|
30
|
+
ACKNOWLEDGMENTS = {
|
|
31
|
+
"yes",
|
|
32
|
+
"no",
|
|
33
|
+
"ok",
|
|
34
|
+
"okay",
|
|
35
|
+
"sure",
|
|
36
|
+
"done",
|
|
37
|
+
"got it",
|
|
38
|
+
"thanks",
|
|
39
|
+
"thank you",
|
|
40
|
+
"do it",
|
|
41
|
+
"go ahead",
|
|
42
|
+
"proceed",
|
|
43
|
+
"continue",
|
|
44
|
+
"next",
|
|
45
|
+
"skip",
|
|
46
|
+
"stop",
|
|
47
|
+
"warmup",
|
|
48
|
+
"y",
|
|
49
|
+
"n",
|
|
50
|
+
"k",
|
|
51
|
+
"yep",
|
|
52
|
+
"nope",
|
|
53
|
+
"yup",
|
|
54
|
+
"ah",
|
|
55
|
+
"oh",
|
|
56
|
+
"hmm",
|
|
57
|
+
"ah gotcha",
|
|
58
|
+
"gotcha",
|
|
59
|
+
"i see",
|
|
60
|
+
"makes sense",
|
|
61
|
+
"perfect",
|
|
62
|
+
"great",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Patterns that indicate tool JSON garbage (stringified tool_use inputs)
|
|
66
|
+
TOOL_JSON_PATTERNS = [
|
|
67
|
+
r"^\s*\{\s*['\"](?:command|file_path|pattern|query|url|old_string|new_string|content)['\"]:",
|
|
68
|
+
r"^\s*\{\s*['\"](?:tool_name|function|input|arguments)['\"]:",
|
|
69
|
+
r"^\s*\{\}$", # Empty dict
|
|
70
|
+
r"^\s*\{['\"]todos['\"]\s*:\s*\[\s*\]\s*\}$", # Empty todos
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
# Transitional phrases to skip (assistant text that's ONLY filler, no substance)
|
|
74
|
+
# Only match if the ENTIRE message is transitional (short and starts with filler)
|
|
75
|
+
TRANSITIONAL_PATTERNS = [
|
|
76
|
+
r"^(?:Let me|Now I'll|I'll now|I will now|Checking|Looking at|Running)[^.]*[:.]?$",
|
|
77
|
+
r"^(?:Here's|Here is) (?:the|what)[^.]*[:.]?$",
|
|
78
|
+
r"^(?:Let me check|I'll check|checking now)[:.]?$",
|
|
79
|
+
]
|
|
80
|
+
# Maximum length for transitional text (longer messages likely have substance)
|
|
81
|
+
MAX_TRANSITIONAL_LENGTH = 60
|
|
82
|
+
|
|
83
|
+
# High-value patterns to keep regardless of length
|
|
84
|
+
HIGH_VALUE_PATTERNS = [
|
|
85
|
+
r"\?$", # Questions
|
|
86
|
+
r"(?:error|failed|issue|bug|broken|fix|problem|crash)", # Debugging
|
|
87
|
+
r"(?:because|decided|chose|recommend|should|must)", # Decisions
|
|
88
|
+
r"(?:todo|fixme|hack|workaround)", # Code notes
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ContentType(Enum):
|
|
93
|
+
"""Types of content in Claude Code conversations."""
|
|
94
|
+
|
|
95
|
+
# Conversation content
|
|
96
|
+
AI_CODE = "ai_code" # Code written by Claude (HIGH VALUE)
|
|
97
|
+
STACK_TRACE = "stack_trace" # Error traces (HIGH VALUE - preserve exact)
|
|
98
|
+
USER_MESSAGE = "user_message" # Human questions (HIGH VALUE)
|
|
99
|
+
ASSISTANT_TEXT = "assistant_text" # Claude's explanations
|
|
100
|
+
FILE_READ = "file_read" # Code from tool reads (MEDIUM)
|
|
101
|
+
GIT_DIFF = "git_diff" # Git changes (MEDIUM)
|
|
102
|
+
BUILD_LOG = "build_log" # Build/test output (LOW - summarize)
|
|
103
|
+
DIRECTORY_LISTING = "dir_listing" # ls output (LOW - structure only)
|
|
104
|
+
CONFIG = "config" # Config files (MEDIUM)
|
|
105
|
+
NOISE = "noise" # progress, queue-operation (SKIP)
|
|
106
|
+
|
|
107
|
+
# Markdown content types
|
|
108
|
+
LEARNING = "learning" # Curated learnings (HIGH VALUE)
|
|
109
|
+
SKILL = "skill" # Skill definitions (HIGH VALUE)
|
|
110
|
+
PROJECT_CONFIG = "project_config" # CLAUDE.md files (HIGH VALUE)
|
|
111
|
+
RESEARCH = "research" # Research documents (HIGH VALUE)
|
|
112
|
+
PRD_ARCHIVE = "prd_archive" # PRD archives (MEDIUM)
|
|
113
|
+
VERIFICATION = "verification" # Verification rounds (LOW)
|
|
114
|
+
DOCUMENTATION = "documentation" # General markdown docs (MEDIUM)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class ContentValue(Enum):
|
|
118
|
+
"""Value level for preservation decisions."""
|
|
119
|
+
|
|
120
|
+
HIGH = "high" # Preserve verbatim
|
|
121
|
+
MEDIUM = "medium" # Context-dependent
|
|
122
|
+
LOW = "low" # Summarize or mask
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@dataclass
|
|
126
|
+
class ClassifiedContent:
|
|
127
|
+
"""A classified piece of content."""
|
|
128
|
+
|
|
129
|
+
content: str
|
|
130
|
+
content_type: ContentType
|
|
131
|
+
value: ContentValue
|
|
132
|
+
metadata: dict[str, Any]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Stack trace detection patterns (ReDoS-safe: avoid unbounded .*)
|
|
136
|
+
STACK_TRACE_PATTERNS = [
|
|
137
|
+
r"^Traceback \(most recent call last\):", # Python
|
|
138
|
+
r"^\s+at\s+[\w.]+\([\w.]+:\d+\)", # Java
|
|
139
|
+
r"at\s+[^\(]+\([^\)]+:\d+:\d+\)", # JavaScript/Node (ReDoS-safe)
|
|
140
|
+
r"^\s+File \"[^\"]+\", line \d+", # Python file reference (ReDoS-safe)
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
# Build log patterns
|
|
144
|
+
BUILD_LOG_PATTERNS = [
|
|
145
|
+
r"^\s*\d+ passing", # Test results
|
|
146
|
+
r"^\s*\d+ failing",
|
|
147
|
+
r"^npm (ERR!|WARN)", # npm output
|
|
148
|
+
r"^error\[E\d+\]:", # Rust errors
|
|
149
|
+
r"^\[[\d:]+\]", # Timestamped logs
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _extract_text_content(content: Any) -> str:
|
|
154
|
+
"""
|
|
155
|
+
Extract text from various content formats.
|
|
156
|
+
|
|
157
|
+
Content can be:
|
|
158
|
+
- A plain string
|
|
159
|
+
- A list of content blocks: [{"type": "text", "text": "..."}, ...]
|
|
160
|
+
- A dict with a "text" key
|
|
161
|
+
"""
|
|
162
|
+
if isinstance(content, str):
|
|
163
|
+
return content
|
|
164
|
+
if isinstance(content, list):
|
|
165
|
+
text_parts = []
|
|
166
|
+
for item in content:
|
|
167
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
168
|
+
text_parts.append(item.get("text", ""))
|
|
169
|
+
elif isinstance(item, str):
|
|
170
|
+
text_parts.append(item)
|
|
171
|
+
return "\n".join(text_parts)
|
|
172
|
+
if isinstance(content, dict) and "text" in content:
|
|
173
|
+
return content.get("text", "")
|
|
174
|
+
return str(content) if content else ""
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# =============================================================================
|
|
178
|
+
# SMART FILTERING HELPERS
|
|
179
|
+
# =============================================================================
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _is_tool_json(content: str) -> bool:
|
|
183
|
+
"""Detect stringified tool_use input JSON that has no semantic value."""
|
|
184
|
+
stripped = content.strip()
|
|
185
|
+
for pattern in TOOL_JSON_PATTERNS:
|
|
186
|
+
if re.match(pattern, stripped):
|
|
187
|
+
return True
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _is_acknowledgment(content: str) -> bool:
|
|
192
|
+
"""Check if content is a pure acknowledgment with no semantic value."""
|
|
193
|
+
# Normalize: lowercase, strip, remove trailing punctuation
|
|
194
|
+
normalized = content.strip().lower().rstrip(".,!?")
|
|
195
|
+
return normalized in ACKNOWLEDGMENTS
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _is_transitional(content: str) -> bool:
|
|
199
|
+
"""Check if content is transitional filler text.
|
|
200
|
+
|
|
201
|
+
Only considers content transitional if it's short AND matches a pattern.
|
|
202
|
+
Longer messages that start with transitional phrases but contain substance are kept.
|
|
203
|
+
"""
|
|
204
|
+
stripped = content.strip()
|
|
205
|
+
|
|
206
|
+
# Long messages are not transitional even if they start with filler
|
|
207
|
+
if len(stripped) > MAX_TRANSITIONAL_LENGTH:
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
for pattern in TRANSITIONAL_PATTERNS:
|
|
211
|
+
if re.match(pattern, stripped, re.IGNORECASE):
|
|
212
|
+
return True
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _has_high_value_signal(content: str) -> bool:
|
|
217
|
+
"""Check if content has high-value patterns worth keeping regardless of length."""
|
|
218
|
+
for pattern in HIGH_VALUE_PATTERNS:
|
|
219
|
+
if re.search(pattern, content, re.IGNORECASE):
|
|
220
|
+
return True
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _should_keep_user_message(content: str) -> bool:
|
|
225
|
+
"""
|
|
226
|
+
Smart filtering for user messages.
|
|
227
|
+
|
|
228
|
+
Keep if:
|
|
229
|
+
- Is a question (ends with ?)
|
|
230
|
+
- Has high-value signals (error, bug, fix, etc.)
|
|
231
|
+
- Meets minimum length threshold
|
|
232
|
+
|
|
233
|
+
Skip if:
|
|
234
|
+
- Empty
|
|
235
|
+
- Pure acknowledgment
|
|
236
|
+
- Too short without high-value signals
|
|
237
|
+
"""
|
|
238
|
+
stripped = content.strip()
|
|
239
|
+
|
|
240
|
+
# Always skip empty
|
|
241
|
+
if not stripped:
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
# Always skip pure acknowledgments
|
|
245
|
+
if _is_acknowledgment(stripped):
|
|
246
|
+
return False
|
|
247
|
+
|
|
248
|
+
# Always keep questions
|
|
249
|
+
if stripped.endswith("?"):
|
|
250
|
+
return True
|
|
251
|
+
|
|
252
|
+
# Keep if has high-value signals
|
|
253
|
+
if _has_high_value_signal(stripped):
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
# Otherwise, apply length threshold
|
|
257
|
+
min_len = MIN_LENGTH_THRESHOLDS.get("user_message", DEFAULT_MIN_LENGTH)
|
|
258
|
+
return len(stripped) >= min_len
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _should_keep_assistant_text(content: str) -> bool:
|
|
262
|
+
"""
|
|
263
|
+
Smart filtering for assistant text.
|
|
264
|
+
|
|
265
|
+
Skip if:
|
|
266
|
+
- Tool JSON garbage
|
|
267
|
+
- Transitional filler
|
|
268
|
+
- Too short
|
|
269
|
+
|
|
270
|
+
Keep if:
|
|
271
|
+
- Has code blocks
|
|
272
|
+
- Has high-value signals
|
|
273
|
+
- Meets minimum length threshold
|
|
274
|
+
"""
|
|
275
|
+
stripped = content.strip()
|
|
276
|
+
|
|
277
|
+
# Always skip empty
|
|
278
|
+
if not stripped:
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
# Skip tool JSON garbage
|
|
282
|
+
if _is_tool_json(stripped):
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
# Keep if has code blocks (valuable) — check before transitional filter
|
|
286
|
+
# because "Here's the code: ```python..." starts transitional but has real code
|
|
287
|
+
if "```" in stripped:
|
|
288
|
+
return True
|
|
289
|
+
|
|
290
|
+
# Keep if has high-value signals
|
|
291
|
+
if _has_high_value_signal(stripped):
|
|
292
|
+
return True
|
|
293
|
+
|
|
294
|
+
# Skip transitional filler (after code/signal checks)
|
|
295
|
+
if _is_transitional(stripped):
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
# Otherwise, apply length threshold
|
|
299
|
+
min_len = MIN_LENGTH_THRESHOLDS.get("assistant_text", DEFAULT_MIN_LENGTH)
|
|
300
|
+
return len(stripped) >= min_len
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def classify_content(entry: dict) -> ClassifiedContent | None:
|
|
304
|
+
"""
|
|
305
|
+
Classify a JSONL entry by content type and value.
|
|
306
|
+
|
|
307
|
+
Uses smart filtering to maximize search quality:
|
|
308
|
+
- Preserves questions regardless of length
|
|
309
|
+
- Filters tool JSON garbage
|
|
310
|
+
- Filters pure acknowledgments
|
|
311
|
+
- Content-type-aware thresholds
|
|
312
|
+
|
|
313
|
+
Returns None for entries that should be skipped entirely.
|
|
314
|
+
"""
|
|
315
|
+
entry_type = entry.get("type", "")
|
|
316
|
+
|
|
317
|
+
# Skip noise types entirely
|
|
318
|
+
if entry_type in ("progress", "queue-operation"):
|
|
319
|
+
return None
|
|
320
|
+
|
|
321
|
+
if entry_type == "user":
|
|
322
|
+
raw_content = entry.get("message", {}).get("content", "")
|
|
323
|
+
content = _extract_text_content(raw_content)
|
|
324
|
+
|
|
325
|
+
# Smart filtering for user messages
|
|
326
|
+
if not _should_keep_user_message(content):
|
|
327
|
+
return None
|
|
328
|
+
|
|
329
|
+
# Check if it's a system prompt (first message, very long)
|
|
330
|
+
if len(content) > 2000:
|
|
331
|
+
return ClassifiedContent(
|
|
332
|
+
content=content,
|
|
333
|
+
content_type=ContentType.USER_MESSAGE,
|
|
334
|
+
value=ContentValue.MEDIUM, # System prompts are deduplicated elsewhere
|
|
335
|
+
metadata={"is_system_prompt": True},
|
|
336
|
+
)
|
|
337
|
+
return ClassifiedContent(
|
|
338
|
+
content=content,
|
|
339
|
+
content_type=ContentType.USER_MESSAGE,
|
|
340
|
+
value=ContentValue.HIGH,
|
|
341
|
+
metadata={},
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if entry_type == "assistant":
|
|
345
|
+
message = entry.get("message", {})
|
|
346
|
+
content_blocks = message.get("content", [])
|
|
347
|
+
|
|
348
|
+
# Process each content block
|
|
349
|
+
results = []
|
|
350
|
+
for block in content_blocks:
|
|
351
|
+
if isinstance(block, dict):
|
|
352
|
+
block_type = block.get("type", "")
|
|
353
|
+
|
|
354
|
+
if block_type == "text":
|
|
355
|
+
text = block.get("text", "")
|
|
356
|
+
# Smart filtering for assistant text
|
|
357
|
+
if not _should_keep_assistant_text(text):
|
|
358
|
+
continue
|
|
359
|
+
classified = _classify_text(text)
|
|
360
|
+
results.append(classified)
|
|
361
|
+
|
|
362
|
+
elif block_type == "tool_use":
|
|
363
|
+
# Skip tool_use - these are just API calls, not useful content
|
|
364
|
+
# The tool_result contains the actual content
|
|
365
|
+
continue
|
|
366
|
+
|
|
367
|
+
elif block_type == "tool_result":
|
|
368
|
+
# Tool results need careful classification
|
|
369
|
+
result_content = _extract_text_content(block.get("content", ""))
|
|
370
|
+
# Skip tool JSON and short results
|
|
371
|
+
if _is_tool_json(result_content):
|
|
372
|
+
continue
|
|
373
|
+
min_len = MIN_LENGTH_THRESHOLDS.get("tool_result", DEFAULT_MIN_LENGTH)
|
|
374
|
+
if len(result_content.strip()) < min_len:
|
|
375
|
+
continue
|
|
376
|
+
classified = _classify_tool_result(result_content, block)
|
|
377
|
+
results.append(classified)
|
|
378
|
+
|
|
379
|
+
# Return the highest-value content from this entry
|
|
380
|
+
# Priority: HIGH > MEDIUM > LOW
|
|
381
|
+
if results:
|
|
382
|
+
return min(results, key=lambda x: list(ContentValue).index(x.value))
|
|
383
|
+
|
|
384
|
+
# WhatsApp messages
|
|
385
|
+
if entry_type == "whatsapp_message":
|
|
386
|
+
content = entry.get("content", "")
|
|
387
|
+
role = entry.get("role", "contact")
|
|
388
|
+
ct = ContentType.USER_MESSAGE if role == "user" else ContentType.ASSISTANT_TEXT
|
|
389
|
+
if not content or len(content.strip()) < 10:
|
|
390
|
+
return None
|
|
391
|
+
return ClassifiedContent(
|
|
392
|
+
content=content,
|
|
393
|
+
content_type=ct,
|
|
394
|
+
value=ContentValue.HIGH if role == "user" else ContentValue.MEDIUM,
|
|
395
|
+
metadata={"source": "whatsapp"},
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _classify_text(text: str) -> ClassifiedContent:
|
|
402
|
+
"""Classify assistant text content."""
|
|
403
|
+
# Check for stack traces
|
|
404
|
+
for pattern in STACK_TRACE_PATTERNS:
|
|
405
|
+
if re.search(pattern, text, re.MULTILINE):
|
|
406
|
+
return ClassifiedContent(
|
|
407
|
+
content=text,
|
|
408
|
+
content_type=ContentType.STACK_TRACE,
|
|
409
|
+
value=ContentValue.HIGH,
|
|
410
|
+
metadata={},
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Check for code blocks (AI-generated code)
|
|
414
|
+
if "```" in text:
|
|
415
|
+
return ClassifiedContent(content=text, content_type=ContentType.AI_CODE, value=ContentValue.HIGH, metadata={})
|
|
416
|
+
|
|
417
|
+
return ClassifiedContent(
|
|
418
|
+
content=text,
|
|
419
|
+
content_type=ContentType.ASSISTANT_TEXT,
|
|
420
|
+
value=ContentValue.MEDIUM,
|
|
421
|
+
metadata={},
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def _classify_tool_result(content: str, block: dict) -> ClassifiedContent:
|
|
426
|
+
"""Classify tool result content."""
|
|
427
|
+
# Check for stack traces in tool output
|
|
428
|
+
for pattern in STACK_TRACE_PATTERNS:
|
|
429
|
+
if re.search(pattern, content, re.MULTILINE):
|
|
430
|
+
return ClassifiedContent(
|
|
431
|
+
content=content,
|
|
432
|
+
content_type=ContentType.STACK_TRACE,
|
|
433
|
+
value=ContentValue.HIGH,
|
|
434
|
+
metadata={},
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Check for build logs
|
|
438
|
+
for pattern in BUILD_LOG_PATTERNS:
|
|
439
|
+
if re.search(pattern, content, re.MULTILINE):
|
|
440
|
+
return ClassifiedContent(
|
|
441
|
+
content=content,
|
|
442
|
+
content_type=ContentType.BUILD_LOG,
|
|
443
|
+
value=ContentValue.LOW,
|
|
444
|
+
metadata={"action": "summarize"},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Git diff detection
|
|
448
|
+
if content.startswith("diff --git") or "@@" in content[:500]:
|
|
449
|
+
return ClassifiedContent(
|
|
450
|
+
content=content,
|
|
451
|
+
content_type=ContentType.GIT_DIFF,
|
|
452
|
+
value=ContentValue.MEDIUM,
|
|
453
|
+
metadata={},
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
# Directory listing detection
|
|
457
|
+
if content.count("\n") > 10 and all(
|
|
458
|
+
line.strip().endswith(("/", ".ts", ".js", ".py", ".json", ".md"))
|
|
459
|
+
for line in content.split("\n")[:10]
|
|
460
|
+
if line.strip()
|
|
461
|
+
):
|
|
462
|
+
return ClassifiedContent(
|
|
463
|
+
content=content,
|
|
464
|
+
content_type=ContentType.DIRECTORY_LISTING,
|
|
465
|
+
value=ContentValue.LOW,
|
|
466
|
+
metadata={"action": "structure_only"},
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Default: file read or general output
|
|
470
|
+
return ClassifiedContent(
|
|
471
|
+
content=content, content_type=ContentType.FILE_READ, value=ContentValue.MEDIUM, metadata={}
|
|
472
|
+
)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Cluster-based sampling for representative message selection."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Optional, Protocol, TypeVar
|
|
4
|
+
|
|
5
|
+
T = TypeVar("T")
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class HasText(Protocol):
|
|
9
|
+
"""Protocol for objects with .text attribute."""
|
|
10
|
+
|
|
11
|
+
text: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def cluster_sample_messages(
|
|
15
|
+
messages: List[T], # Must have .text attribute
|
|
16
|
+
embeddings: list[list[float]],
|
|
17
|
+
k: Optional[int] = None,
|
|
18
|
+
samples_per_cluster: int = 3,
|
|
19
|
+
max_total: int = 250,
|
|
20
|
+
) -> List[T]:
|
|
21
|
+
"""
|
|
22
|
+
Sample representative messages via K-means clustering on embeddings.
|
|
23
|
+
|
|
24
|
+
Selects messages nearest to each cluster centroid for diversity.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
messages: Messages (same order as embeddings)
|
|
28
|
+
embeddings: Corresponding embedding vectors
|
|
29
|
+
k: Number of clusters (default: min(8, len(messages)//10))
|
|
30
|
+
samples_per_cluster: Messages to pick per cluster
|
|
31
|
+
max_total: Cap total samples
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Sampled items (messages or text wrappers) for LLM prompt.
|
|
35
|
+
Input items must have .text attribute.
|
|
36
|
+
"""
|
|
37
|
+
import numpy as np
|
|
38
|
+
from sklearn.cluster import KMeans
|
|
39
|
+
|
|
40
|
+
n = len(messages)
|
|
41
|
+
if n == 0 or n != len(embeddings):
|
|
42
|
+
return messages[:max_total] if messages else []
|
|
43
|
+
|
|
44
|
+
if k is None:
|
|
45
|
+
k = min(8, max(2, n // 10))
|
|
46
|
+
k = min(k, n)
|
|
47
|
+
|
|
48
|
+
X = np.array(embeddings, dtype=np.float32)
|
|
49
|
+
kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
|
|
50
|
+
labels = kmeans.fit_predict(X)
|
|
51
|
+
centroids = kmeans.cluster_centers_
|
|
52
|
+
|
|
53
|
+
# For each cluster, get indices of points nearest to centroid
|
|
54
|
+
sampled_indices = []
|
|
55
|
+
for c in range(k):
|
|
56
|
+
mask = labels == c
|
|
57
|
+
indices = np.where(mask)[0]
|
|
58
|
+
if len(indices) == 0:
|
|
59
|
+
continue
|
|
60
|
+
# Distances to centroid
|
|
61
|
+
dists = np.linalg.norm(X[indices] - centroids[c], axis=1)
|
|
62
|
+
# Take closest samples_per_cluster
|
|
63
|
+
closest = indices[np.argsort(dists)[:samples_per_cluster]]
|
|
64
|
+
sampled_indices.extend(closest.tolist())
|
|
65
|
+
|
|
66
|
+
# Deduplicate and cap
|
|
67
|
+
seen = set()
|
|
68
|
+
result = []
|
|
69
|
+
for i in sampled_indices:
|
|
70
|
+
if i not in seen and len(result) < max_total:
|
|
71
|
+
seen.add(i)
|
|
72
|
+
result.append(messages[i])
|
|
73
|
+
return result
|