gitview 0.1.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.
gitview/storyteller.py ADDED
@@ -0,0 +1,352 @@
1
+ """Generate global narrative from phase summaries."""
2
+
3
+ import os
4
+ from typing import List, Dict, Any, Optional
5
+ from pathlib import Path
6
+ from datetime import datetime
7
+
8
+ from .chunker import Phase
9
+ from .backends import LLMRouter, LLMMessage
10
+
11
+
12
+ class StoryTeller:
13
+ """Generate global repository story from phase summaries."""
14
+
15
+ def __init__(self, backend: Optional[str] = None, model: Optional[str] = None,
16
+ api_key: Optional[str] = None, **kwargs):
17
+ """
18
+ Initialize storyteller with LLM backend.
19
+
20
+ Args:
21
+ backend: LLM backend ('anthropic', 'openai', 'ollama')
22
+ model: Model identifier (uses backend defaults if not specified)
23
+ api_key: API key for the backend (if required)
24
+ **kwargs: Additional backend parameters
25
+ """
26
+ self.router = LLMRouter(backend=backend, model=model, api_key=api_key, **kwargs)
27
+ self.model = self.router.model
28
+
29
+ def generate_global_story(self, phases: List[Phase],
30
+ repo_name: Optional[str] = None) -> Dict[str, str]:
31
+ """
32
+ Generate comprehensive repository story from phases.
33
+
34
+ Args:
35
+ phases: List of Phase objects with summaries
36
+ repo_name: Optional repository name
37
+
38
+ Returns:
39
+ Dict with different story sections:
40
+ - 'executive_summary': High-level overview
41
+ - 'timeline': Chronological timeline with headings
42
+ - 'technical_evolution': Technical architecture evolution
43
+ - 'deletion_story': Story of what was removed and why
44
+ - 'full_narrative': Complete detailed narrative
45
+ """
46
+ # Ensure all phases have summaries
47
+ if any(p.summary is None for p in phases):
48
+ raise ValueError("All phases must have summaries before generating global story")
49
+
50
+ print("Generating global story...")
51
+
52
+ # Prepare phase summaries data
53
+ phase_summaries = self._prepare_phase_summaries(phases)
54
+
55
+ # Generate different story sections
56
+ stories = {}
57
+
58
+ print(" - Executive summary...")
59
+ stories['executive_summary'] = self._generate_executive_summary(
60
+ phase_summaries, repo_name
61
+ )
62
+
63
+ print(" - Timeline...")
64
+ stories['timeline'] = self._generate_timeline(phase_summaries, repo_name)
65
+
66
+ print(" - Technical evolution...")
67
+ stories['technical_evolution'] = self._generate_technical_evolution(
68
+ phase_summaries, repo_name
69
+ )
70
+
71
+ print(" - Deletion story...")
72
+ stories['deletion_story'] = self._generate_deletion_story(
73
+ phase_summaries, repo_name
74
+ )
75
+
76
+ print(" - Full narrative...")
77
+ stories['full_narrative'] = self._generate_full_narrative(
78
+ phase_summaries, repo_name
79
+ )
80
+
81
+ return stories
82
+
83
+ def _prepare_phase_summaries(self, phases: List[Phase]) -> List[Dict[str, Any]]:
84
+ """Prepare phase summaries for LLM prompts."""
85
+ summaries = []
86
+
87
+ for phase in phases:
88
+ summaries.append({
89
+ 'phase_number': phase.phase_number,
90
+ 'start_date': phase.start_date[:10],
91
+ 'end_date': phase.end_date[:10],
92
+ 'commit_count': phase.commit_count,
93
+ 'loc_delta': phase.loc_delta,
94
+ 'loc_delta_percent': phase.loc_delta_percent,
95
+ 'total_insertions': phase.total_insertions,
96
+ 'total_deletions': phase.total_deletions,
97
+ 'authors': phase.authors,
98
+ 'primary_author': phase.primary_author,
99
+ 'has_large_deletion': phase.has_large_deletion,
100
+ 'has_large_addition': phase.has_large_addition,
101
+ 'has_refactor': phase.has_refactor,
102
+ 'readme_changed': phase.readme_changed,
103
+ 'summary': phase.summary,
104
+ })
105
+
106
+ return summaries
107
+
108
+ def _generate_executive_summary(self, phase_summaries: List[Dict[str, Any]],
109
+ repo_name: Optional[str] = None) -> str:
110
+ """Generate high-level executive summary."""
111
+ prompt = self._build_executive_summary_prompt(phase_summaries, repo_name)
112
+
113
+ messages = [LLMMessage(role="user", content=prompt)]
114
+ response = self.router.generate(messages, max_tokens=1500)
115
+
116
+ return response.content.strip()
117
+
118
+ def _generate_timeline(self, phase_summaries: List[Dict[str, Any]],
119
+ repo_name: Optional[str] = None) -> str:
120
+ """Generate chronological timeline."""
121
+ prompt = self._build_timeline_prompt(phase_summaries, repo_name)
122
+
123
+ messages = [LLMMessage(role="user", content=prompt)]
124
+ response = self.router.generate(messages, max_tokens=3000)
125
+
126
+ return response.content.strip()
127
+
128
+ def _generate_technical_evolution(self, phase_summaries: List[Dict[str, Any]],
129
+ repo_name: Optional[str] = None) -> str:
130
+ """Generate technical architecture evolution story."""
131
+ prompt = self._build_technical_evolution_prompt(phase_summaries, repo_name)
132
+
133
+ messages = [LLMMessage(role="user", content=prompt)]
134
+ response = self.router.generate(messages, max_tokens=3000)
135
+
136
+ return response.content.strip()
137
+
138
+ def _generate_deletion_story(self, phase_summaries: List[Dict[str, Any]],
139
+ repo_name: Optional[str] = None) -> str:
140
+ """Generate story of code deletions and cleanups."""
141
+ prompt = self._build_deletion_story_prompt(phase_summaries, repo_name)
142
+
143
+ messages = [LLMMessage(role="user", content=prompt)]
144
+ response = self.router.generate(messages, max_tokens=2000)
145
+
146
+ return response.content.strip()
147
+
148
+ def _generate_full_narrative(self, phase_summaries: List[Dict[str, Any]],
149
+ repo_name: Optional[str] = None) -> str:
150
+ """Generate complete detailed narrative."""
151
+ prompt = self._build_full_narrative_prompt(phase_summaries, repo_name)
152
+
153
+ messages = [LLMMessage(role="user", content=prompt)]
154
+ response = self.router.generate(messages, max_tokens=4000)
155
+
156
+ return response.content.strip()
157
+
158
+ def _build_executive_summary_prompt(self, phase_summaries: List[Dict[str, Any]],
159
+ repo_name: Optional[str]) -> str:
160
+ """Build prompt for executive summary."""
161
+ repo_title = repo_name or "this repository"
162
+
163
+ # Calculate totals
164
+ total_commits = sum(p['commit_count'] for p in phase_summaries)
165
+ total_insertions = sum(p['total_insertions'] for p in phase_summaries)
166
+ total_deletions = sum(p['total_deletions'] for p in phase_summaries)
167
+ all_authors = set()
168
+ for p in phase_summaries:
169
+ all_authors.update(p['authors'])
170
+
171
+ prompt = f"""You are writing an executive summary of the evolution of {repo_title}.
172
+
173
+ **Overall Statistics:**
174
+ - Total Phases: {len(phase_summaries)}
175
+ - Total Commits: {total_commits:,}
176
+ - Total Insertions: +{total_insertions:,} lines
177
+ - Total Deletions: -{total_deletions:,} lines
178
+ - Contributors: {len(all_authors)}
179
+ - Time Span: {phase_summaries[0]['start_date']} to {phase_summaries[-1]['end_date']}
180
+
181
+ **Phase Summaries:**
182
+ """
183
+
184
+ for p in phase_summaries:
185
+ prompt += f"\n**Phase {p['phase_number']} ({p['start_date']} to {p['end_date']})**\n"
186
+ prompt += f"- LOC Δ: {p['loc_delta']:+,d} ({p['loc_delta_percent']:+.1f}%)\n"
187
+ prompt += f"- Summary: {p['summary']}\n"
188
+
189
+ prompt += """
190
+ **Your Task:**
191
+ Write a concise executive summary (2-3 paragraphs) that:
192
+ 1. Provides a high-level overview of the repository's evolution
193
+ 2. Highlights the major milestones and transformations
194
+ 3. Identifies key themes across the entire history
195
+ 4. Summarizes the overall trajectory (growth, maturity, focus areas)
196
+
197
+ Keep it business-friendly and accessible to non-technical readers while maintaining technical accuracy.
198
+
199
+ Write the executive summary now:"""
200
+
201
+ return prompt
202
+
203
+ def _build_timeline_prompt(self, phase_summaries: List[Dict[str, Any]],
204
+ repo_name: Optional[str]) -> str:
205
+ """Build prompt for timeline generation."""
206
+ repo_title = repo_name or "Repository"
207
+
208
+ prompt = f"""Create a chronological timeline of {repo_title}'s evolution with clear headings for each phase.
209
+
210
+ **Phase Summaries:**
211
+ """
212
+
213
+ for p in phase_summaries:
214
+ prompt += f"\n**Phase {p['phase_number']} ({p['start_date']} to {p['end_date']})**\n"
215
+ prompt += f"- Commits: {p['commit_count']}\n"
216
+ prompt += f"- LOC Δ: {p['loc_delta']:+,d} ({p['loc_delta_percent']:+.1f}%)\n"
217
+ prompt += f"- Authors: {', '.join(p['authors'])}\n"
218
+ prompt += f"- Summary: {p['summary']}\n"
219
+
220
+ prompt += """
221
+ **Your Task:**
222
+ Create a timeline in markdown format with:
223
+ 1. A descriptive heading for each phase (e.g., "Early Prototyping", "Major Refactoring", "Stabilization")
224
+ 2. Date range
225
+ 3. Key highlights in bullet points
226
+ 4. Major changes and decisions
227
+
228
+ Format example:
229
+ ## Phase 1: Early Prototyping (Jan - Mar 2018)
230
+ - Initial commit with basic structure
231
+ - Rapid experimentation with...
232
+ - ...
233
+
234
+ Write the timeline now:"""
235
+
236
+ return prompt
237
+
238
+ def _build_technical_evolution_prompt(self, phase_summaries: List[Dict[str, Any]],
239
+ repo_name: Optional[str]) -> str:
240
+ """Build prompt for technical evolution."""
241
+ repo_title = repo_name or "this codebase"
242
+
243
+ prompt = f"""Analyze the technical and architectural evolution of {repo_title}.
244
+
245
+ **Phase Summaries:**
246
+ """
247
+
248
+ for p in phase_summaries:
249
+ prompt += f"\n**Phase {p['phase_number']}** ({p['start_date']} to {p['end_date']})\n"
250
+ prompt += f"{p['summary']}\n"
251
+
252
+ prompt += """
253
+ **Your Task:**
254
+ Write a technical retrospective that:
255
+ 1. Traces the architectural evolution across phases
256
+ 2. Identifies major technical decisions and their motivations
257
+ 3. Highlights refactorings and their impact
258
+ 4. Discusses technology choices and migrations
259
+ 5. Notes patterns in how the codebase matured
260
+
261
+ Write from a senior engineer's perspective, focusing on the technical journey.
262
+
263
+ Write the technical evolution now:"""
264
+
265
+ return prompt
266
+
267
+ def _build_deletion_story_prompt(self, phase_summaries: List[Dict[str, Any]],
268
+ repo_name: Optional[str]) -> str:
269
+ """Build prompt for deletion story."""
270
+ # Find phases with significant deletions
271
+ deletion_phases = [p for p in phase_summaries if p['has_large_deletion']]
272
+
273
+ prompt = f"""Tell the story of what was removed from the codebase and why.
274
+
275
+ **All Phases:**
276
+ """
277
+
278
+ for p in phase_summaries:
279
+ prompt += f"\nPhase {p['phase_number']} ({p['start_date']} to {p['end_date']})\n"
280
+ prompt += f"- Deletions: -{p['total_deletions']:,} lines\n"
281
+ prompt += f"- Large Deletion: {p['has_large_deletion']}\n"
282
+ prompt += f"- Summary: {p['summary']}\n"
283
+
284
+ prompt += """
285
+ **Your Task:**
286
+ Write a narrative about the deletion and cleanup efforts:
287
+ 1. What major components were removed?
288
+ 2. Why were they removed? (deprecated, replaced, experimental, etc.)
289
+ 3. How did these deletions improve the codebase?
290
+ 4. What does this tell us about the project's evolution?
291
+
292
+ Focus on the story of simplification, refactoring, and evolution through removal.
293
+
294
+ Write the deletion story now:"""
295
+
296
+ return prompt
297
+
298
+ def _build_full_narrative_prompt(self, phase_summaries: List[Dict[str, Any]],
299
+ repo_name: Optional[str]) -> str:
300
+ """Build prompt for full narrative."""
301
+ repo_title = repo_name or "this repository"
302
+
303
+ prompt = f"""Write a comprehensive narrative of {repo_title}'s evolution.
304
+
305
+ **Phase Summaries:**
306
+ """
307
+
308
+ for p in phase_summaries:
309
+ prompt += f"\n**Phase {p['phase_number']} ({p['start_date']} to {p['end_date']})**\n"
310
+ prompt += f"- Commits: {p['commit_count']}, LOC Δ: {p['loc_delta']:+,d}\n"
311
+ prompt += f"- Authors: {', '.join(p['authors'])}\n"
312
+ prompt += f"{p['summary']}\n"
313
+
314
+ prompt += """
315
+ **Your Task:**
316
+ Write a complete, detailed narrative (multiple paragraphs) that:
317
+ 1. Tells the full story of the repository from beginning to end
318
+ 2. Weaves together all phases into a coherent narrative
319
+ 3. Highlights the evolution, challenges, and successes
320
+ 4. Maintains chronological flow while identifying themes
321
+ 5. Makes connections between phases
322
+ 6. Provides both technical depth and big-picture perspective
323
+
324
+ This should read like a well-crafted story with a beginning, middle, and current state.
325
+
326
+ Write the full narrative now:"""
327
+
328
+ return prompt
329
+
330
+
331
+ def generate_story(phases: List[Phase],
332
+ repo_name: Optional[str] = None,
333
+ backend: Optional[str] = None,
334
+ model: Optional[str] = None,
335
+ api_key: Optional[str] = None,
336
+ **kwargs) -> Dict[str, str]:
337
+ """
338
+ Generate global repository story.
339
+
340
+ Args:
341
+ phases: List of Phase objects with summaries
342
+ repo_name: Optional repository name
343
+ backend: LLM backend ('anthropic', 'openai', 'ollama')
344
+ model: Model identifier (uses backend defaults if not specified)
345
+ api_key: API key for the backend (if required)
346
+ **kwargs: Additional backend parameters
347
+
348
+ Returns:
349
+ Dict with story sections
350
+ """
351
+ storyteller = StoryTeller(backend=backend, model=model, api_key=api_key, **kwargs)
352
+ return storyteller.generate_global_story(phases, repo_name)
gitview/summarizer.py ADDED
@@ -0,0 +1,270 @@
1
+ """LLM-based phase summarization."""
2
+
3
+ import json
4
+ import os
5
+ from typing import List, Dict, Any, Optional
6
+ from pathlib import Path
7
+
8
+ from .chunker import Phase
9
+ from .backends import LLMRouter, LLMMessage
10
+
11
+
12
+ class PhaseSummarizer:
13
+ """Summarize git history phases using LLM."""
14
+
15
+ def __init__(self, backend: Optional[str] = None, model: Optional[str] = None,
16
+ api_key: Optional[str] = None, **kwargs):
17
+ """
18
+ Initialize summarizer with LLM backend.
19
+
20
+ Args:
21
+ backend: LLM backend ('anthropic', 'openai', 'ollama')
22
+ model: Model identifier (uses backend defaults if not specified)
23
+ api_key: API key for the backend (if required)
24
+ **kwargs: Additional backend parameters
25
+ """
26
+ self.router = LLMRouter(backend=backend, model=model, api_key=api_key, **kwargs)
27
+ self.model = self.router.model
28
+
29
+ def summarize_phase(self, phase: Phase, context: Optional[str] = None) -> str:
30
+ """
31
+ Generate a narrative summary for a single phase.
32
+
33
+ Args:
34
+ phase: Phase object to summarize
35
+ context: Optional context from previous phases
36
+
37
+ Returns:
38
+ Narrative summary string
39
+ """
40
+ # Prepare phase data for LLM
41
+ phase_data = self._prepare_phase_data(phase)
42
+
43
+ # Build prompt
44
+ prompt = self._build_phase_prompt(phase_data, context)
45
+
46
+ # Call LLM backend
47
+ messages = [LLMMessage(role="user", content=prompt)]
48
+ response = self.router.generate(messages, max_tokens=2000)
49
+
50
+ return response.content.strip()
51
+
52
+ def summarize_all_phases(self, phases: List[Phase],
53
+ output_dir: Optional[str] = None) -> List[Phase]:
54
+ """
55
+ Summarize all phases with context from previous phases.
56
+
57
+ Args:
58
+ phases: List of Phase objects
59
+ output_dir: Optional directory to save updated phases
60
+
61
+ Returns:
62
+ List of Phase objects with summaries filled in
63
+ """
64
+ previous_summaries = []
65
+
66
+ for i, phase in enumerate(phases):
67
+ print(f"Summarizing phase {phase.phase_number}/{len(phases)}...")
68
+
69
+ # Build context from previous phases
70
+ context = self._build_context(previous_summaries)
71
+
72
+ # Generate summary
73
+ summary = self.summarize_phase(phase, context)
74
+ phase.summary = summary
75
+
76
+ # Store for next iteration
77
+ previous_summaries.append({
78
+ 'phase_number': phase.phase_number,
79
+ 'summary': summary,
80
+ 'loc_delta': phase.loc_delta,
81
+ })
82
+
83
+ # Save updated phase if output_dir provided
84
+ if output_dir:
85
+ self._save_phase_with_summary(phase, output_dir)
86
+
87
+ return phases
88
+
89
+ def _prepare_phase_data(self, phase: Phase) -> Dict[str, Any]:
90
+ """Prepare phase data for LLM prompt."""
91
+ # Get commit details
92
+ commits_summary = []
93
+ for commit in phase.commits[:20]: # Limit to first 20 commits
94
+ commits_summary.append({
95
+ 'hash': commit.short_hash,
96
+ 'date': commit.timestamp[:10], # Just the date
97
+ 'author': commit.author,
98
+ 'message': commit.commit_subject,
99
+ 'insertions': commit.insertions,
100
+ 'deletions': commit.deletions,
101
+ 'files_changed': commit.files_changed,
102
+ 'is_refactor': commit.is_refactor,
103
+ 'is_large_deletion': commit.is_large_deletion,
104
+ 'is_large_addition': commit.is_large_addition,
105
+ })
106
+
107
+ # Get significant commits (large changes, refactors)
108
+ significant_commits = []
109
+ for commit in phase.commits:
110
+ if commit.is_large_deletion or commit.is_large_addition or commit.is_refactor:
111
+ significant_commits.append({
112
+ 'hash': commit.short_hash,
113
+ 'message': commit.commit_message,
114
+ 'insertions': commit.insertions,
115
+ 'deletions': commit.deletions,
116
+ 'is_refactor': commit.is_refactor,
117
+ 'is_large_deletion': commit.is_large_deletion,
118
+ 'is_large_addition': commit.is_large_addition,
119
+ })
120
+
121
+ # Get README changes
122
+ readme_changes = []
123
+ for i, commit in enumerate(phase.commits):
124
+ if commit.readme_exists and commit.readme_excerpt:
125
+ if i == 0 or i == len(phase.commits) - 1:
126
+ readme_changes.append({
127
+ 'hash': commit.short_hash,
128
+ 'excerpt': commit.readme_excerpt,
129
+ 'position': 'start' if i == 0 else 'end'
130
+ })
131
+
132
+ # Get comment samples
133
+ comment_samples = []
134
+ for commit in phase.commits:
135
+ if commit.comment_samples:
136
+ comment_samples.extend(commit.comment_samples[:2])
137
+ comment_samples = comment_samples[:5] # Limit total
138
+
139
+ return {
140
+ 'phase_number': phase.phase_number,
141
+ 'start_date': phase.start_date[:10],
142
+ 'end_date': phase.end_date[:10],
143
+ 'commit_count': phase.commit_count,
144
+ 'loc_start': phase.loc_start,
145
+ 'loc_end': phase.loc_end,
146
+ 'loc_delta': phase.loc_delta,
147
+ 'loc_delta_percent': phase.loc_delta_percent,
148
+ 'total_insertions': phase.total_insertions,
149
+ 'total_deletions': phase.total_deletions,
150
+ 'languages_start': phase.languages_start,
151
+ 'languages_end': phase.languages_end,
152
+ 'authors': phase.authors,
153
+ 'primary_author': phase.primary_author,
154
+ 'has_large_deletion': phase.has_large_deletion,
155
+ 'has_large_addition': phase.has_large_addition,
156
+ 'has_refactor': phase.has_refactor,
157
+ 'readme_changed': phase.readme_changed,
158
+ 'commits': commits_summary,
159
+ 'significant_commits': significant_commits,
160
+ 'readme_changes': readme_changes,
161
+ 'comment_samples': comment_samples,
162
+ }
163
+
164
+ def _build_phase_prompt(self, phase_data: Dict[str, Any],
165
+ context: Optional[str] = None) -> str:
166
+ """Build prompt for phase summarization."""
167
+ prompt = f"""You are analyzing a phase in a git repository's history. Your task is to write a concise narrative summary of what happened during this phase.
168
+
169
+ **Phase Overview:**
170
+ - Phase Number: {phase_data['phase_number']}
171
+ - Time Period: {phase_data['start_date']} to {phase_data['end_date']}
172
+ - Commits: {phase_data['commit_count']}
173
+ - LOC Change: {phase_data['loc_delta']:+,d} ({phase_data['loc_delta_percent']:+.1f}%)
174
+ - Start: {phase_data['loc_start']:,} LOC
175
+ - End: {phase_data['loc_end']:,} LOC
176
+ - Total Changes: +{phase_data['total_insertions']:,} / -{phase_data['total_deletions']:,} lines
177
+ - Authors: {', '.join(phase_data['authors'])}
178
+ - Primary Author: {phase_data['primary_author']}
179
+
180
+ **Language Breakdown:**
181
+ - Start: {phase_data['languages_start']}
182
+ - End: {phase_data['languages_end']}
183
+
184
+ **Major Events:**
185
+ - Large Deletion: {phase_data['has_large_deletion']}
186
+ - Large Addition: {phase_data['has_large_addition']}
187
+ - Refactoring: {phase_data['has_refactor']}
188
+ - README Changed: {phase_data['readme_changed']}
189
+
190
+ **Commits Summary:**
191
+ {json.dumps(phase_data['commits'], indent=2)}
192
+
193
+ **Significant Commits (Large Changes/Refactors):**
194
+ {json.dumps(phase_data['significant_commits'], indent=2)}
195
+
196
+ **README Changes:**
197
+ {json.dumps(phase_data['readme_changes'], indent=2)}
198
+
199
+ **Comment Samples:**
200
+ {json.dumps(phase_data['comment_samples'], indent=2)}
201
+ """
202
+
203
+ if context:
204
+ prompt += f"\n**Context from Previous Phases:**\n{context}\n"
205
+
206
+ prompt += """
207
+ **Your Task:**
208
+ Write a concise narrative summary (3-5 paragraphs) that:
209
+
210
+ 1. Describes the main activities during this phase
211
+ 2. Explains major code additions, deletions, migrations, and cleanups
212
+ 3. Highlights how the README or documentation evolved
213
+ 4. Identifies themes from commit messages and comments (TODOs, deprecations, commentary)
214
+ 5. Explains the intent behind large diffs or refactorings
215
+ 6. Notes any significant architectural or technical decisions
216
+ 7. Maintains chronological flow while being concise
217
+
218
+ Focus on the "why" and "what changed" rather than just listing commits. Make it read like a story of the codebase's evolution.
219
+
220
+ Write the summary now:"""
221
+
222
+ return prompt
223
+
224
+ def _build_context(self, previous_summaries: List[Dict[str, Any]]) -> str:
225
+ """Build context string from previous phase summaries."""
226
+ if not previous_summaries:
227
+ return ""
228
+
229
+ context_parts = []
230
+ for summary_info in previous_summaries[-3:]: # Last 3 phases
231
+ context_parts.append(
232
+ f"Phase {summary_info['phase_number']} "
233
+ f"(LOC Δ: {summary_info['loc_delta']:+,d}): "
234
+ f"{summary_info['summary'][:200]}..."
235
+ )
236
+
237
+ return "\n\n".join(context_parts)
238
+
239
+ def _save_phase_with_summary(self, phase: Phase, output_dir: str):
240
+ """Save phase with updated summary."""
241
+ output_path = Path(output_dir)
242
+ output_path.mkdir(parents=True, exist_ok=True)
243
+
244
+ phase_file = output_path / f"phase_{phase.phase_number:02d}.json"
245
+ with open(phase_file, 'w') as f:
246
+ json.dump(phase.to_dict(), f, indent=2)
247
+
248
+
249
+ def summarize_phases(phases: List[Phase],
250
+ output_dir: str = "output/phases",
251
+ backend: Optional[str] = None,
252
+ model: Optional[str] = None,
253
+ api_key: Optional[str] = None,
254
+ **kwargs) -> List[Phase]:
255
+ """
256
+ Summarize all phases using LLM backend.
257
+
258
+ Args:
259
+ phases: List of Phase objects
260
+ output_dir: Directory to save updated phases
261
+ backend: LLM backend ('anthropic', 'openai', 'ollama')
262
+ model: Model identifier (uses backend defaults if not specified)
263
+ api_key: API key for the backend (if required)
264
+ **kwargs: Additional backend parameters
265
+
266
+ Returns:
267
+ List of Phase objects with summaries
268
+ """
269
+ summarizer = PhaseSummarizer(backend=backend, model=model, api_key=api_key, **kwargs)
270
+ return summarizer.summarize_all_phases(phases, output_dir)