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

1"""Analysis report module for session_start hook 

2 

3Handles session log analysis and daily report generation. 

4 

5Responsibilities: 

6- Generate daily analysis reports 

7- Analyze Claude Code session logs 

8- Format analysis results as markdown reports 

9""" 

10 

11import json 

12import logging 

13from datetime import datetime 

14from pathlib import Path 

15from typing import Any, Dict, Optional 

16 

17logger = logging.getLogger(__name__) 

18 

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" 

32 

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 

38 

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 } 

45 

46 

47class AnalysisError(Exception): 

48 """Exception raised for analysis-related errors""" 

49 

50 pass 

51 

52 

53def generate_daily_analysis(config: Dict[str, Any]) -> Optional[str]: 

54 """Generate daily session analysis report 

55 

56 Args: 

57 config: Configuration dictionary 

58 

59 Returns: 

60 Path to generated report file, or None if disabled/failed 

61 

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 

69 

70 # Analyze session logs 

71 report_path = analyze_session_logs(analysis_config) 

72 

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) 

79 

80 config_data["daily_analysis"]["last_analysis"] = ( 

81 datetime.now().strftime("%Y-%m-%d") 

82 ) 

83 

84 with open(config_file, "w", encoding="utf-8") as f: 

85 json.dump(config_data, f, indent=2, ensure_ascii=False) 

86 

87 return report_path 

88 

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 

92 

93 

94def analyze_session_logs(analysis_config: Dict[str, Any]) -> Optional[str]: 

95 """Analyze Claude Code session logs 

96 

97 Args: 

98 analysis_config: Analysis configuration 

99 

100 Returns: 

101 Path to generated report file, or None if no logs found 

102 

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 

110 

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) 

118 

119 if not project_sessions: 

120 logger.info("No session logs found") 

121 return None 

122 

123 # Analyze recent sessions (last 10) 

124 recent_sessions = sorted( 

125 project_sessions, key=lambda f: f.stat().st_mtime, reverse=True 

126 )[:10] 

127 

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 } 

137 

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 ) 

147 

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) 

154 

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 ) 

162 

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 ) 

172 

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 

183 

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 

190 

191 # Calculate duration statistics 

192 if all_durations: 

193 analysis_data["duration_stats"] = get_summary_stats(all_durations) 

194 

195 # Format and save report 

196 report_content = format_analysis_report(analysis_data) 

197 

198 # Save report to file 

199 base_path = Path(".moai/reports") 

200 base_path.mkdir(exist_ok=True, parents=True) 

201 

202 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 

203 report_file = base_path / f"daily-analysis-{timestamp}.md" 

204 

205 with open(report_file, "w", encoding="utf-8") as f: 

206 f.write(report_content) 

207 

208 logger.info(f"Daily analysis report saved: {report_file}") 

209 return str(report_file) 

210 

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 

214 

215 

216def format_analysis_report(analysis_data: Dict[str, Any]) -> str: 

217 """Format analysis results as markdown report 

218 

219 Args: 

220 analysis_data: Analysis data dictionary 

221 

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 ] 

235 

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") 

244 

245 report_lines.extend( 

246 [ 

247 "", 

248 "## ⚠️ Errors", 

249 "", 

250 ] 

251 ) 

252 

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") 

262 

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 ) 

278 

279 # Add recommendations 

280 report_lines.extend( 

281 [ 

282 "", 

283 "", 

284 "## 💡 Recommendations", 

285 "", 

286 ] 

287 ) 

288 

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 ) 

296 

297 if len(errors) > 3: 

298 report_lines.append("- ⚠️ Frequent errors detected. Stability review recommended.") 

299 

300 if duration_stats.get("mean", 0) > 1800: # >30 min 

301 report_lines.append("- ⏰ Long session duration. Consider breaking down tasks.") 

302 

303 if not report_lines[-1].startswith("-"): 

304 report_lines.append("- Current session pattern is good.") 

305 

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 ) 

315 

316 return "\n".join(report_lines)