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,508 @@
|
|
|
1
|
+
"""Analyze communication patterns to generate personalized rules.
|
|
2
|
+
|
|
3
|
+
Extracts patterns from:
|
|
4
|
+
1. User's writing style (from WhatsApp, Claude chats)
|
|
5
|
+
2. Claude's response patterns that work well for the user
|
|
6
|
+
3. Common clarifying questions and their answers
|
|
7
|
+
4. Semantic topic-based style analysis (optional, requires ML dependencies)
|
|
8
|
+
|
|
9
|
+
Generates rules for desktop apps to help with text interactions.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import re
|
|
14
|
+
from collections import Counter
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any, Optional
|
|
17
|
+
|
|
18
|
+
# Optional semantic analysis (requires sentence-transformers, sklearn)
|
|
19
|
+
try:
|
|
20
|
+
from .semantic_style import SemanticStyleAnalysis, SemanticStyleAnalyzer
|
|
21
|
+
|
|
22
|
+
HAS_SEMANTIC = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
HAS_SEMANTIC = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CommunicationAnalyzer:
|
|
28
|
+
"""Analyze communication patterns and generate rules."""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.user_messages = []
|
|
32
|
+
self.assistant_messages = []
|
|
33
|
+
self.clarifying_questions = []
|
|
34
|
+
|
|
35
|
+
def add_whatsapp_messages(self, messages: list[dict]):
|
|
36
|
+
"""Add WhatsApp messages to analysis."""
|
|
37
|
+
for msg in messages:
|
|
38
|
+
if msg["is_from_me"]:
|
|
39
|
+
self.user_messages.append(
|
|
40
|
+
{
|
|
41
|
+
"text": msg["text"],
|
|
42
|
+
"source": "whatsapp",
|
|
43
|
+
"timestamp": msg.get("timestamp"),
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def add_claude_conversations(self, conversations: list[dict]):
|
|
48
|
+
"""Add Claude conversations to analysis."""
|
|
49
|
+
for conv in conversations:
|
|
50
|
+
for msg in conv.get("messages", []):
|
|
51
|
+
if msg.get("role") == "user":
|
|
52
|
+
self.user_messages.append(
|
|
53
|
+
{
|
|
54
|
+
"text": msg.get("content", ""),
|
|
55
|
+
"source": "claude",
|
|
56
|
+
"timestamp": msg.get("timestamp"),
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
elif msg.get("role") == "assistant":
|
|
60
|
+
self.assistant_messages.append(
|
|
61
|
+
{
|
|
62
|
+
"text": msg.get("content", ""),
|
|
63
|
+
"source": "claude",
|
|
64
|
+
"timestamp": msg.get("timestamp"),
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Detect clarifying questions
|
|
69
|
+
if self._is_clarifying_question(msg.get("content", "")):
|
|
70
|
+
self.clarifying_questions.append(msg.get("content", ""))
|
|
71
|
+
|
|
72
|
+
def _is_clarifying_question(self, text: str) -> bool:
|
|
73
|
+
"""Detect if a message is a clarifying question."""
|
|
74
|
+
question_markers = [
|
|
75
|
+
"could you clarify",
|
|
76
|
+
"what do you mean",
|
|
77
|
+
"can you provide more details",
|
|
78
|
+
"would you like me to",
|
|
79
|
+
"should i",
|
|
80
|
+
"do you want",
|
|
81
|
+
"which",
|
|
82
|
+
"how would you like",
|
|
83
|
+
]
|
|
84
|
+
text_lower = text.lower()
|
|
85
|
+
return any(marker in text_lower for marker in question_markers) and "?" in text
|
|
86
|
+
|
|
87
|
+
def analyze_writing_style(self) -> dict[str, Any]:
|
|
88
|
+
"""Analyze user's writing style."""
|
|
89
|
+
if not self.user_messages:
|
|
90
|
+
return {}
|
|
91
|
+
|
|
92
|
+
texts = [msg["text"] for msg in self.user_messages]
|
|
93
|
+
|
|
94
|
+
# Length analysis
|
|
95
|
+
lengths = [len(text) for text in texts]
|
|
96
|
+
avg_length = sum(lengths) / len(lengths)
|
|
97
|
+
|
|
98
|
+
# Sentence structure
|
|
99
|
+
sentences_per_message = [text.count(".") + text.count("!") + text.count("?") for text in texts]
|
|
100
|
+
avg_sentences = sum(sentences_per_message) / len(sentences_per_message)
|
|
101
|
+
|
|
102
|
+
# Formality analysis - comprehensive markers
|
|
103
|
+
# English informal markers
|
|
104
|
+
informal_markers_en = [
|
|
105
|
+
"lol",
|
|
106
|
+
"haha",
|
|
107
|
+
"hahaha",
|
|
108
|
+
"btw",
|
|
109
|
+
"idk",
|
|
110
|
+
"tbh",
|
|
111
|
+
"ngl",
|
|
112
|
+
"omg",
|
|
113
|
+
"wtf",
|
|
114
|
+
"gonna",
|
|
115
|
+
"wanna",
|
|
116
|
+
"gotta",
|
|
117
|
+
"kinda",
|
|
118
|
+
"sorta",
|
|
119
|
+
"dunno",
|
|
120
|
+
"lemme",
|
|
121
|
+
"yeah",
|
|
122
|
+
"yep",
|
|
123
|
+
"nope",
|
|
124
|
+
"nah",
|
|
125
|
+
"yup",
|
|
126
|
+
"ok",
|
|
127
|
+
"okay",
|
|
128
|
+
"k",
|
|
129
|
+
"cool",
|
|
130
|
+
"awesome",
|
|
131
|
+
"dude",
|
|
132
|
+
"guys",
|
|
133
|
+
"hey",
|
|
134
|
+
"yo",
|
|
135
|
+
"sup",
|
|
136
|
+
"lmao",
|
|
137
|
+
"lmfao",
|
|
138
|
+
"rofl",
|
|
139
|
+
"brb",
|
|
140
|
+
"ttyl",
|
|
141
|
+
"imo",
|
|
142
|
+
"imho",
|
|
143
|
+
"fyi",
|
|
144
|
+
"thx",
|
|
145
|
+
"ty",
|
|
146
|
+
"np",
|
|
147
|
+
"pls",
|
|
148
|
+
"plz",
|
|
149
|
+
"rn",
|
|
150
|
+
"asap",
|
|
151
|
+
"afaik",
|
|
152
|
+
]
|
|
153
|
+
# Hebrew informal markers
|
|
154
|
+
informal_markers_he = [
|
|
155
|
+
"חח",
|
|
156
|
+
"חחח",
|
|
157
|
+
"חחחח",
|
|
158
|
+
"לול",
|
|
159
|
+
"אוקי",
|
|
160
|
+
"יאללה",
|
|
161
|
+
"סבבה",
|
|
162
|
+
"וואלה",
|
|
163
|
+
"אחלה",
|
|
164
|
+
"יופי",
|
|
165
|
+
"בסדר",
|
|
166
|
+
"טוב",
|
|
167
|
+
"נו",
|
|
168
|
+
"מה",
|
|
169
|
+
"כן",
|
|
170
|
+
"לא",
|
|
171
|
+
"אז",
|
|
172
|
+
"רגע",
|
|
173
|
+
"שניה",
|
|
174
|
+
"בקיצור",
|
|
175
|
+
]
|
|
176
|
+
informal_markers = informal_markers_en + informal_markers_he
|
|
177
|
+
|
|
178
|
+
# Count informal markers
|
|
179
|
+
informal_count = sum(1 for text in texts for marker in informal_markers if marker in text.lower())
|
|
180
|
+
|
|
181
|
+
# Also check for formal indicators (increase formality score)
|
|
182
|
+
formal_markers = [
|
|
183
|
+
"dear",
|
|
184
|
+
"sincerely",
|
|
185
|
+
"regards",
|
|
186
|
+
"respectfully",
|
|
187
|
+
"hereby",
|
|
188
|
+
"pursuant",
|
|
189
|
+
"kindly",
|
|
190
|
+
"please find",
|
|
191
|
+
"attached herewith",
|
|
192
|
+
"i am writing to",
|
|
193
|
+
"to whom it may concern",
|
|
194
|
+
]
|
|
195
|
+
formal_count = sum(1 for text in texts for marker in formal_markers if marker in text.lower())
|
|
196
|
+
|
|
197
|
+
# Check for casual style indicators (count messages with casual traits)
|
|
198
|
+
casual_messages = 0
|
|
199
|
+
for text in texts:
|
|
200
|
+
is_casual = False
|
|
201
|
+
# Short messages are casual
|
|
202
|
+
if len(text) < 30:
|
|
203
|
+
is_casual = True
|
|
204
|
+
# Messages without proper capitalization
|
|
205
|
+
if text and text[0].islower():
|
|
206
|
+
is_casual = True
|
|
207
|
+
# Messages ending without punctuation
|
|
208
|
+
if text and text[-1] not in ".!?":
|
|
209
|
+
is_casual = True
|
|
210
|
+
# Contractions
|
|
211
|
+
if any(c in text.lower() for c in ["don't", "won't", "can't", "i'm", "it's", "that's", "what's"]):
|
|
212
|
+
is_casual = True
|
|
213
|
+
if is_casual:
|
|
214
|
+
casual_messages += 1
|
|
215
|
+
|
|
216
|
+
# Calculate ratios (all 0-1 range now, clamped for consistency)
|
|
217
|
+
informal_marker_ratio = min(1.0, informal_count / max(len(texts), 1))
|
|
218
|
+
casual_style_ratio = casual_messages / max(len(texts), 1)
|
|
219
|
+
formal_marker_ratio = min(1.0, formal_count / max(len(texts), 1))
|
|
220
|
+
|
|
221
|
+
# Formality score: 0 = very casual, 1 = very formal
|
|
222
|
+
# Start at 0.5, subtract for informal/casual indicators, add for formal
|
|
223
|
+
# Tuned weights: casual=0.15, informal=0.1 (lighter touch to avoid extremes)
|
|
224
|
+
raw_score = 0.5 - (casual_style_ratio * 0.15) - (informal_marker_ratio * 0.1) + (formal_marker_ratio * 0.3)
|
|
225
|
+
formality_score = max(0.1, min(0.9, raw_score)) # Clamp to 0.1-0.9 range
|
|
226
|
+
|
|
227
|
+
# Punctuation patterns
|
|
228
|
+
exclamation_rate = sum(text.count("!") for text in texts) / len(texts)
|
|
229
|
+
question_rate = sum(text.count("?") for text in texts) / len(texts)
|
|
230
|
+
|
|
231
|
+
# Emoji usage
|
|
232
|
+
emoji_pattern = re.compile(
|
|
233
|
+
"["
|
|
234
|
+
"\U0001f600-\U0001f64f" # emoticons
|
|
235
|
+
"\U0001f300-\U0001f5ff" # symbols & pictographs
|
|
236
|
+
"\U0001f680-\U0001f6ff" # transport & map symbols
|
|
237
|
+
"\U0001f1e0-\U0001f1ff" # flags
|
|
238
|
+
"\U00002702-\U000027b0"
|
|
239
|
+
"\U000024c2-\U0001f251"
|
|
240
|
+
"]+",
|
|
241
|
+
flags=re.UNICODE,
|
|
242
|
+
)
|
|
243
|
+
emoji_count = sum(len(emoji_pattern.findall(text)) for text in texts)
|
|
244
|
+
emoji_rate = emoji_count / len(texts)
|
|
245
|
+
|
|
246
|
+
# Common phrases
|
|
247
|
+
all_text = " ".join(texts).lower()
|
|
248
|
+
words = re.findall(r"\b\w+\b", all_text)
|
|
249
|
+
word_freq = Counter(words)
|
|
250
|
+
common_words = [word for word, count in word_freq.most_common(20) if len(word) > 3]
|
|
251
|
+
|
|
252
|
+
# Greeting patterns
|
|
253
|
+
greetings = ["hi", "hello", "hey", "good morning", "good afternoon", "good evening"]
|
|
254
|
+
greeting_usage = sum(1 for text in texts for greeting in greetings if text.lower().startswith(greeting)) / len(
|
|
255
|
+
texts
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
"avg_message_length": avg_length,
|
|
260
|
+
"avg_sentences_per_message": avg_sentences,
|
|
261
|
+
"formality_score": formality_score, # 0 = very informal, 1 = very formal
|
|
262
|
+
"exclamation_rate": exclamation_rate,
|
|
263
|
+
"question_rate": question_rate,
|
|
264
|
+
"emoji_rate": emoji_rate,
|
|
265
|
+
"common_words": common_words,
|
|
266
|
+
"greeting_usage": greeting_usage,
|
|
267
|
+
"total_messages_analyzed": len(texts),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
def analyze_claude_response_patterns(self) -> dict[str, Any]:
|
|
271
|
+
"""Analyze Claude's response patterns that work well."""
|
|
272
|
+
if not self.assistant_messages:
|
|
273
|
+
return {}
|
|
274
|
+
|
|
275
|
+
texts = [msg["text"] for msg in self.assistant_messages]
|
|
276
|
+
|
|
277
|
+
# Response structure
|
|
278
|
+
uses_bullet_points = sum("•" in text or "- " in text for text in texts) / len(texts)
|
|
279
|
+
uses_code_blocks = sum("```" in text for text in texts) / len(texts)
|
|
280
|
+
uses_numbered_lists = sum(bool(re.search(r"\n\d+\.", text)) for text in texts) / len(texts)
|
|
281
|
+
|
|
282
|
+
# Tone analysis
|
|
283
|
+
encouraging_phrases = ["great", "excellent", "perfect", "nice work", "well done"]
|
|
284
|
+
encouraging_rate = sum(1 for text in texts for phrase in encouraging_phrases if phrase in text.lower()) / len(
|
|
285
|
+
texts
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Explanation style
|
|
289
|
+
uses_examples = sum("for example" in text.lower() or "e.g." in text.lower() for text in texts) / len(texts)
|
|
290
|
+
uses_analogies = sum("like" in text.lower() or "similar to" in text.lower() for text in texts) / len(texts)
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
"uses_bullet_points_rate": uses_bullet_points,
|
|
294
|
+
"uses_code_blocks_rate": uses_code_blocks,
|
|
295
|
+
"uses_numbered_lists_rate": uses_numbered_lists,
|
|
296
|
+
"encouraging_rate": encouraging_rate,
|
|
297
|
+
"uses_examples_rate": uses_examples,
|
|
298
|
+
"uses_analogies_rate": uses_analogies,
|
|
299
|
+
"total_responses_analyzed": len(texts),
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
def extract_common_clarifications(self) -> list[str]:
|
|
303
|
+
"""Extract common clarifying questions Claude asks."""
|
|
304
|
+
if not self.clarifying_questions:
|
|
305
|
+
return []
|
|
306
|
+
|
|
307
|
+
# Group similar questions
|
|
308
|
+
question_patterns = []
|
|
309
|
+
for q in self.clarifying_questions[:20]: # Top 20
|
|
310
|
+
# Extract the core question
|
|
311
|
+
core = re.sub(r"^(could you|can you|would you|should i)\s+", "", q.lower())
|
|
312
|
+
question_patterns.append(core)
|
|
313
|
+
|
|
314
|
+
return question_patterns
|
|
315
|
+
|
|
316
|
+
def generate_rules(self, output_path: Path):
|
|
317
|
+
"""Generate Cursor rules based on analysis."""
|
|
318
|
+
writing_style = self.analyze_writing_style()
|
|
319
|
+
response_patterns = self.analyze_claude_response_patterns()
|
|
320
|
+
clarifications = self.extract_common_clarifications()
|
|
321
|
+
|
|
322
|
+
rules = self._format_rules(writing_style, response_patterns, clarifications)
|
|
323
|
+
|
|
324
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
325
|
+
with open(output_path, "w") as f:
|
|
326
|
+
f.write(rules)
|
|
327
|
+
|
|
328
|
+
return rules
|
|
329
|
+
|
|
330
|
+
def _format_rules(self, writing_style: dict, response_patterns: dict, clarifications: list[str]) -> str:
|
|
331
|
+
"""Format analysis into Cursor rules."""
|
|
332
|
+
|
|
333
|
+
# Determine tone
|
|
334
|
+
if writing_style.get("formality_score", 0.5) > 0.7:
|
|
335
|
+
tone = "professional and formal"
|
|
336
|
+
elif writing_style.get("formality_score", 0.5) < 0.3:
|
|
337
|
+
tone = "casual and conversational"
|
|
338
|
+
else:
|
|
339
|
+
tone = "balanced between casual and professional"
|
|
340
|
+
|
|
341
|
+
# Determine verbosity
|
|
342
|
+
avg_length = writing_style.get("avg_message_length", 100)
|
|
343
|
+
if avg_length < 50:
|
|
344
|
+
verbosity = "concise and to-the-point"
|
|
345
|
+
elif avg_length > 200:
|
|
346
|
+
verbosity = "detailed and thorough"
|
|
347
|
+
else:
|
|
348
|
+
verbosity = "moderately detailed"
|
|
349
|
+
|
|
350
|
+
# Emoji preference
|
|
351
|
+
emoji_rate = writing_style.get("emoji_rate", 0)
|
|
352
|
+
emoji_pref = "Use emojis occasionally" if emoji_rate > 0.5 else "Avoid emojis unless requested"
|
|
353
|
+
|
|
354
|
+
# Response structure preference
|
|
355
|
+
structure_prefs = []
|
|
356
|
+
if response_patterns.get("uses_bullet_points_rate", 0) > 0.5:
|
|
357
|
+
structure_prefs.append("Use bullet points for lists")
|
|
358
|
+
if response_patterns.get("uses_numbered_lists_rate", 0) > 0.5:
|
|
359
|
+
structure_prefs.append("Use numbered lists for sequential steps")
|
|
360
|
+
if response_patterns.get("uses_examples_rate", 0) > 0.5:
|
|
361
|
+
structure_prefs.append("Provide concrete examples")
|
|
362
|
+
|
|
363
|
+
rules_content = f"""# Communication Style Rules
|
|
364
|
+
# Auto-generated from analysis of {writing_style.get("total_messages_analyzed", 0)} messages
|
|
365
|
+
|
|
366
|
+
## User's Writing Style
|
|
367
|
+
|
|
368
|
+
**Tone**: {tone}
|
|
369
|
+
**Verbosity**: {verbosity}
|
|
370
|
+
**Average message length**: {writing_style.get("avg_message_length", 0):.0f} characters
|
|
371
|
+
**Formality score**: {writing_style.get("formality_score", 0):.2f} (0=informal, 1=formal)
|
|
372
|
+
|
|
373
|
+
### Preferences
|
|
374
|
+
|
|
375
|
+
- {emoji_pref}
|
|
376
|
+
- Exclamation usage: {writing_style.get("exclamation_rate", 0):.2f} per message
|
|
377
|
+
- Question usage: {writing_style.get("question_rate", 0):.2f} per message
|
|
378
|
+
|
|
379
|
+
## Response Style Guidelines
|
|
380
|
+
|
|
381
|
+
When helping this user with text interactions:
|
|
382
|
+
|
|
383
|
+
1. **Match their tone**: Write in a {tone} style
|
|
384
|
+
2. **Match their verbosity**: Keep responses {verbosity}
|
|
385
|
+
3. **Structure**:
|
|
386
|
+
{chr(10).join(f" - {pref}" for pref in structure_prefs) if structure_prefs else " - Use clear, simple structure"}
|
|
387
|
+
|
|
388
|
+
## Common Clarifying Questions
|
|
389
|
+
|
|
390
|
+
When the user's request is ambiguous, consider asking:
|
|
391
|
+
|
|
392
|
+
{chr(10).join(f"- {q}" for q in clarifications[:10]) if clarifications else "- Ask for specific details about their goal"}
|
|
393
|
+
|
|
394
|
+
## Desktop App Integration
|
|
395
|
+
|
|
396
|
+
When helping draft messages or responses:
|
|
397
|
+
|
|
398
|
+
1. **Analyze the context**: What is the user trying to communicate?
|
|
399
|
+
2. **Match their style**: Use the tone and verbosity patterns above
|
|
400
|
+
3. **Ask clarifying questions**: If unclear, ask specific questions before drafting
|
|
401
|
+
4. **Provide options**: Offer 2-3 variations (formal, casual, concise)
|
|
402
|
+
|
|
403
|
+
## Example Phrases
|
|
404
|
+
|
|
405
|
+
Common words/phrases this user uses:
|
|
406
|
+
{", ".join(writing_style.get("common_words", [])[:15])}
|
|
407
|
+
|
|
408
|
+
Use these naturally when appropriate to match their voice.
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
*Generated: {Path.cwd()}*
|
|
413
|
+
*Analysis based on WhatsApp and Claude chat history*
|
|
414
|
+
"""
|
|
415
|
+
|
|
416
|
+
return rules_content
|
|
417
|
+
|
|
418
|
+
def export_analysis(self, output_path: Path):
|
|
419
|
+
"""Export full analysis as JSON."""
|
|
420
|
+
analysis = {
|
|
421
|
+
"writing_style": self.analyze_writing_style(),
|
|
422
|
+
"response_patterns": self.analyze_claude_response_patterns(),
|
|
423
|
+
"clarifying_questions": self.extract_common_clarifications(),
|
|
424
|
+
"sample_user_messages": [msg["text"] for msg in self.user_messages[:20]],
|
|
425
|
+
"sample_assistant_messages": [msg["text"] for msg in self.assistant_messages[:20]],
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
429
|
+
with open(output_path, "w") as f:
|
|
430
|
+
json.dump(analysis, f, indent=2)
|
|
431
|
+
|
|
432
|
+
return analysis
|
|
433
|
+
|
|
434
|
+
def analyze_semantic_style(
|
|
435
|
+
self,
|
|
436
|
+
min_cluster_size: int = 10,
|
|
437
|
+
) -> Optional["SemanticStyleAnalysis"]:
|
|
438
|
+
"""Run semantic style analysis on user messages.
|
|
439
|
+
|
|
440
|
+
Clusters messages by topic and analyzes style patterns per topic.
|
|
441
|
+
Requires sentence-transformers and scikit-learn.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
min_cluster_size: Minimum messages per topic cluster
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
SemanticStyleAnalysis or None if dependencies unavailable
|
|
448
|
+
"""
|
|
449
|
+
if not HAS_SEMANTIC:
|
|
450
|
+
print("[SemanticStyle] Dependencies not available. Install: pip install sentence-transformers scikit-learn")
|
|
451
|
+
return None
|
|
452
|
+
|
|
453
|
+
if not self.user_messages:
|
|
454
|
+
print("[SemanticStyle] No user messages to analyze")
|
|
455
|
+
return None
|
|
456
|
+
|
|
457
|
+
texts = [msg["text"] for msg in self.user_messages if msg.get("text")]
|
|
458
|
+
if len(texts) < min_cluster_size * 2:
|
|
459
|
+
print(f"[SemanticStyle] Need at least {min_cluster_size * 2} messages, have {len(texts)}")
|
|
460
|
+
return None
|
|
461
|
+
|
|
462
|
+
analyzer = SemanticStyleAnalyzer()
|
|
463
|
+
return analyzer.analyze(texts, min_cluster_size=min_cluster_size)
|
|
464
|
+
|
|
465
|
+
def generate_semantic_rules(
|
|
466
|
+
self,
|
|
467
|
+
output_dir: Path,
|
|
468
|
+
min_cluster_size: int = 10,
|
|
469
|
+
) -> Optional[Path]:
|
|
470
|
+
"""Generate semantic style rules and save to directory.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
output_dir: Directory to save semantic-style-rules.md and JSON
|
|
474
|
+
min_cluster_size: Minimum messages per topic cluster
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Path to markdown file or None if analysis failed
|
|
478
|
+
"""
|
|
479
|
+
analysis = self.analyze_semantic_style(min_cluster_size=min_cluster_size)
|
|
480
|
+
if analysis is None:
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
484
|
+
|
|
485
|
+
# Save markdown rules
|
|
486
|
+
rules_path = output_dir / "semantic-style-rules.md"
|
|
487
|
+
rules_path.write_text(analysis.style_rules_markdown)
|
|
488
|
+
|
|
489
|
+
# Save JSON data
|
|
490
|
+
data = {
|
|
491
|
+
"topics": {
|
|
492
|
+
name: {
|
|
493
|
+
"message_count": len(cluster.messages),
|
|
494
|
+
"avg_length": cluster.avg_length,
|
|
495
|
+
"formality": cluster.formality,
|
|
496
|
+
"emoji_rate": cluster.emoji_rate,
|
|
497
|
+
"language_mix": cluster.language_mix,
|
|
498
|
+
"common_phrases": cluster.common_phrases,
|
|
499
|
+
}
|
|
500
|
+
for name, cluster in analysis.topic_clusters.items()
|
|
501
|
+
},
|
|
502
|
+
"insights": analysis.cross_topic_insights,
|
|
503
|
+
}
|
|
504
|
+
json_path = output_dir / "semantic-style-data.json"
|
|
505
|
+
json_path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
506
|
+
|
|
507
|
+
print(f"[SemanticStyle] Saved rules to {rules_path}")
|
|
508
|
+
return rules_path
|