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,422 @@
1
+ """Stage 3: Chunk content using AST-aware splitting for code.
2
+
3
+ Content-type-aware chunking with different minimum sizes:
4
+ - user_message: 15 chars (short questions can be valuable)
5
+ - assistant_text: 50 chars (technical explanations can be concise)
6
+ - ai_code: 30 chars (code can be short but meaningful)
7
+ - stack_trace: 0 (always keep)
8
+ - tool outputs: 50 chars
9
+ """
10
+
11
+ import logging
12
+ import re
13
+ from dataclasses import dataclass
14
+ from typing import Any
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ # tree-sitter for AST parsing
19
+ try:
20
+ import tree_sitter_languages
21
+
22
+ HAS_TREE_SITTER = True
23
+ except ImportError:
24
+ HAS_TREE_SITTER = False
25
+
26
+ from .classify import ClassifiedContent, ContentType, ContentValue
27
+
28
+
29
+ @dataclass
30
+ class Chunk:
31
+ """A chunk of content ready for embedding."""
32
+
33
+ content: str
34
+ content_type: ContentType
35
+ value: ContentValue
36
+ metadata: dict[str, Any]
37
+ char_count: int
38
+
39
+
40
+ # Target chunk size in characters (research suggests ~500 tokens ≈ 2000 chars)
41
+ TARGET_CHUNK_SIZE = 2000
42
+ MAX_CHUNK_SIZE = 4000
43
+
44
+ # Content-type-aware minimum chunk sizes
45
+ # Note: Most filtering happens in classify.py, but this is a safety net
46
+ MIN_CHUNK_SIZES = {
47
+ ContentType.USER_MESSAGE: 15, # Short questions can be valuable
48
+ ContentType.ASSISTANT_TEXT: 80, # Explanations need context
49
+ ContentType.AI_CODE: 30, # Code can be short but meaningful
50
+ ContentType.STACK_TRACE: 0, # Always keep stack traces
51
+ ContentType.FILE_READ: 50, # Tool outputs need context
52
+ ContentType.BUILD_LOG: 50,
53
+ ContentType.DIRECTORY_LISTING: 50,
54
+ ContentType.GIT_DIFF: 50,
55
+ ContentType.CONFIG: 30,
56
+ }
57
+ DEFAULT_MIN_CHUNK_SIZE = 50
58
+
59
+
60
+ def _get_min_chunk_size(content_type: ContentType) -> int:
61
+ """Get minimum chunk size for a content type."""
62
+ return MIN_CHUNK_SIZES.get(content_type, DEFAULT_MIN_CHUNK_SIZE)
63
+
64
+
65
+ def chunk_content(classified: ClassifiedContent) -> list[Chunk]:
66
+ """
67
+ Chunk classified content appropriately based on type.
68
+
69
+ - Code: AST-aware chunking with tree-sitter
70
+ - Stack traces: Never split (preserve exact)
71
+ - Conversation: Turn-based with overlap
72
+ - Large tool outputs: Observation masking or summarization marker
73
+
74
+ Uses content-type-aware minimum sizes to preserve short but valuable content.
75
+ """
76
+ content = classified.content
77
+ content_type = classified.content_type
78
+
79
+ # Get content-type-aware minimum size
80
+ min_size = _get_min_chunk_size(content_type)
81
+
82
+ # Skip content that's too short for its type
83
+ if len(content.strip()) < min_size:
84
+ return []
85
+
86
+ # Never split stack traces
87
+ if content_type == ContentType.STACK_TRACE:
88
+ return [
89
+ Chunk(
90
+ content=content,
91
+ content_type=content_type,
92
+ value=classified.value,
93
+ metadata=classified.metadata,
94
+ char_count=len(content),
95
+ )
96
+ ]
97
+
98
+ # AI-generated code: AST-aware chunking
99
+ if content_type == ContentType.AI_CODE:
100
+ return _chunk_code(classified)
101
+
102
+ # Large tool outputs: Use observation masking
103
+ if content_type in (
104
+ ContentType.FILE_READ,
105
+ ContentType.BUILD_LOG,
106
+ ContentType.DIRECTORY_LISTING,
107
+ ):
108
+ if len(content) > MAX_CHUNK_SIZE:
109
+ return _mask_large_output(classified)
110
+
111
+ # Default: simple character-based chunking with overlap
112
+ return _chunk_text(classified)
113
+
114
+
115
+ def _chunk_code(classified: ClassifiedContent) -> list[Chunk]:
116
+ """
117
+ AST-aware chunking for code content.
118
+
119
+ Uses tree-sitter to identify semantic boundaries (functions, classes).
120
+ Falls back to line-based chunking if tree-sitter unavailable.
121
+ """
122
+ content = classified.content
123
+ chunks = []
124
+
125
+ # Extract code blocks from markdown
126
+ code_blocks = _extract_code_blocks(content)
127
+
128
+ for lang, code in code_blocks:
129
+ if HAS_TREE_SITTER and lang:
130
+ # Try AST-based chunking
131
+ ast_chunks = _ast_chunk(code, lang)
132
+ if ast_chunks:
133
+ for chunk_text in ast_chunks:
134
+ chunks.append(
135
+ Chunk(
136
+ content=chunk_text,
137
+ content_type=classified.content_type,
138
+ value=classified.value,
139
+ metadata={**classified.metadata, "language": lang},
140
+ char_count=len(chunk_text),
141
+ )
142
+ )
143
+ continue
144
+
145
+ # Fallback: line-based chunking
146
+ line_chunks = _line_based_chunk(code)
147
+ for chunk_text in line_chunks:
148
+ chunks.append(
149
+ Chunk(
150
+ content=chunk_text,
151
+ content_type=classified.content_type,
152
+ value=classified.value,
153
+ metadata={**classified.metadata, "language": lang or "unknown"},
154
+ char_count=len(chunk_text),
155
+ )
156
+ )
157
+
158
+ # Also include any text outside code blocks
159
+ non_code = _extract_non_code(content)
160
+ if non_code.strip():
161
+ chunks.append(
162
+ Chunk(
163
+ content=non_code,
164
+ content_type=ContentType.ASSISTANT_TEXT,
165
+ value=ContentValue.MEDIUM,
166
+ metadata=classified.metadata,
167
+ char_count=len(non_code),
168
+ )
169
+ )
170
+
171
+ return (
172
+ chunks
173
+ if chunks
174
+ else [
175
+ Chunk(
176
+ content=content,
177
+ content_type=classified.content_type,
178
+ value=classified.value,
179
+ metadata=classified.metadata,
180
+ char_count=len(content),
181
+ )
182
+ ]
183
+ )
184
+
185
+
186
+ def _ast_chunk(code: str, language: str) -> list[str] | None:
187
+ """Use tree-sitter to chunk code at semantic boundaries."""
188
+ if not HAS_TREE_SITTER:
189
+ return None
190
+
191
+ try:
192
+ parser = tree_sitter_languages.get_parser(language)
193
+ tree = parser.parse(code.encode())
194
+
195
+ chunks = []
196
+ current_chunk = []
197
+ current_size = 0
198
+
199
+ def traverse(node):
200
+ nonlocal current_chunk, current_size
201
+
202
+ # Top-level definitions are natural chunk boundaries
203
+ if node.type in (
204
+ "function_definition",
205
+ "function_declaration",
206
+ "class_definition",
207
+ "class_declaration",
208
+ "method_definition",
209
+ "method_declaration",
210
+ "interface_declaration",
211
+ "type_alias_declaration",
212
+ ):
213
+ text = code[node.start_byte : node.end_byte]
214
+
215
+ # If adding this would exceed max, start new chunk
216
+ if current_size + len(text) > MAX_CHUNK_SIZE and current_chunk:
217
+ chunks.append("\n".join(current_chunk))
218
+ current_chunk = []
219
+ current_size = 0
220
+
221
+ current_chunk.append(text)
222
+ current_size += len(text)
223
+ return # Don't traverse children
224
+
225
+ # Recurse into children
226
+ for child in node.children:
227
+ traverse(child)
228
+
229
+ traverse(tree.root_node)
230
+
231
+ # Don't forget the last chunk
232
+ if current_chunk:
233
+ chunks.append("\n".join(current_chunk))
234
+
235
+ return chunks if chunks else None
236
+
237
+ except Exception as e:
238
+ logger.debug(f"AST parsing failed for {language}: {e}")
239
+ return None
240
+
241
+
242
+ def _line_based_chunk(text: str) -> list[str]:
243
+ """Simple line-based chunking with overlap."""
244
+ lines = text.split("\n")
245
+ chunks = []
246
+ current_chunk = []
247
+ current_size = 0
248
+
249
+ for line in lines:
250
+ line_size = len(line) + 1 # +1 for newline
251
+
252
+ if current_size + line_size > TARGET_CHUNK_SIZE and current_chunk:
253
+ chunks.append("\n".join(current_chunk))
254
+ # Keep last 2 lines for overlap
255
+ current_chunk = current_chunk[-2:] if len(current_chunk) > 2 else []
256
+ current_size = sum(len(l) + 1 for l in current_chunk)
257
+
258
+ current_chunk.append(line)
259
+ current_size += line_size
260
+
261
+ if current_chunk:
262
+ chunks.append("\n".join(current_chunk))
263
+
264
+ return chunks
265
+
266
+
267
+ def _split_at_sentences(text: str, target_size: int = TARGET_CHUNK_SIZE) -> list[str]:
268
+ """Split text at sentence boundaries, respecting target chunk size.
269
+
270
+ Falls back to line boundaries if no sentences found.
271
+ """
272
+ # Match sentence endings: period/question/exclamation followed by space or newline
273
+ sentence_pattern = re.compile(r"(?<=[.!?])\s+")
274
+ sentences = sentence_pattern.split(text)
275
+
276
+ if len(sentences) <= 1:
277
+ # No sentence boundaries found, fall back to line-based
278
+ return _line_based_chunk(text)
279
+
280
+ chunks = []
281
+ current = []
282
+ current_size = 0
283
+
284
+ for sent in sentences:
285
+ sent_size = len(sent) + 1 # +1 for space separator
286
+
287
+ if current_size + sent_size > target_size and current:
288
+ chunks.append(" ".join(current))
289
+ current = []
290
+ current_size = 0
291
+
292
+ current.append(sent)
293
+ current_size += sent_size
294
+
295
+ if current:
296
+ chunks.append(" ".join(current))
297
+
298
+ return chunks
299
+
300
+
301
+ def _chunk_text(classified: ClassifiedContent) -> list[Chunk]:
302
+ """Text chunking that respects paragraph and sentence boundaries."""
303
+ content = classified.content
304
+
305
+ if len(content) <= TARGET_CHUNK_SIZE:
306
+ return [
307
+ Chunk(
308
+ content=content,
309
+ content_type=classified.content_type,
310
+ value=classified.value,
311
+ metadata=classified.metadata,
312
+ char_count=len(content),
313
+ )
314
+ ]
315
+
316
+ # Chunk by paragraphs first
317
+ paragraphs = content.split("\n\n")
318
+ chunks = []
319
+ current_chunk = []
320
+ current_size = 0
321
+
322
+ for para in paragraphs:
323
+ para_size = len(para) + 2 # +2 for paragraph separator
324
+
325
+ # If a single paragraph exceeds target, split it at sentence boundaries
326
+ if para_size > TARGET_CHUNK_SIZE:
327
+ # Flush current chunk first
328
+ if current_chunk:
329
+ chunks.append(
330
+ Chunk(
331
+ content="\n\n".join(current_chunk),
332
+ content_type=classified.content_type,
333
+ value=classified.value,
334
+ metadata=classified.metadata,
335
+ char_count=current_size,
336
+ )
337
+ )
338
+ current_chunk = []
339
+ current_size = 0
340
+
341
+ # Split the oversized paragraph at sentence boundaries
342
+ sub_chunks = _split_at_sentences(para)
343
+ for sub in sub_chunks:
344
+ chunks.append(
345
+ Chunk(
346
+ content=sub,
347
+ content_type=classified.content_type,
348
+ value=classified.value,
349
+ metadata=classified.metadata,
350
+ char_count=len(sub),
351
+ )
352
+ )
353
+ continue
354
+
355
+ if current_size + para_size > TARGET_CHUNK_SIZE and current_chunk:
356
+ chunks.append(
357
+ Chunk(
358
+ content="\n\n".join(current_chunk),
359
+ content_type=classified.content_type,
360
+ value=classified.value,
361
+ metadata=classified.metadata,
362
+ char_count=current_size,
363
+ )
364
+ )
365
+ current_chunk = []
366
+ current_size = 0
367
+
368
+ current_chunk.append(para)
369
+ current_size += para_size
370
+
371
+ if current_chunk:
372
+ chunks.append(
373
+ Chunk(
374
+ content="\n\n".join(current_chunk),
375
+ content_type=classified.content_type,
376
+ value=classified.value,
377
+ metadata=classified.metadata,
378
+ char_count=current_size,
379
+ )
380
+ )
381
+
382
+ return chunks
383
+
384
+
385
+ def _mask_large_output(classified: ClassifiedContent) -> list[Chunk]:
386
+ """
387
+ Apply observation masking to large tool outputs.
388
+
389
+ Research shows this often performs as well as LLM summarization.
390
+ """
391
+ content = classified.content
392
+ line_count = content.count("\n") + 1
393
+
394
+ # Create a masked summary
395
+ first_lines = "\n".join(content.split("\n")[:5])
396
+ last_lines = "\n".join(content.split("\n")[-3:])
397
+
398
+ elided_count = max(0, line_count - 8)
399
+ masked = f"{first_lines}\n\n[... {elided_count} lines elided ...]\n\n{last_lines}"
400
+
401
+ return [
402
+ Chunk(
403
+ content=masked,
404
+ content_type=classified.content_type,
405
+ value=ContentValue.LOW,
406
+ metadata={**classified.metadata, "masked": True, "original_lines": line_count},
407
+ char_count=len(masked),
408
+ )
409
+ ]
410
+
411
+
412
+ def _extract_code_blocks(text: str) -> list[tuple[str | None, str]]:
413
+ """Extract code blocks from markdown-formatted text."""
414
+ pattern = r"```(\w+)?\n(.*?)```"
415
+ matches = re.findall(pattern, text, re.DOTALL)
416
+
417
+ return [(lang or None, code.strip()) for lang, code in matches]
418
+
419
+
420
+ def _extract_non_code(text: str) -> str:
421
+ """Extract text that's not inside code blocks."""
422
+ return re.sub(r"```\w*\n.*?```", "", text, flags=re.DOTALL).strip()