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,568 @@
|
|
|
1
|
+
"""Longitudinal communication style analyzer.
|
|
2
|
+
|
|
3
|
+
Analyzes communication patterns over time periods using LLM,
|
|
4
|
+
tracks evolution, and generates style rules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import ollama
|
|
15
|
+
|
|
16
|
+
HAS_OLLAMA = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
HAS_OLLAMA = False
|
|
19
|
+
|
|
20
|
+
from .time_batcher import TimeBatch, get_period_weight
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class PeriodAnalysis:
|
|
25
|
+
"""Analysis results for a single time period."""
|
|
26
|
+
|
|
27
|
+
period: str
|
|
28
|
+
language: str # "hebrew", "english", or "all"
|
|
29
|
+
message_count: int
|
|
30
|
+
avg_length: float
|
|
31
|
+
formality_score: float # 0-10
|
|
32
|
+
emoji_rate: float
|
|
33
|
+
common_phrases: list[str]
|
|
34
|
+
tone_description: str
|
|
35
|
+
key_characteristics: list[str]
|
|
36
|
+
raw_analysis: str # Full LLM output
|
|
37
|
+
|
|
38
|
+
def to_dict(self) -> dict:
|
|
39
|
+
return {
|
|
40
|
+
"period": self.period,
|
|
41
|
+
"language": self.language,
|
|
42
|
+
"message_count": self.message_count,
|
|
43
|
+
"avg_length": self.avg_length,
|
|
44
|
+
"formality_score": self.formality_score,
|
|
45
|
+
"emoji_rate": self.emoji_rate,
|
|
46
|
+
"common_phrases": self.common_phrases,
|
|
47
|
+
"tone_description": self.tone_description,
|
|
48
|
+
"key_characteristics": self.key_characteristics,
|
|
49
|
+
"raw_analysis": self.raw_analysis,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def analyze_batch_with_llm(
|
|
54
|
+
batch: TimeBatch,
|
|
55
|
+
language: str = "all",
|
|
56
|
+
model: str = "qwen3-coder-64k",
|
|
57
|
+
max_samples: int = 250,
|
|
58
|
+
style_collection=None,
|
|
59
|
+
) -> PeriodAnalysis:
|
|
60
|
+
"""
|
|
61
|
+
Analyze a time batch using LLM.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
batch: The time batch to analyze
|
|
65
|
+
language: "hebrew", "english", or "all"
|
|
66
|
+
model: Ollama model to use
|
|
67
|
+
max_samples: Maximum messages to include in prompt
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
PeriodAnalysis with results
|
|
71
|
+
"""
|
|
72
|
+
if not HAS_OLLAMA:
|
|
73
|
+
raise ImportError("ollama package required: pip install ollama")
|
|
74
|
+
|
|
75
|
+
# Filter by language if specified
|
|
76
|
+
if language == "hebrew":
|
|
77
|
+
messages = batch.hebrew_messages
|
|
78
|
+
elif language == "english":
|
|
79
|
+
messages = batch.english_messages
|
|
80
|
+
else:
|
|
81
|
+
messages = batch.messages
|
|
82
|
+
|
|
83
|
+
if not messages:
|
|
84
|
+
return PeriodAnalysis(
|
|
85
|
+
period=batch.period,
|
|
86
|
+
language=language,
|
|
87
|
+
message_count=0,
|
|
88
|
+
avg_length=0,
|
|
89
|
+
formality_score=5.0,
|
|
90
|
+
emoji_rate=0,
|
|
91
|
+
common_phrases=[],
|
|
92
|
+
tone_description="No messages in this period",
|
|
93
|
+
key_characteristics=[],
|
|
94
|
+
raw_analysis="",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Sample messages for prompt: cluster-based if style_collection, else first N
|
|
98
|
+
if style_collection:
|
|
99
|
+
from .cluster_sampling import cluster_sample_messages
|
|
100
|
+
from .style_index import get_embeddings_for_batch
|
|
101
|
+
|
|
102
|
+
start_epoch = batch.start_date.timestamp()
|
|
103
|
+
end_epoch = batch.end_date.timestamp()
|
|
104
|
+
embs, docs = get_embeddings_for_batch(style_collection, start_epoch, end_epoch, language)
|
|
105
|
+
if embs and docs:
|
|
106
|
+
|
|
107
|
+
class _TextDoc:
|
|
108
|
+
def __init__(self, text):
|
|
109
|
+
self.text = text
|
|
110
|
+
|
|
111
|
+
items = [_TextDoc(d) for d in docs]
|
|
112
|
+
samples = cluster_sample_messages(items, embs, max_total=max_samples)
|
|
113
|
+
samples_text = "\n".join([f"- {m.text[:200]}" for m in samples])
|
|
114
|
+
else:
|
|
115
|
+
samples = messages[:max_samples]
|
|
116
|
+
samples_text = "\n".join([f"- {m.text[:200]}" for m in samples])
|
|
117
|
+
else:
|
|
118
|
+
samples = messages[:max_samples]
|
|
119
|
+
samples_text = "\n".join([f"- {m.text[:200]}" for m in samples])
|
|
120
|
+
|
|
121
|
+
# Calculate basic metrics
|
|
122
|
+
total_length = sum(len(m.text) for m in messages)
|
|
123
|
+
avg_length = total_length / len(messages)
|
|
124
|
+
|
|
125
|
+
# Count emojis (simple pattern)
|
|
126
|
+
import re
|
|
127
|
+
|
|
128
|
+
emoji_pattern = re.compile(
|
|
129
|
+
r"[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F1E0-\U0001F1FF]"
|
|
130
|
+
)
|
|
131
|
+
emoji_count = sum(len(emoji_pattern.findall(m.text)) for m in messages)
|
|
132
|
+
emoji_rate = emoji_count / len(messages)
|
|
133
|
+
|
|
134
|
+
relationship_ctx = batch.get_relationship_context()
|
|
135
|
+
relationship_block = (
|
|
136
|
+
f"\n{relationship_ctx}\nConsider how tone may shift by relationship.\n" if relationship_ctx else ""
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
prompt = f"""Analyze this person's communication style from {len(samples)} messages in the period {batch.period}.
|
|
140
|
+
|
|
141
|
+
Language focus: {language.upper()}
|
|
142
|
+
{relationship_block}
|
|
143
|
+
CRITICAL RULES:
|
|
144
|
+
- Every phrase, example, and quote MUST appear verbatim in the sample messages below. Do NOT invent or paraphrase.
|
|
145
|
+
- Be specific. Cite exact text from the messages.
|
|
146
|
+
- If a phrase appears only once, note it as "occasional" not "frequent."
|
|
147
|
+
|
|
148
|
+
Sample messages:
|
|
149
|
+
{samples_text}
|
|
150
|
+
|
|
151
|
+
Provide a structured analysis:
|
|
152
|
+
|
|
153
|
+
## 1. FORMALITY SCORE (0-10)
|
|
154
|
+
Rate from 0 (very casual) to 10 (very formal). Provide the number and brief justification. Reference specific messages.
|
|
155
|
+
|
|
156
|
+
## 2. TONE DESCRIPTION
|
|
157
|
+
Describe the overall tone in 1-2 sentences. Base this on actual message content.
|
|
158
|
+
|
|
159
|
+
## 3. KEY CHARACTERISTICS
|
|
160
|
+
List 3-5 distinct characteristics. Each must be different—no overlap (e.g., don't list both "short messages" and "concise" as separate items).
|
|
161
|
+
|
|
162
|
+
## 4. COMMON PHRASES
|
|
163
|
+
List 5-10 phrases they use. Copy them EXACTLY from the messages—character for character. Include nothing invented.
|
|
164
|
+
|
|
165
|
+
## 5. COMMUNICATION PATTERNS
|
|
166
|
+
- How do they start messages? (quote real examples)
|
|
167
|
+
- How do they make requests? (quote real examples)
|
|
168
|
+
- How do they express emotions? (quote real examples)
|
|
169
|
+
- Use of punctuation and capitalization
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
response = ollama.generate(
|
|
173
|
+
model=model,
|
|
174
|
+
prompt=prompt,
|
|
175
|
+
options={
|
|
176
|
+
"num_ctx": 16000,
|
|
177
|
+
"temperature": 0.1,
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
raw_analysis = response["response"]
|
|
182
|
+
|
|
183
|
+
# Parse formality score from response (simple extraction)
|
|
184
|
+
formality_score = 5.0 # Default
|
|
185
|
+
import re
|
|
186
|
+
|
|
187
|
+
score_match = re.search(r"(?:score|rating)[:\s]*(\d+(?:\.\d+)?)", raw_analysis.lower())
|
|
188
|
+
if score_match:
|
|
189
|
+
try:
|
|
190
|
+
formality_score = float(score_match.group(1))
|
|
191
|
+
except ValueError:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
# Extract key characteristics (lines starting with - or *)
|
|
195
|
+
char_pattern = re.compile(r"^[\-\*]\s*(.+)$", re.MULTILINE)
|
|
196
|
+
characteristics = char_pattern.findall(raw_analysis)[:10]
|
|
197
|
+
|
|
198
|
+
# Extract common phrases (quoted text)
|
|
199
|
+
phrase_pattern = re.compile(r'"([^"]+)"')
|
|
200
|
+
phrases = phrase_pattern.findall(raw_analysis)[:10]
|
|
201
|
+
|
|
202
|
+
# Extract tone description (first sentence after "tone" keyword)
|
|
203
|
+
tone_desc = "Professional and direct" # Default
|
|
204
|
+
tone_match = re.search(r"tone[:\s]+([^.]+\.)", raw_analysis.lower())
|
|
205
|
+
if tone_match:
|
|
206
|
+
tone_desc = tone_match.group(1).strip().capitalize()
|
|
207
|
+
|
|
208
|
+
return PeriodAnalysis(
|
|
209
|
+
period=batch.period,
|
|
210
|
+
language=language,
|
|
211
|
+
message_count=len(messages),
|
|
212
|
+
avg_length=avg_length,
|
|
213
|
+
formality_score=formality_score,
|
|
214
|
+
emoji_rate=emoji_rate,
|
|
215
|
+
common_phrases=phrases,
|
|
216
|
+
tone_description=tone_desc,
|
|
217
|
+
key_characteristics=characteristics,
|
|
218
|
+
raw_analysis=raw_analysis,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def analyze_evolution(
|
|
223
|
+
analyses: list[PeriodAnalysis],
|
|
224
|
+
model: str = "qwen3-coder-64k",
|
|
225
|
+
) -> str:
|
|
226
|
+
"""
|
|
227
|
+
Analyze how communication style evolved across periods.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
analyses: List of period analyses in chronological order
|
|
231
|
+
model: Ollama model to use
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Evolution analysis as markdown
|
|
235
|
+
"""
|
|
236
|
+
if not HAS_OLLAMA:
|
|
237
|
+
raise ImportError("ollama package required: pip install ollama")
|
|
238
|
+
|
|
239
|
+
if len(analyses) < 2:
|
|
240
|
+
return "Not enough periods to analyze evolution."
|
|
241
|
+
|
|
242
|
+
# Build summary of each period
|
|
243
|
+
summaries = []
|
|
244
|
+
for a in analyses:
|
|
245
|
+
summaries.append(f"""
|
|
246
|
+
### {a.period} ({a.language})
|
|
247
|
+
- Messages: {a.message_count}
|
|
248
|
+
- Formality: {a.formality_score}/10
|
|
249
|
+
- Avg length: {a.avg_length:.0f} chars
|
|
250
|
+
- Emoji rate: {a.emoji_rate:.2f}
|
|
251
|
+
- Tone: {a.tone_description}
|
|
252
|
+
- Key traits: {", ".join(a.key_characteristics[:3])}
|
|
253
|
+
""")
|
|
254
|
+
|
|
255
|
+
prompt = f"""Analyze how this person's communication style evolved over time.
|
|
256
|
+
|
|
257
|
+
Period summaries:
|
|
258
|
+
{"".join(summaries)}
|
|
259
|
+
|
|
260
|
+
Provide an evolution analysis:
|
|
261
|
+
|
|
262
|
+
## 1. OVERALL TRAJECTORY
|
|
263
|
+
How has their style changed from earliest to latest period?
|
|
264
|
+
|
|
265
|
+
## 2. FORMALITY TREND
|
|
266
|
+
Are they becoming more or less formal? Why might this be?
|
|
267
|
+
|
|
268
|
+
## 3. NOTABLE CHANGES
|
|
269
|
+
What are the most significant changes between periods?
|
|
270
|
+
|
|
271
|
+
## 4. CONSISTENCY
|
|
272
|
+
What aspects of their style have remained consistent?
|
|
273
|
+
|
|
274
|
+
## 5. PREDICTIONS
|
|
275
|
+
Based on the trajectory, how might their style continue to evolve?
|
|
276
|
+
|
|
277
|
+
Be specific and reference the data from each period.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
response = ollama.generate(
|
|
281
|
+
model=model,
|
|
282
|
+
prompt=prompt,
|
|
283
|
+
options={
|
|
284
|
+
"num_ctx": 16000,
|
|
285
|
+
"temperature": 0.1,
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return response["response"]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _collect_grounded_phrases(analyses: list[PeriodAnalysis]) -> list[str]:
|
|
293
|
+
"""Collect unique phrases from analyses (recent-weighted) for grounded examples."""
|
|
294
|
+
seen = set()
|
|
295
|
+
phrases = []
|
|
296
|
+
current_year = datetime.now().year
|
|
297
|
+
for a in reversed(analyses): # Recent first
|
|
298
|
+
weight = get_period_weight(a.period, current_year)
|
|
299
|
+
for p in a.common_phrases:
|
|
300
|
+
key = p.strip().lower()[:50]
|
|
301
|
+
if key and key not in seen and len(p) > 2:
|
|
302
|
+
seen.add(key)
|
|
303
|
+
phrases.append(p)
|
|
304
|
+
if len(phrases) >= 30:
|
|
305
|
+
return phrases
|
|
306
|
+
return phrases
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def generate_weighted_master_guide(
|
|
310
|
+
analyses: list[PeriodAnalysis],
|
|
311
|
+
evolution_analysis: str,
|
|
312
|
+
model: str = "qwen3-coder-64k",
|
|
313
|
+
) -> str:
|
|
314
|
+
"""
|
|
315
|
+
Generate a master style guide using two-pass approach:
|
|
316
|
+
Pass 1: Extract raw rules (grounded in actual phrases)
|
|
317
|
+
Pass 2: Consolidate, deduplicate, validate
|
|
318
|
+
"""
|
|
319
|
+
if not HAS_OLLAMA:
|
|
320
|
+
raise ImportError("ollama package required: pip install ollama")
|
|
321
|
+
|
|
322
|
+
current_year = datetime.now().year
|
|
323
|
+
grounded_phrases = _collect_grounded_phrases(analyses)
|
|
324
|
+
phrases_block = "\n".join(f'- "{p}"' for p in grounded_phrases[:25])
|
|
325
|
+
|
|
326
|
+
weighted_summaries = []
|
|
327
|
+
for a in analyses:
|
|
328
|
+
weight = get_period_weight(a.period, current_year)
|
|
329
|
+
weighted_summaries.append(f"""
|
|
330
|
+
### {a.period} (weight: {weight:.0%})
|
|
331
|
+
- Formality: {a.formality_score}/10
|
|
332
|
+
- Tone: {a.tone_description}
|
|
333
|
+
- Key traits: {", ".join(a.key_characteristics[:5])}
|
|
334
|
+
- Common phrases: {", ".join(a.common_phrases[:5])}
|
|
335
|
+
""")
|
|
336
|
+
|
|
337
|
+
# Pass 1: Extract with strict grounding
|
|
338
|
+
pass1_prompt = f"""Create a communication style guide based on this person's writing patterns.
|
|
339
|
+
|
|
340
|
+
PHRASES you MAY use as examples (quote exactly—do not invent):
|
|
341
|
+
{phrases_block}
|
|
342
|
+
|
|
343
|
+
Period analyses:
|
|
344
|
+
{"".join(weighted_summaries)}
|
|
345
|
+
|
|
346
|
+
Evolution context:
|
|
347
|
+
{evolution_analysis[:1500]}
|
|
348
|
+
|
|
349
|
+
Generate a MASTER STYLE GUIDE. CRITICAL RULES:
|
|
350
|
+
1. Every example in DO's and DON'Ts MUST be a direct quote from the phrases above or clearly derived from the period analyses. No invented examples.
|
|
351
|
+
2. Each DO rule must be DISTINCT—if two rules say the same thing, merge them.
|
|
352
|
+
3. Each DON'T rule must be DISTINCT—same principle.
|
|
353
|
+
4. Aim for 8-10 DO's and 8-10 DON'Ts. Quality over quantity.
|
|
354
|
+
|
|
355
|
+
Structure:
|
|
356
|
+
## 1. CURRENT VOICE (Who They Are Now)
|
|
357
|
+
## 2. DO's - Rules for AI to Follow (with real examples)
|
|
358
|
+
## 3. DON'Ts - What to Avoid (with real counter-examples)
|
|
359
|
+
## 4. LANGUAGE-SPECIFIC NOTES
|
|
360
|
+
## 5. EXAMPLE TRANSFORMATIONS (use phrases from the list above)
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
response1 = ollama.generate(model=model, prompt=pass1_prompt, options={"num_ctx": 24000, "temperature": 0.15})
|
|
364
|
+
raw_guide = response1["response"]
|
|
365
|
+
|
|
366
|
+
# Pass 2: Consolidate and validate
|
|
367
|
+
pass2_prompt = f"""You have a draft style guide. Your task: consolidate and validate it.
|
|
368
|
+
|
|
369
|
+
GROUNDED PHRASES (these are real—use only these for examples):
|
|
370
|
+
{phrases_block}
|
|
371
|
+
|
|
372
|
+
DRAFT GUIDE:
|
|
373
|
+
{raw_guide}
|
|
374
|
+
|
|
375
|
+
REVISION RULES:
|
|
376
|
+
1. Merge any DO rules that convey the same principle (e.g., "short messages" + "be concise" = one rule).
|
|
377
|
+
2. Merge any DON'T rules that overlap.
|
|
378
|
+
3. Replace any invented example with a phrase from the GROUNDED PHRASES list. If no match exists, remove the example.
|
|
379
|
+
4. Ensure each rule is actionable and distinct.
|
|
380
|
+
5. Remove redundancy—every rule should add new information.
|
|
381
|
+
6. Keep the same structure (CURRENT VOICE, DO's, DON'Ts, LANGUAGE-SPECIFIC, TRANSFORMATIONS).
|
|
382
|
+
7. Final output: 8-10 DO's, 8-10 DON'Ts.
|
|
383
|
+
|
|
384
|
+
Output the revised, consolidated guide. No preamble.
|
|
385
|
+
"""
|
|
386
|
+
|
|
387
|
+
response2 = ollama.generate(model=model, prompt=pass2_prompt, options={"num_ctx": 24000, "temperature": 0.1})
|
|
388
|
+
|
|
389
|
+
return response2["response"]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def generate_human_summary(
|
|
393
|
+
analyses: list[PeriodAnalysis],
|
|
394
|
+
evolution_analysis: str,
|
|
395
|
+
) -> str:
|
|
396
|
+
"""
|
|
397
|
+
Generate a short, human-readable summary.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
analyses: List of period analyses
|
|
401
|
+
evolution_analysis: The evolution analysis text
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Human-friendly summary as markdown
|
|
405
|
+
"""
|
|
406
|
+
if not analyses:
|
|
407
|
+
return "# No Data\n\nNo communication data available for analysis."
|
|
408
|
+
|
|
409
|
+
# Get most recent analysis
|
|
410
|
+
recent = analyses[-1] if analyses else None
|
|
411
|
+
|
|
412
|
+
# Calculate averages
|
|
413
|
+
avg_formality = sum(a.formality_score for a in analyses) / len(analyses)
|
|
414
|
+
avg_length = sum(a.avg_length for a in analyses) / len(analyses)
|
|
415
|
+
avg_emoji = sum(a.emoji_rate for a in analyses) / len(analyses)
|
|
416
|
+
|
|
417
|
+
# Determine trends
|
|
418
|
+
if len(analyses) >= 2:
|
|
419
|
+
formality_trend = analyses[-1].formality_score - analyses[0].formality_score
|
|
420
|
+
if formality_trend > 1:
|
|
421
|
+
formality_direction = "becoming more formal"
|
|
422
|
+
elif formality_trend < -1:
|
|
423
|
+
formality_direction = "becoming more casual"
|
|
424
|
+
else:
|
|
425
|
+
formality_direction = "staying consistent"
|
|
426
|
+
else:
|
|
427
|
+
formality_direction = "not enough data for trend"
|
|
428
|
+
|
|
429
|
+
summary = f"""# Your Communication Style - Summary
|
|
430
|
+
|
|
431
|
+
*Generated: {datetime.now().strftime("%Y-%m-%d")}*
|
|
432
|
+
*Based on {sum(a.message_count for a in analyses):,} messages across {len(analyses)} time periods*
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## Who You Are Now ({recent.period if recent else "N/A"})
|
|
437
|
+
|
|
438
|
+
- **Formality**: {recent.formality_score if recent else "N/A"}/10
|
|
439
|
+
- **Average message length**: {(recent.avg_length if recent else 0):.0f} characters
|
|
440
|
+
- **Emoji usage**: {(recent.emoji_rate if recent else 0):.2f} per message
|
|
441
|
+
- **Tone**: {recent.tone_description if recent else "Unknown"}
|
|
442
|
+
|
|
443
|
+
## Key Characteristics
|
|
444
|
+
|
|
445
|
+
{chr(10).join(f"- {c}" for c in (recent.key_characteristics[:5] if recent else []))}
|
|
446
|
+
|
|
447
|
+
## How You've Changed
|
|
448
|
+
|
|
449
|
+
- **Overall**: {formality_direction}
|
|
450
|
+
- **Average formality**: {avg_formality:.1f}/10
|
|
451
|
+
- **Average message length**: {avg_length:.0f} chars
|
|
452
|
+
|
|
453
|
+
## Common Phrases You Use
|
|
454
|
+
|
|
455
|
+
{chr(10).join(f'- "{p}"' for p in (recent.common_phrases[:5] if recent else []))}
|
|
456
|
+
|
|
457
|
+
## Quick Rules for AI
|
|
458
|
+
|
|
459
|
+
1. Match formality level: {recent.formality_score if recent else 5}/10
|
|
460
|
+
2. Keep messages around {(recent.avg_length if recent else 100):.0f} characters
|
|
461
|
+
3. {"Use emojis sparingly" if (recent and recent.emoji_rate < 0.5) else "Emojis are acceptable"}
|
|
462
|
+
4. Be direct and get to the point
|
|
463
|
+
5. Match the {recent.tone_description.lower() if recent else "neutral"} tone
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
*For detailed rules, see master-style-guide.md*
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
return summary
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def run_full_analysis(
|
|
474
|
+
batches: list[TimeBatch],
|
|
475
|
+
output_dir: Path,
|
|
476
|
+
languages: list[str] = ["hebrew", "english"],
|
|
477
|
+
model: str = "qwen3-coder-64k",
|
|
478
|
+
progress_callback: Optional[callable] = None,
|
|
479
|
+
style_collection=None,
|
|
480
|
+
) -> dict:
|
|
481
|
+
"""
|
|
482
|
+
Run complete longitudinal analysis and save all outputs.
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
batches: List of time batches
|
|
486
|
+
output_dir: Directory to save output files
|
|
487
|
+
languages: Languages to analyze
|
|
488
|
+
model: Ollama model to use
|
|
489
|
+
progress_callback: Optional callback for progress updates
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Dictionary with paths to generated files
|
|
493
|
+
"""
|
|
494
|
+
output_dir = Path(output_dir)
|
|
495
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
496
|
+
|
|
497
|
+
per_period_dir = output_dir / "per-period"
|
|
498
|
+
per_period_dir.mkdir(exist_ok=True)
|
|
499
|
+
|
|
500
|
+
all_analyses = []
|
|
501
|
+
|
|
502
|
+
# Analyze each batch
|
|
503
|
+
for i, batch in enumerate(batches):
|
|
504
|
+
if progress_callback:
|
|
505
|
+
progress_callback(f"Analyzing {batch.period}...", i, len(batches))
|
|
506
|
+
|
|
507
|
+
for lang in languages:
|
|
508
|
+
# Check if there are messages in this language
|
|
509
|
+
if lang == "hebrew" and not batch.hebrew_messages:
|
|
510
|
+
continue
|
|
511
|
+
if lang == "english" and not batch.english_messages:
|
|
512
|
+
continue
|
|
513
|
+
|
|
514
|
+
analysis = analyze_batch_with_llm(batch, language=lang, model=model, style_collection=style_collection)
|
|
515
|
+
all_analyses.append(analysis)
|
|
516
|
+
|
|
517
|
+
# Save individual period analysis
|
|
518
|
+
period_file = per_period_dir / f"{batch.period}-{lang}-style.md"
|
|
519
|
+
with open(period_file, "w") as f:
|
|
520
|
+
f.write(f"# {batch.period} - {lang.title()} Style\n\n")
|
|
521
|
+
f.write(analysis.raw_analysis)
|
|
522
|
+
|
|
523
|
+
if progress_callback:
|
|
524
|
+
progress_callback("Analyzing evolution...", len(batches), len(batches))
|
|
525
|
+
|
|
526
|
+
# Analyze evolution
|
|
527
|
+
evolution = analyze_evolution(all_analyses, model=model)
|
|
528
|
+
evolution_file = output_dir / "evolution-analysis.md"
|
|
529
|
+
with open(evolution_file, "w") as f:
|
|
530
|
+
f.write("# Communication Style Evolution\n\n")
|
|
531
|
+
f.write(evolution)
|
|
532
|
+
|
|
533
|
+
if progress_callback:
|
|
534
|
+
progress_callback("Generating master guide...", len(batches), len(batches))
|
|
535
|
+
|
|
536
|
+
# Generate master guide
|
|
537
|
+
master_guide = generate_weighted_master_guide(all_analyses, evolution, model=model)
|
|
538
|
+
master_file = output_dir / "master-style-guide.md"
|
|
539
|
+
with open(master_file, "w") as f:
|
|
540
|
+
f.write("# Master Communication Style Guide\n\n")
|
|
541
|
+
f.write(master_guide)
|
|
542
|
+
|
|
543
|
+
# Generate human summary
|
|
544
|
+
human_summary = generate_human_summary(all_analyses, evolution)
|
|
545
|
+
summary_file = output_dir / "human-summary.md"
|
|
546
|
+
with open(summary_file, "w") as f:
|
|
547
|
+
f.write(human_summary)
|
|
548
|
+
|
|
549
|
+
# Save raw analysis data
|
|
550
|
+
data_file = output_dir / "analysis-data.json"
|
|
551
|
+
with open(data_file, "w") as f:
|
|
552
|
+
json.dump(
|
|
553
|
+
{
|
|
554
|
+
"analyses": [a.to_dict() for a in all_analyses],
|
|
555
|
+
"evolution": evolution,
|
|
556
|
+
"generated_at": datetime.now().isoformat(),
|
|
557
|
+
},
|
|
558
|
+
f,
|
|
559
|
+
indent=2,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
"per_period_dir": str(per_period_dir),
|
|
564
|
+
"evolution_file": str(evolution_file),
|
|
565
|
+
"master_file": str(master_file),
|
|
566
|
+
"summary_file": str(summary_file),
|
|
567
|
+
"data_file": str(data_file),
|
|
568
|
+
}
|