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.
Files changed (53) hide show
  1. brainlayer/__init__.py +3 -0
  2. brainlayer/cli/__init__.py +1545 -0
  3. brainlayer/cli/wizard.py +132 -0
  4. brainlayer/cli_new.py +151 -0
  5. brainlayer/client.py +164 -0
  6. brainlayer/clustering.py +736 -0
  7. brainlayer/daemon.py +1105 -0
  8. brainlayer/dashboard/README.md +129 -0
  9. brainlayer/dashboard/__init__.py +5 -0
  10. brainlayer/dashboard/app.py +151 -0
  11. brainlayer/dashboard/search.py +229 -0
  12. brainlayer/dashboard/views.py +230 -0
  13. brainlayer/embeddings.py +131 -0
  14. brainlayer/engine.py +550 -0
  15. brainlayer/index_new.py +87 -0
  16. brainlayer/mcp/__init__.py +1558 -0
  17. brainlayer/migrate.py +205 -0
  18. brainlayer/paths.py +43 -0
  19. brainlayer/pipeline/__init__.py +47 -0
  20. brainlayer/pipeline/analyze_communication.py +508 -0
  21. brainlayer/pipeline/brain_graph.py +567 -0
  22. brainlayer/pipeline/chat_tags.py +63 -0
  23. brainlayer/pipeline/chunk.py +422 -0
  24. brainlayer/pipeline/classify.py +472 -0
  25. brainlayer/pipeline/cluster_sampling.py +73 -0
  26. brainlayer/pipeline/enrichment.py +810 -0
  27. brainlayer/pipeline/extract.py +66 -0
  28. brainlayer/pipeline/extract_claude_desktop.py +149 -0
  29. brainlayer/pipeline/extract_corrections.py +231 -0
  30. brainlayer/pipeline/extract_markdown.py +195 -0
  31. brainlayer/pipeline/extract_whatsapp.py +227 -0
  32. brainlayer/pipeline/git_overlay.py +301 -0
  33. brainlayer/pipeline/longitudinal_analyzer.py +568 -0
  34. brainlayer/pipeline/obsidian_export.py +455 -0
  35. brainlayer/pipeline/operation_grouping.py +486 -0
  36. brainlayer/pipeline/plan_linking.py +313 -0
  37. brainlayer/pipeline/sanitize.py +549 -0
  38. brainlayer/pipeline/semantic_style.py +574 -0
  39. brainlayer/pipeline/session_enrichment.py +472 -0
  40. brainlayer/pipeline/style_embed.py +67 -0
  41. brainlayer/pipeline/style_index.py +139 -0
  42. brainlayer/pipeline/temporal_chains.py +203 -0
  43. brainlayer/pipeline/time_batcher.py +248 -0
  44. brainlayer/pipeline/unified_timeline.py +569 -0
  45. brainlayer/storage.py +66 -0
  46. brainlayer/store.py +155 -0
  47. brainlayer/taxonomy.json +80 -0
  48. brainlayer/vector_store.py +1891 -0
  49. brainlayer-1.0.0.dist-info/METADATA +313 -0
  50. brainlayer-1.0.0.dist-info/RECORD +53 -0
  51. brainlayer-1.0.0.dist-info/WHEEL +4 -0
  52. brainlayer-1.0.0.dist-info/entry_points.txt +4 -0
  53. 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