Coverage for src / moai_adk / hooks / session_start / analysis_report.py: 0.00%
132 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-20 20:52 +0900
1"""Analysis report module for session_start hook
3Handles session log analysis and daily report generation.
5Responsibilities:
6- Generate daily analysis reports
7- Analyze Claude Code session logs
8- Format analysis results as markdown reports
9"""
11import json
12import logging
13from datetime import datetime
14from pathlib import Path
15from typing import Any, Dict, Optional
17logger = logging.getLogger(__name__)
19try:
20 from moai_adk.utils.common import format_duration, get_summary_stats
21except ImportError:
22 # Fallback implementations
23 def format_duration(seconds: float) -> str:
24 """Format duration in seconds to readable string"""
25 if seconds < 60:
26 return f"{seconds:.1f}s"
27 minutes = seconds / 60
28 if minutes < 60:
29 return f"{minutes:.1f}m"
30 hours = minutes / 60
31 return f"{hours:.1f}h"
33 def get_summary_stats(values: list) -> Dict[str, float]:
34 """Get summary statistics for a list of values"""
35 if not values:
36 return {"mean": 0, "min": 0, "max": 0, "std": 0}
37 import statistics
39 return {
40 "mean": statistics.mean(values),
41 "min": min(values),
42 "max": max(values),
43 "std": statistics.stdev(values) if len(values) > 1 else 0,
44 }
47class AnalysisError(Exception):
48 """Exception raised for analysis-related errors"""
50 pass
53def generate_daily_analysis(config: Dict[str, Any]) -> Optional[str]:
54 """Generate daily session analysis report
56 Args:
57 config: Configuration dictionary
59 Returns:
60 Path to generated report file, or None if disabled/failed
62 Raises:
63 AnalysisError: If analysis operations fail
64 """
65 try:
66 analysis_config = config.get("daily_analysis", {})
67 if not analysis_config.get("enabled", True):
68 return None
70 # Analyze session logs
71 report_path = analyze_session_logs(analysis_config)
73 # Update last analysis date in config
74 if report_path:
75 config_file = Path(".moai/config/config.json")
76 if config_file.exists():
77 with open(config_file, "r", encoding="utf-8") as f:
78 config_data = json.load(f)
80 config_data["daily_analysis"]["last_analysis"] = (
81 datetime.now().strftime("%Y-%m-%d")
82 )
84 with open(config_file, "w", encoding="utf-8") as f:
85 json.dump(config_data, f, indent=2, ensure_ascii=False)
87 return report_path
89 except Exception as e:
90 logger.error(f"Daily analysis failed: {e}")
91 raise AnalysisError(f"Failed to generate daily analysis: {e}") from e
94def analyze_session_logs(analysis_config: Dict[str, Any]) -> Optional[str]:
95 """Analyze Claude Code session logs
97 Args:
98 analysis_config: Analysis configuration
100 Returns:
101 Path to generated report file, or None if no logs found
103 Raises:
104 AnalysisError: If analysis operations fail
105 """
106 try:
107 # Find Claude Code session logs
108 session_logs_dir = Path.home() / ".claude" / "projects"
109 project_name = Path.cwd().name
111 # Collect sessions for current project
112 project_sessions = []
113 if session_logs_dir.exists():
114 for project_dir in session_logs_dir.iterdir():
115 if project_dir.is_dir() and project_dir.name.endswith(project_name):
116 session_files = list(project_dir.glob("session-*.json"))
117 project_sessions.extend(session_files)
119 if not project_sessions:
120 logger.info("No session logs found")
121 return None
123 # Analyze recent sessions (last 10)
124 recent_sessions = sorted(
125 project_sessions, key=lambda f: f.stat().st_mtime, reverse=True
126 )[:10]
128 # Collect analysis data
129 analysis_data = {
130 "total_sessions": len(recent_sessions),
131 "date_range": "",
132 "tools_used": {},
133 "errors_found": [],
134 "duration_stats": {},
135 "recommendations": [],
136 }
138 if recent_sessions:
139 first_session = datetime.fromtimestamp(
140 recent_sessions[-1].stat().st_mtime
141 )
142 last_session = datetime.fromtimestamp(recent_sessions[0].stat().st_mtime)
143 analysis_data["date_range"] = (
144 f"{first_session.strftime('%Y-%m-%d')} ~ "
145 f"{last_session.strftime('%Y-%m-%d')}"
146 )
148 # Analyze each session
149 all_durations = []
150 for session_file in recent_sessions:
151 try:
152 with open(session_file, "r", encoding="utf-8") as f:
153 session_data = json.load(f)
155 # Analyze tool usage
156 if "tool_use" in session_data:
157 for tool_use in session_data["tool_use"]:
158 tool_name = tool_use.get("name", "unknown")
159 analysis_data["tools_used"][tool_name] = (
160 analysis_data["tools_used"].get(tool_name, 0) + 1
161 )
163 # Collect errors
164 if "errors" in session_data:
165 for error in session_data["errors"]:
166 analysis_data["errors_found"].append(
167 {
168 "timestamp": error.get("timestamp", ""),
169 "error": error.get("message", "")[:100],
170 }
171 )
173 # Calculate session duration
174 if "start_time" in session_data and "end_time" in session_data:
175 start = session_data["start_time"]
176 end = session_data["end_time"]
177 if start and end:
178 try:
179 duration = float(end) - float(start)
180 all_durations.append(duration)
181 except (ValueError, TypeError):
182 pass
184 except json.JSONDecodeError as e:
185 logger.warning(f"Failed to parse session {session_file}: {e}")
186 continue
187 except Exception as e:
188 logger.warning(f"Failed to analyze session {session_file}: {e}")
189 continue
191 # Calculate duration statistics
192 if all_durations:
193 analysis_data["duration_stats"] = get_summary_stats(all_durations)
195 # Format and save report
196 report_content = format_analysis_report(analysis_data)
198 # Save report to file
199 base_path = Path(".moai/reports")
200 base_path.mkdir(exist_ok=True, parents=True)
202 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
203 report_file = base_path / f"daily-analysis-{timestamp}.md"
205 with open(report_file, "w", encoding="utf-8") as f:
206 f.write(report_content)
208 logger.info(f"Daily analysis report saved: {report_file}")
209 return str(report_file)
211 except Exception as e:
212 logger.error(f"Session log analysis failed: {e}")
213 raise AnalysisError(f"Failed to analyze session logs: {e}") from e
216def format_analysis_report(analysis_data: Dict[str, Any]) -> str:
217 """Format analysis results as markdown report
219 Args:
220 analysis_data: Analysis data dictionary
222 Returns:
223 Formatted markdown report content
224 """
225 report_lines = [
226 "# Daily Session Analysis Report",
227 "",
228 f"Generated at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
229 f"Analysis Period: {analysis_data.get('date_range', 'N/A')}",
230 f"Total Sessions: {analysis_data.get('total_sessions', 0)}",
231 "",
232 "## 📊 Tool Usage",
233 "",
234 ]
236 # Add tool usage
237 tools_used = analysis_data.get("tools_used", {})
238 if tools_used:
239 sorted_tools = sorted(tools_used.items(), key=lambda x: x[1], reverse=True)
240 for tool_name, count in sorted_tools[:10]: # TOP 10
241 report_lines.append(f"- **{tool_name}**: {count} times")
242 else:
243 report_lines.append("- No tools used")
245 report_lines.extend(
246 [
247 "",
248 "## ⚠️ Errors",
249 "",
250 ]
251 )
253 # Add error summary
254 errors = analysis_data.get("errors_found", [])
255 if errors:
256 for i, error in enumerate(errors[:5], 1): # Recent 5 errors
257 report_lines.append(
258 f"{i}. {error.get('error', 'N/A')} ({error.get('timestamp', 'N/A')})"
259 )
260 else:
261 report_lines.append("- No errors found")
263 # Add session duration statistics
264 duration_stats = analysis_data.get("duration_stats", {})
265 if duration_stats.get("mean", 0) > 0:
266 report_lines.extend(
267 [
268 "",
269 "",
270 "## ⏱️ Session Duration Statistics",
271 "",
272 f"- Mean: {format_duration(duration_stats['mean'])}",
273 f"- Min: {format_duration(duration_stats['min'])}",
274 f"- Max: {format_duration(duration_stats['max'])}",
275 f"- Std Dev: {format_duration(duration_stats['std'])}",
276 ]
277 )
279 # Add recommendations
280 report_lines.extend(
281 [
282 "",
283 "",
284 "## 💡 Recommendations",
285 "",
286 ]
287 )
289 # Tool usage based recommendations
290 if tools_used:
291 most_used_tool = max(tools_used.items(), key=lambda x: x[1])[0]
292 if "Bash" in most_used_tool and tools_used[most_used_tool] > 10:
293 report_lines.append(
294 "- 🔧 Frequent Bash command usage. Consider script automation."
295 )
297 if len(errors) > 3:
298 report_lines.append("- ⚠️ Frequent errors detected. Stability review recommended.")
300 if duration_stats.get("mean", 0) > 1800: # >30 min
301 report_lines.append("- ⏰ Long session duration. Consider breaking down tasks.")
303 if not report_lines[-1].startswith("-"):
304 report_lines.append("- Current session pattern is good.")
306 report_lines.extend(
307 [
308 "",
309 "---",
310 "---",
311 "*Report automatically generated by Alfred's SessionStart Hook*",
312 "*Analysis settings can be managed in `daily_analysis` section of `.moai/config/config.json`*",
313 ]
314 )
316 return "\n".join(report_lines)