Coverage for src/ollamapy/skillgen_report.py: 0%

192 statements  

« prev     ^ index     » next       coverage.py v7.10.6, created at 2025-09-01 12:29 -0400

1"""Navigatable documentation generation for skills with individual pages and comprehensive reporting.""" 

2 

3from typing import Dict, List, Any, Optional, Tuple 

4from datetime import datetime 

5import json 

6import os 

7from pathlib import Path 

8import plotly.graph_objects as go 

9from plotly.subplots import make_subplots 

10 

11 

12class SkillDocumentationGenerator: 

13 """Generates navigatable HTML documentation for all skills with individual pages.""" 

14 

15 def __init__(self, model: str = None, analysis_model: str = None, output_dir: str = "skill_docs"): 

16 """Initialize the documentation generator. 

17  

18 Args: 

19 model: The generation model used (optional) 

20 analysis_model: The analysis model used for vibe tests (optional) 

21 output_dir: Directory to save documentation files 

22 """ 

23 self.model = model or "N/A" 

24 self.analysis_model = analysis_model or "N/A" 

25 self.output_dir = Path(output_dir) 

26 self.timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") 

27 self.skills_data_dir = Path("src/ollamapy/skills_data") 

28 

29 # Create output directory if it doesn't exist 

30 self.output_dir.mkdir(parents=True, exist_ok=True) 

31 

32 def load_all_skills(self) -> Dict[str, Dict]: 

33 """Load all skills from the skills_data directory. 

34  

35 Returns: 

36 Dictionary of skill name to skill data 

37 """ 

38 skills = {} 

39 if self.skills_data_dir.exists(): 

40 for skill_file in self.skills_data_dir.glob("*.json"): 

41 try: 

42 with open(skill_file, 'r') as f: 

43 skill_data = json.load(f) 

44 skills[skill_data['name']] = skill_data 

45 except Exception as e: 

46 print(f"Error loading skill {skill_file}: {e}") 

47 return skills 

48 

49 def generate_skill_page(self, skill_data: Dict, is_new: bool = False) -> str: 

50 """Generate an individual HTML page for a skill. 

51  

52 Args: 

53 skill_data: The skill's data dictionary 

54 is_new: Whether this is a newly generated skill 

55  

56 Returns: 

57 HTML content for the skill page 

58 """ 

59 skill_name = skill_data.get('name', 'Unknown') 

60 description = skill_data.get('description', 'No description') 

61 role = skill_data.get('role', 'general') 

62 created_at = skill_data.get('created_at', 'Unknown') 

63 verified = skill_data.get('verified', False) 

64 

65 # Format vibe test phrases 

66 vibe_phrases_html = "" 

67 if skill_data.get('vibe_test_phrases'): 

68 vibe_phrases_html = "<ul>" 

69 for phrase in skill_data['vibe_test_phrases']: 

70 vibe_phrases_html += f"<li>{phrase}</li>" 

71 vibe_phrases_html += "</ul>" 

72 else: 

73 vibe_phrases_html = "<p>No vibe test phrases defined</p>" 

74 

75 # Format parameters 

76 params_html = "" 

77 if skill_data.get('parameters'): 

78 params_html = "<table class='params-table'>" 

79 params_html += "<tr><th>Parameter</th><th>Type</th><th>Required</th><th>Description</th></tr>" 

80 for param_name, param_info in skill_data['parameters'].items(): 

81 required = "✓" if param_info.get('required', False) else "" 

82 params_html += f""" 

83 <tr> 

84 <td><code>{param_name}</code></td> 

85 <td>{param_info.get('type', 'unknown')}</td> 

86 <td>{required}</td> 

87 <td>{param_info.get('description', '')}</td> 

88 </tr> 

89 """ 

90 params_html += "</table>" 

91 else: 

92 params_html = "<p>No parameters required</p>" 

93 

94 # Format code with syntax highlighting 

95 code = skill_data.get('function_code', 'No code available') 

96 code_html = f"<pre><code class='language-python'>{self.escape_html(code)}</code></pre>" 

97 

98 # Badge for new skills 

99 new_badge = '<span class="badge-new">NEW</span>' if is_new else '' 

100 verified_badge = '<span class="badge-verified">VERIFIED</span>' if verified else '<span class="badge-unverified">UNVERIFIED</span>' 

101 

102 # Edit button (only for non-verified skills) 

103 if not verified: 

104 disabled_attr = "disabled" if verified else "" 

105 button_text = "🔒 Protected" if verified else "✏️ Edit Skill" 

106 edit_button = f'<button class="edit-btn" onclick="editSkill()" {disabled_attr}>{button_text}</button>' 

107 else: 

108 edit_button = '<span class="protected-note">🔒 Built-in skills cannot be edited</span>' 

109 

110 common_styles = self.get_common_styles() 

111 newline = '\n' 

112 return f"""<!DOCTYPE html> 

113<html lang="en"> 

114<head> 

115 <meta charset="utf-8"> 

116 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 

117 <title>{skill_name} - Skill Documentation</title> 

118 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css"> 

119 <style> 

120 {common_styles} 

121 .skill-header {{ 

122 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 

123 color: white; 

124 padding: 40px; 

125 border-radius: 15px 15px 0 0; 

126 margin: -40px -40px 30px -40px; 

127 }} 

128 .skill-title {{ 

129 font-size: 2.5em; 

130 margin-bottom: 10px; 

131 display: flex; 

132 align-items: center; 

133 gap: 15px; 

134 }} 

135 .badge-new {{ 

136 background: #ffc107; 

137 color: #000; 

138 padding: 5px 10px; 

139 border-radius: 20px; 

140 font-size: 0.4em; 

141 font-weight: bold; 

142 }} 

143 .badge-verified {{ 

144 background: #28a745; 

145 color: white; 

146 padding: 5px 10px; 

147 border-radius: 20px; 

148 font-size: 0.4em; 

149 }} 

150 .badge-unverified {{ 

151 background: #dc3545; 

152 color: white; 

153 padding: 5px 10px; 

154 border-radius: 20px; 

155 font-size: 0.4em; 

156 }} 

157 .skill-meta {{ 

158 display: flex; 

159 gap: 30px; 

160 opacity: 0.9; 

161 flex-wrap: wrap; 

162 }} 

163 .meta-item {{ 

164 display: flex; 

165 flex-direction: column; 

166 }} 

167 .meta-label {{ 

168 font-size: 0.9em; 

169 opacity: 0.8; 

170 }} 

171 .meta-value {{ 

172 font-size: 1.1em; 

173 font-weight: bold; 

174 }} 

175 .section {{ 

176 margin: 30px 0; 

177 padding: 25px; 

178 background: #f8f9fa; 

179 border-radius: 10px; 

180 border-left: 4px solid #667eea; 

181 }} 

182 .section h2 {{ 

183 margin-top: 0; 

184 color: #333; 

185 }} 

186 .params-table {{ 

187 width: 100%; 

188 border-collapse: collapse; 

189 margin-top: 15px; 

190 }} 

191 .params-table th {{ 

192 background: #667eea; 

193 color: white; 

194 padding: 10px; 

195 text-align: left; 

196 }} 

197 .params-table td {{ 

198 padding: 10px; 

199 border-bottom: 1px solid #ddd; 

200 }} 

201 .params-table code {{ 

202 background: #e9ecef; 

203 padding: 2px 6px; 

204 border-radius: 3px; 

205 font-family: 'Courier New', monospace; 

206 }} 

207 pre {{ 

208 background: #2d2d2d; 

209 padding: 20px; 

210 border-radius: 8px; 

211 overflow-x: auto; 

212 }} 

213 pre code {{ 

214 color: #f8f8f2; 

215 font-family: 'Courier New', monospace; 

216 font-size: 14px; 

217 }} 

218 .nav-buttons {{ 

219 display: flex; 

220 justify-content: space-between; 

221 margin-top: 40px; 

222 padding-top: 20px; 

223 border-top: 2px solid #e9ecef; 

224 }} 

225 .nav-button {{ 

226 background: #667eea; 

227 color: white; 

228 padding: 10px 20px; 

229 border-radius: 5px; 

230 text-decoration: none; 

231 transition: background 0.3s; 

232 }} 

233 .nav-button:hover {{ 

234 background: #764ba2; 

235 }} 

236 .edit-btn {{ 

237 background: #28a745; 

238 color: white; 

239 border: none; 

240 padding: 12px 24px; 

241 border-radius: 6px; 

242 cursor: pointer; 

243 font-size: 16px; 

244 margin-right: 10px; 

245 transition: background 0.2s; 

246 }} 

247 .edit-btn:hover:not(:disabled) {{ 

248 background: #218838; 

249 }} 

250 .edit-btn:disabled {{ 

251 background: #6c757d; 

252 cursor: not-allowed; 

253 }} 

254 .protected-note {{ 

255 color: #6c757d; 

256 font-style: italic; 

257 padding: 12px 0; 

258 }} 

259 .edit-panel {{ 

260 display: none; 

261 background: #fff; 

262 border: 2px solid #667eea; 

263 border-radius: 10px; 

264 padding: 30px; 

265 margin: 30px 0; 

266 }} 

267 .form-group {{ 

268 margin-bottom: 20px; 

269 }} 

270 .form-group label {{ 

271 display: block; 

272 margin-bottom: 8px; 

273 font-weight: 600; 

274 color: #333; 

275 }} 

276 .form-control {{ 

277 width: 100%; 

278 padding: 12px; 

279 border: 2px solid #e2e8f0; 

280 border-radius: 6px; 

281 font-size: 14px; 

282 transition: border-color 0.2s; 

283 box-sizing: border-box; 

284 }} 

285 .form-control:focus {{ 

286 outline: none; 

287 border-color: #667eea; 

288 }} 

289 .form-control.code {{ 

290 font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; 

291 resize: vertical; 

292 }} 

293 .btn-save {{ 

294 background: #28a745; 

295 color: white; 

296 border: none; 

297 padding: 12px 24px; 

298 border-radius: 6px; 

299 cursor: pointer; 

300 margin-right: 10px; 

301 }} 

302 .btn-cancel {{ 

303 background: #6c757d; 

304 color: white; 

305 border: none; 

306 padding: 12px 24px; 

307 border-radius: 6px; 

308 cursor: pointer; 

309 }} 

310 .btn-test {{ 

311 background: #17a2b8; 

312 color: white; 

313 border: none; 

314 padding: 12px 24px; 

315 border-radius: 6px; 

316 cursor: pointer; 

317 margin-right: 10px; 

318 }} 

319 .message {{ 

320 padding: 15px; 

321 border-radius: 6px; 

322 margin: 15px 0; 

323 }} 

324 .message.success {{ 

325 background: #d4edda; 

326 color: #155724; 

327 border: 1px solid #c3e6cb; 

328 }} 

329 .message.error {{ 

330 background: #f8d7da; 

331 color: #721c24; 

332 border: 1px solid #f5c6cb; 

333 }} 

334 .test-output {{ 

335 background: #2d3748; 

336 color: #e2e8f0; 

337 padding: 15px; 

338 border-radius: 6px; 

339 font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; 

340 white-space: pre-wrap; 

341 margin-top: 10px; 

342 max-height: 300px; 

343 overflow-y: auto; 

344 display: none; 

345 }} 

346 </style> 

347</head> 

348<body> 

349 <div class="container"> 

350 <div class="skill-header"> 

351 <h1 class="skill-title"> 

352 {skill_name} 

353 {new_badge} 

354 {verified_badge} 

355 </h1> 

356 <p style="font-size: 1.2em; margin: 10px 0;">{description}</p> 

357 <div class="skill-meta"> 

358 <div class="meta-item"> 

359 <span class="meta-label">Role</span> 

360 <span class="meta-value">{role}</span> 

361 </div> 

362 <div class="meta-item"> 

363 <span class="meta-label">Created</span> 

364 <span class="meta-value">{created_at[:10] if len(created_at) > 10 else created_at}</span> 

365 </div> 

366 <div class="meta-item"> 

367 <span class="meta-label">Execution Count</span> 

368 <span class="meta-value">{skill_data.get('execution_count', 0)}</span> 

369 </div> 

370 <div class="meta-item"> 

371 <span class="meta-label">Success Rate</span> 

372 <span class="meta-value">{skill_data.get('success_rate', 0):.1f}%</span> 

373 </div> 

374 </div> 

375 </div> 

376  

377 <div style="text-align: center; margin: 30px 0;"> 

378 {edit_button} 

379 </div> 

380  

381 <div id="edit-panel" class="edit-panel"> 

382 <h2>✏️ Edit Skill</h2> 

383 <div id="message-area"></div> 

384 <form id="edit-form"> 

385 <div class="form-group"> 

386 <label>Description</label> 

387 <textarea id="edit-description" class="form-control" rows="3">{description}</textarea> 

388 </div> 

389  

390 <div class="form-group"> 

391 <label>Role</label> 

392 <select id="edit-role" class="form-control"> 

393 <option value="general" {"selected" if role == "general" else ""}>General</option> 

394 <option value="text_processing" {"selected" if role == "text_processing" else ""}>Text Processing</option> 

395 <option value="mathematics" {"selected" if role == "mathematics" else ""}>Mathematics</option> 

396 <option value="data_analysis" {"selected" if role == "data_analysis" else ""}>Data Analysis</option> 

397 <option value="file_operations" {"selected" if role == "file_operations" else ""}>File Operations</option> 

398 <option value="web_utilities" {"selected" if role == "web_utilities" else ""}>Web Utilities</option> 

399 <option value="time_date" {"selected" if role == "time_date" else ""}>Time & Date</option> 

400 <option value="formatting" {"selected" if role == "formatting" else ""}>Formatting</option> 

401 <option value="validation" {"selected" if role == "validation" else ""}>Validation</option> 

402 <option value="emotional_response" {"selected" if role == "emotional_response" else ""}>Emotional Response</option> 

403 <option value="information" {"selected" if role == "information" else ""}>Information</option> 

404 <option value="advanced" {"selected" if role == "advanced" else ""}>Advanced</option> 

405 </select> 

406 </div> 

407  

408 <div class="form-group"> 

409 <label>Vibe Test Phrases (one per line)</label> 

410 <textarea id="edit-vibe-phrases" class="form-control" rows="5">{newline.join(skill_data.get('vibe_test_phrases', []))}</textarea> 

411 </div> 

412  

413 <div class="form-group"> 

414 <label>Function Code</label> 

415 <textarea id="edit-function-code" class="form-control code" rows="15">{self.escape_html(skill_data.get('function_code', ''))}</textarea> 

416 </div> 

417  

418 <div style="margin: 20px 0;"> 

419 <button type="button" class="btn-test" onclick="testSkill()">🧪 Test Skill</button> 

420 <div id="test-output" class="test-output"></div> 

421 </div> 

422  

423 <div> 

424 <button type="submit" class="btn-save">💾 Save Changes</button> 

425 <button type="button" class="btn-cancel" onclick="cancelEdit()">❌ Cancel</button> 

426 </div> 

427 </form> 

428 </div> 

429  

430 <div id="view-panel"> 

431 <div class="section"> 

432 <h2>📝 Description</h2> 

433 <p>{description}</p> 

434 </div> 

435  

436 <div class="section"> 

437 <h2>🧪 Vibe Test Phrases</h2> 

438 <p>These phrases should trigger this skill:</p> 

439 {vibe_phrases_html} 

440 </div> 

441  

442 <div class="section"> 

443 <h2>⚙️ Parameters</h2> 

444 {params_html} 

445 </div> 

446  

447 <div class="section"> 

448 <h2>💻 Implementation</h2> 

449 {code_html} 

450 </div> 

451 </div> 

452  

453 <div class="nav-buttons"> 

454 <a href="index.html" class="nav-button">← Back to Index</a> 

455 </div> 

456 </div> 

457 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script> 

458 <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script> 

459  

460 <script> 

461 const skillData = {json.dumps(skill_data, indent=2)}; 

462 const isBuiltIn = {str(verified).lower()}; 

463  

464 function editSkill() {{ 

465 if (isBuiltIn) {{ 

466 showMessage('Built-in skills cannot be edited', 'error'); 

467 return; 

468 }} 

469 document.getElementById('view-panel').style.display = 'none'; 

470 document.getElementById('edit-panel').style.display = 'block'; 

471 }} 

472  

473 function cancelEdit() {{ 

474 document.getElementById('edit-panel').style.display = 'none'; 

475 document.getElementById('view-panel').style.display = 'block'; 

476 clearMessage(); 

477 }} 

478  

479 async function testSkill() {{ 

480 const formData = collectFormData(); 

481  

482 try {{ 

483 const response = await fetch('http://localhost:5000/api/skills/test', {{ 

484 method: 'POST', 

485 headers: {{'Content-Type': 'application/json'}}, 

486 body: JSON.stringify({{ 

487 skill_data: formData, 

488 test_input: {{}} 

489 }}) 

490 }}); 

491  

492 const data = await response.json(); 

493 const output = document.getElementById('test-output'); 

494  

495 if (data.success) {{ 

496 if (data.execution_successful) {{ 

497 output.textContent = 'Test passed!' + newline + newline + 'Output:' + newline + data.output.join(newline); 

498 output.style.background = '#2d5a27'; 

499 }} else {{ 

500 output.textContent = 'Test failed:' + newline + data.error; 

501 output.style.background = '#8b2635'; 

502 }} 

503 }} else {{ 

504 output.textContent = 'Error: ' + data.error; 

505 output.style.background = '#8b2635'; 

506 }} 

507  

508 output.style.display = 'block'; 

509 }} catch (error) {{ 

510 const output = document.getElementById('test-output'); 

511 output.textContent = 'Network error: ' + error.message; 

512 output.style.background = '#8b2635'; 

513 output.style.display = 'block'; 

514 }} 

515 }} 

516  

517 function collectFormData() {{ 

518 const vibePhrasesText = document.getElementById('edit-vibe-phrases').value; 

519 return {{ 

520 name: skillData.name, 

521 description: document.getElementById('edit-description').value, 

522 role: document.getElementById('edit-role').value, 

523 vibe_test_phrases: vibePhrasesText.split(newline).filter(p => p.trim()), 

524 parameters: skillData.parameters || {{}}, 

525 function_code: document.getElementById('edit-function-code').value, 

526 verified: skillData.verified, 

527 scope: skillData.scope || 'local', 

528 tags: skillData.tags || [], 

529 created_at: skillData.created_at, 

530 execution_count: skillData.execution_count || 0, 

531 success_rate: skillData.success_rate || 100.0, 

532 average_execution_time: skillData.average_execution_time || 0.0 

533 }}; 

534 }} 

535  

536 async function saveSkill() {{ 

537 if (isBuiltIn) {{ 

538 showMessage('Built-in skills cannot be edited', 'error'); 

539 return; 

540 }} 

541  

542 const formData = collectFormData(); 

543 formData.last_modified = new Date().toISOString(); 

544  

545 try {{ 

546 const response = await fetch(`http://localhost:5000/api/skills/${{skillData.name}}`, {{ 

547 method: 'PUT', 

548 headers: {{'Content-Type': 'application/json'}}, 

549 body: JSON.stringify(formData) 

550 }}); 

551  

552 const data = await response.json(); 

553  

554 if (data.success) {{ 

555 showMessage('Skill updated successfully! Refresh the page to see changes.', 'success'); 

556 // Update local skill data 

557 Object.assign(skillData, formData); 

558 }} else {{ 

559 showMessage('Failed to update skill: ' + data.error, 'error'); 

560 if (data.validation_errors) {{ 

561 showMessage('Validation errors: ' + data.validation_errors.join(', '), 'error'); 

562 }} 

563 }} 

564 }} catch (error) {{ 

565 showMessage('Network error: ' + error.message, 'error'); 

566 }} 

567 }} 

568  

569 function showMessage(message, type) {{ 

570 const area = document.getElementById('message-area'); 

571 area.innerHTML = `<div class="message ${{type}}">${{message}}</div>`; 

572 setTimeout(() => area.innerHTML = '', 5000); 

573 }} 

574  

575 function clearMessage() {{ 

576 document.getElementById('message-area').innerHTML = ''; 

577 }} 

578  

579 // Form submission handler 

580 document.getElementById('edit-form').addEventListener('submit', function(e) {{ 

581 e.preventDefault(); 

582 saveSkill(); 

583 }}); 

584  

585 // Check if skill editor API is available 

586 async function checkAPIAvailability() {{ 

587 try {{ 

588 await fetch('http://localhost:5000/api/skills', {{method: 'HEAD'}}); 

589 }} catch (error) {{ 

590 console.warn('Skill editor API not available. Interactive editing disabled.'); 

591 const editBtn = document.querySelector('.edit-btn'); 

592 if (editBtn && !isBuiltIn) {{ 

593 editBtn.disabled = true; 

594 editBtn.textContent = '🔌 API Offline'; 

595 editBtn.title = 'Start the skill editor server to enable editing'; 

596 }} 

597 }} 

598 }} 

599  

600 // Check API availability on page load 

601 checkAPIAvailability(); 

602 </script> 

603</body> 

604</html>""" 

605 

606 def generate_index_page(self, all_skills: Dict[str, Dict], new_skills: List[str], 

607 generation_results: List[Dict] = None) -> str: 

608 """Generate the main index page with links to all skills. 

609  

610 Args: 

611 all_skills: Dictionary of all skills 

612 new_skills: List of newly generated skill names 

613 generation_results: Optional list of generation results for reporting 

614  

615 Returns: 

616 HTML content for the index page 

617 """ 

618 # Group skills by role 

619 skills_by_role = {} 

620 for skill_name, skill_data in all_skills.items(): 

621 role = skill_data.get('role', 'general') 

622 if role not in skills_by_role: 

623 skills_by_role[role] = [] 

624 skills_by_role[role].append((skill_name, skill_data)) 

625 

626 # Sort skills within each role 

627 for role in skills_by_role: 

628 skills_by_role[role].sort(key=lambda x: x[0]) 

629 

630 # Generate skills listing HTML 

631 skills_html = "" 

632 for role in sorted(skills_by_role.keys()): 

633 role_emoji = self.get_role_emoji(role) 

634 role_title = role.replace('_', ' ').title() 

635 skills_html += f""" 

636 <div class="role-section"> 

637 <h2>{role_emoji} {role_title}</h2> 

638 <div class="skills-grid"> 

639 """ 

640 

641 for skill_name, skill_data in skills_by_role[role]: 

642 is_new = skill_name in new_skills 

643 verified = skill_data.get('verified', False) 

644 description = skill_data.get('description', 'No description')[:100] 

645 if len(skill_data.get('description', '')) > 100: 

646 description += '...' 

647 

648 new_badge = '<span class="badge new">NEW</span>' if is_new else '' 

649 verified_badge = '<span class="badge verified">✓</span>' if verified else '' 

650 

651 skills_html += f""" 

652 <a href="{skill_name}.html" class="skill-card {'new-skill' if is_new else ''}"> 

653 <div class="skill-card-header"> 

654 <h3>{skill_name}</h3> 

655 <div class="badges"> 

656 {new_badge} 

657 {verified_badge} 

658 </div> 

659 </div> 

660 <p>{description}</p> 

661 </a> 

662 """ 

663 

664 skills_html += """ 

665 </div> 

666 </div> 

667 """ 

668 

669 # Generate statistics 

670 total_skills = len(all_skills) 

671 new_count = len(new_skills) 

672 verified_count = sum(1 for s in all_skills.values() if s.get('verified', False)) 

673 

674 # Generate charts if we have generation results 

675 charts_html = "" 

676 if generation_results: 

677 charts_html = self.generate_report_charts(generation_results) 

678 

679 common_styles = self.get_common_styles() 

680 return f"""<!DOCTYPE html> 

681<html lang="en"> 

682<head> 

683 <meta charset="utf-8"> 

684 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 

685 <title>OllamaPy Skills Documentation</title> 

686 <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> 

687 <style> 

688 {common_styles} 

689 .header {{ 

690 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); 

691 color: white; 

692 padding: 60px 40px; 

693 border-radius: 15px; 

694 margin-bottom: 40px; 

695 text-align: center; 

696 }} 

697 .header h1 {{ 

698 font-size: 3em; 

699 margin-bottom: 20px; 

700 }} 

701 .stats {{ 

702 display: flex; 

703 justify-content: center; 

704 gap: 40px; 

705 margin-top: 30px; 

706 flex-wrap: wrap; 

707 }} 

708 .stat {{ 

709 background: rgba(255, 255, 255, 0.2); 

710 padding: 20px 30px; 

711 border-radius: 10px; 

712 }} 

713 .stat-value {{ 

714 font-size: 2.5em; 

715 font-weight: bold; 

716 }} 

717 .stat-label {{ 

718 font-size: 1.1em; 

719 opacity: 0.9; 

720 }} 

721 .role-section {{ 

722 margin: 40px 0; 

723 }} 

724 .role-section h2 {{ 

725 color: #333; 

726 border-bottom: 2px solid #667eea; 

727 padding-bottom: 10px; 

728 margin-bottom: 20px; 

729 }} 

730 .skills-grid {{ 

731 display: grid; 

732 grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 

733 gap: 20px; 

734 }} 

735 .skill-card {{ 

736 background: #f8f9fa; 

737 padding: 20px; 

738 border-radius: 10px; 

739 text-decoration: none; 

740 color: #333; 

741 transition: all 0.3s; 

742 border: 2px solid transparent; 

743 display: block; 

744 }} 

745 .skill-card:hover {{ 

746 transform: translateY(-5px); 

747 box-shadow: 0 10px 30px rgba(0,0,0,0.1); 

748 border-color: #667eea; 

749 }} 

750 .skill-card.new-skill {{ 

751 background: linear-gradient(135deg, #fff9e6 0%, #fffbf0 100%); 

752 border-color: #ffc107; 

753 }} 

754 .skill-card-header {{ 

755 display: flex; 

756 justify-content: space-between; 

757 align-items: center; 

758 margin-bottom: 10px; 

759 }} 

760 .skill-card h3 {{ 

761 margin: 0; 

762 color: #667eea; 

763 }} 

764 .skill-card p {{ 

765 margin: 0; 

766 color: #666; 

767 font-size: 0.95em; 

768 }} 

769 .badges {{ 

770 display: flex; 

771 gap: 5px; 

772 }} 

773 .badge {{ 

774 padding: 3px 8px; 

775 border-radius: 12px; 

776 font-size: 0.75em; 

777 font-weight: bold; 

778 }} 

779 .badge.new {{ 

780 background: #ffc107; 

781 color: #000; 

782 }} 

783 .badge.verified {{ 

784 background: #28a745; 

785 color: white; 

786 }} 

787 .search-box {{ 

788 margin: 30px 0; 

789 text-align: center; 

790 }} 

791 .search-box input {{ 

792 width: 100%; 

793 max-width: 500px; 

794 padding: 15px 20px; 

795 font-size: 1.1em; 

796 border: 2px solid #667eea; 

797 border-radius: 50px; 

798 outline: none; 

799 }} 

800 .search-box input:focus {{ 

801 box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); 

802 }} 

803 .generation-report {{ 

804 margin: 40px 0; 

805 padding: 30px; 

806 background: #f8f9fa; 

807 border-radius: 10px; 

808 }} 

809 .generation-report h2 {{ 

810 color: #333; 

811 margin-bottom: 20px; 

812 }} 

813 </style> 

814</head> 

815<body> 

816 <div class="container"> 

817 <div class="header"> 

818 <h1>🤖 OllamaPy Skills Documentation</h1> 

819 <p style="font-size: 1.2em;">Comprehensive documentation for all available AI skills</p> 

820 <div class="stats"> 

821 <div class="stat"> 

822 <div class="stat-value">{total_skills}</div> 

823 <div class="stat-label">Total Skills</div> 

824 </div> 

825 <div class="stat"> 

826 <div class="stat-value">{new_count}</div> 

827 <div class="stat-label">New Skills</div> 

828 </div> 

829 <div class="stat"> 

830 <div class="stat-value">{verified_count}</div> 

831 <div class="stat-label">Verified</div> 

832 </div> 

833 </div> 

834 </div> 

835  

836 <div class="search-box"> 

837 <input type="text" id="skillSearch" placeholder="Search skills..." onkeyup="filterSkills()"> 

838 </div> 

839  

840 {charts_html} 

841  

842 {skills_html} 

843  

844 <div class="footer"> 

845 <p>Generated: {self.timestamp}</p> 

846 <p>Models: {self.model} (generation) | {self.analysis_model} (analysis)</p> 

847 </div> 

848 </div> 

849  

850 <script> 

851 function filterSkills() {{ 

852 const input = document.getElementById('skillSearch'); 

853 const filter = input.value.toLowerCase(); 

854 const cards = document.getElementsByClassName('skill-card'); 

855  

856 for (let card of cards) {{ 

857 const text = card.textContent.toLowerCase(); 

858 card.style.display = text.includes(filter) ? '' : 'none'; 

859 }} 

860 }} 

861 </script> 

862</body> 

863</html>""" 

864 

865 def generate_error_report(self, failed_results: List[Dict]) -> str: 

866 """Generate a separate error report for failed generations. 

867  

868 Args: 

869 failed_results: List of failed generation results 

870  

871 Returns: 

872 HTML content for the error report 

873 """ 

874 if not failed_results: 

875 return "" 

876 

877 errors_html = "" 

878 for i, result in enumerate(failed_results, 1): 

879 errors = result.get('errors', ['Unknown error']) 

880 step_results = result.get('step_results', {}) 

881 

882 # Determine where it failed 

883 failure_point = "Unknown" 

884 if not step_results.get('plan_created'): 

885 failure_point = "Plan Creation" 

886 elif not step_results.get('validation_passed'): 

887 failure_point = "Validation" 

888 elif not step_results.get('skill_registered'): 

889 failure_point = "Registration" 

890 elif not step_results.get('vibe_test_passed'): 

891 failure_point = "Vibe Test" 

892 

893 errors_html += f""" 

894 <div class="error-card"> 

895 <h3>Failed Generation #{i}</h3> 

896 <div class="error-details"> 

897 <p><strong>Failure Point:</strong> {failure_point}</p> 

898 <p><strong>Attempts:</strong> {result.get('attempts', 1)}</p> 

899 <p><strong>Time:</strong> {result.get('generation_time', 0):.1f}s</p> 

900 <p><strong>Errors:</strong></p> 

901 <ul class="error-list"> 

902 """ 

903 

904 for error in errors: 

905 errors_html += f"<li>{error}</li>" 

906 

907 errors_html += """ 

908 </ul> 

909 </div> 

910 """ 

911 

912 # Add plan details if available 

913 if result.get('plan'): 

914 plan = result['plan'] 

915 errors_html += f""" 

916 <div class="plan-details"> 

917 <h4>Attempted Skill Plan:</h4> 

918 <p><strong>Idea:</strong> {plan.get('idea', 'N/A')}</p> 

919 <p><strong>Name:</strong> {plan.get('name', 'N/A')}</p> 

920 <p><strong>Description:</strong> {plan.get('description', 'N/A')}</p> 

921 <p><strong>Role:</strong> {plan.get('role', 'N/A')}</p> 

922 </div> 

923 """ 

924 

925 errors_html += "</div>" 

926 

927 common_styles = self.get_common_styles() 

928 return f"""<!DOCTYPE html> 

929<html lang="en"> 

930<head> 

931 <meta charset="utf-8"> 

932 <meta name="viewport" content="width=device-width, initial-scale=1.0"> 

933 <title>Skill Generation Error Report</title> 

934 <style> 

935 {common_styles} 

936 .header {{ 

937 background: linear-gradient(135deg, #dc3545 0%, #c82333 100%); 

938 color: white; 

939 padding: 40px; 

940 border-radius: 15px; 

941 margin-bottom: 30px; 

942 text-align: center; 

943 }} 

944 .error-card {{ 

945 background: #fff3cd; 

946 border-left: 4px solid #ffc107; 

947 padding: 20px; 

948 margin: 20px 0; 

949 border-radius: 5px; 

950 }} 

951 .error-card h3 {{ 

952 margin-top: 0; 

953 color: #856404; 

954 }} 

955 .error-details {{ 

956 margin: 15px 0; 

957 }} 

958 .error-list {{ 

959 background: white; 

960 padding: 10px 10px 10px 30px; 

961 border-radius: 5px; 

962 margin: 10px 0; 

963 }} 

964 .plan-details {{ 

965 background: white; 

966 padding: 15px; 

967 border-radius: 5px; 

968 margin-top: 15px; 

969 }} 

970 .plan-details h4 {{ 

971 margin-top: 0; 

972 color: #495057; 

973 }} 

974 .summary {{ 

975 background: #f8f9fa; 

976 padding: 20px; 

977 border-radius: 10px; 

978 margin: 30px 0; 

979 }} 

980 .nav-button {{ 

981 background: #667eea; 

982 color: white; 

983 padding: 10px 20px; 

984 border-radius: 5px; 

985 text-decoration: none; 

986 display: inline-block; 

987 margin-top: 20px; 

988 }} 

989 .nav-button:hover {{ 

990 background: #764ba2; 

991 }} 

992 </style> 

993</head> 

994<body> 

995 <div class="container"> 

996 <div class="header"> 

997 <h1>❌ Skill Generation Error Report</h1> 

998 <p>Analysis of failed skill generation attempts</p> 

999 </div> 

1000  

1001 <div class="summary"> 

1002 <h2>Summary</h2> 

1003 <p><strong>Total Failed Attempts:</strong> {len(failed_results)}</p> 

1004 <p><strong>Generated:</strong> {self.timestamp}</p> 

1005 <p>This report contains details about skill generation attempts that failed,  

1006 including the failure points and error messages to help improve future generations.</p> 

1007 </div> 

1008  

1009 {errors_html} 

1010  

1011 <a href="index.html" class="nav-button">← Back to Skills Documentation</a> 

1012 </div> 

1013</body> 

1014</html>""" 

1015 

1016 def generate_report_charts(self, generation_results: List[Dict]) -> str: 

1017 """Generate charts for the generation report. 

1018  

1019 Args: 

1020 generation_results: List of generation results 

1021  

1022 Returns: 

1023 HTML containing the charts 

1024 """ 

1025 if not generation_results: 

1026 return "" 

1027 

1028 # Prepare data 

1029 successful = sum(1 for r in generation_results if r.get('success', False)) 

1030 failed = len(generation_results) - successful 

1031 

1032 # Success rate pie chart 

1033 fig1 = go.Figure(data=[go.Pie( 

1034 labels=['Successful', 'Failed'], 

1035 values=[successful, failed], 

1036 hole=0.3, 

1037 marker=dict(colors=['#28a745', '#dc3545']) 

1038 )]) 

1039 fig1.update_layout( 

1040 title="Generation Success Rate", 

1041 height=300, 

1042 showlegend=True 

1043 ) 

1044 

1045 chart1_html = fig1.to_html(full_html=False, include_plotlyjs=False, div_id="success-pie") 

1046 

1047 return f""" 

1048 <div class="generation-report"> 

1049 <h2>📊 Latest Generation Report</h2> 

1050 <div class="chart-container"> 

1051 {chart1_html} 

1052 </div> 

1053 <p style="text-align: center; margin-top: 20px;"> 

1054 <a href="error_report.html" style="color: #dc3545;">View detailed error report →</a> 

1055 </p> 

1056 </div> 

1057 """ 

1058 

1059 def get_role_emoji(self, role: str) -> str: 

1060 """Get emoji for a skill role.""" 

1061 emojis = { 

1062 'text_processing': '📝', 

1063 'mathematics': '🔢', 

1064 'data_analysis': '📊', 

1065 'file_operations': '📁', 

1066 'web_utilities': '🌐', 

1067 'time_date': '⏰', 

1068 'formatting': '✨', 

1069 'validation': '✅', 

1070 'general': '🔧' 

1071 } 

1072 return emojis.get(role, '🔧') 

1073 

1074 def get_common_styles(self) -> str: 

1075 """Get common CSS styles used across all pages.""" 

1076 return """ 

1077 body { 

1078 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; 

1079 margin: 0; 

1080 padding: 0; 

1081 background: #f5f5f5; 

1082 min-height: 100vh; 

1083 } 

1084 .container { 

1085 max-width: 1200px; 

1086 margin: 0 auto; 

1087 padding: 40px 20px; 

1088 background: white; 

1089 min-height: 100vh; 

1090 } 

1091 .footer { 

1092 text-align: center; 

1093 margin-top: 60px; 

1094 padding-top: 20px; 

1095 border-top: 2px solid #e9ecef; 

1096 color: #6c757d; 

1097 } 

1098 .chart-container { 

1099 margin: 20px 0; 

1100 } 

1101 """ 

1102 

1103 def escape_html(self, text: str) -> str: 

1104 """Escape HTML special characters.""" 

1105 return text.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&#39;') 

1106 

1107 def generate_documentation(self, generation_results: List[Dict] = None) -> str: 

1108 """Generate complete documentation for all skills. 

1109  

1110 Args: 

1111 generation_results: Optional list of recent generation results 

1112  

1113 Returns: 

1114 Path to the generated documentation 

1115 """ 

1116 # Load all skills 

1117 all_skills = self.load_all_skills() 

1118 

1119 # Identify new skills from generation results 

1120 new_skills = [] 

1121 failed_results = [] 

1122 

1123 if generation_results: 

1124 for result in generation_results: 

1125 if result.get('success') and result.get('skill'): 

1126 skill = result['skill'] 

1127 skill_name = skill.get('name') or (skill.name if hasattr(skill, 'name') else 'unknown') 

1128 new_skills.append(skill_name) 

1129 elif not result.get('success'): 

1130 failed_results.append(result) 

1131 

1132 # Generate individual skill pages 

1133 for skill_name, skill_data in all_skills.items(): 

1134 is_new = skill_name in new_skills 

1135 skill_html = self.generate_skill_page(skill_data, is_new) 

1136 skill_file = self.output_dir / f"{skill_name}.html" 

1137 with open(skill_file, 'w', encoding='utf-8') as f: 

1138 f.write(skill_html) 

1139 

1140 # Generate index page 

1141 index_html = self.generate_index_page(all_skills, new_skills, generation_results) 

1142 index_file = self.output_dir / "index.html" 

1143 with open(index_file, 'w', encoding='utf-8') as f: 

1144 f.write(index_html) 

1145 

1146 # Generate error report if there were failures 

1147 if failed_results: 

1148 error_html = self.generate_error_report(failed_results) 

1149 error_file = self.output_dir / "error_report.html" 

1150 with open(error_file, 'w', encoding='utf-8') as f: 

1151 f.write(error_html) 

1152 

1153 return str(self.output_dir / "index.html") 

1154 

1155 

1156class SkillGenerationReporter: 

1157 """Handles reporting for skill generation sessions.""" 

1158 

1159 def __init__(self, model: str = None, analysis_model: str = None): 

1160 """Initialize the reporter. 

1161  

1162 Args: 

1163 model: The generation model used 

1164 analysis_model: The analysis model used 

1165 """ 

1166 self.model = model 

1167 self.analysis_model = analysis_model 

1168 self.results = [] 

1169 

1170 def add_result(self, result: Dict): 

1171 """Add a generation result to the reporter. 

1172  

1173 Args: 

1174 result: Generation result dictionary 

1175 """ 

1176 self.results.append(result) 

1177 

1178 def generate_report(self, output_dir: str = "skill_docs") -> str: 

1179 """Generate the complete report. 

1180  

1181 Args: 

1182 output_dir: Directory to save the documentation 

1183  

1184 Returns: 

1185 Path to the generated documentation 

1186 """ 

1187 doc_generator = SkillDocumentationGenerator( 

1188 model=self.model, 

1189 analysis_model=self.analysis_model, 

1190 output_dir=output_dir 

1191 ) 

1192 

1193 # Convert results to the expected format 

1194 formatted_results = [] 

1195 for result in self.results: 

1196 formatted_result = { 

1197 'success': result.get('success', False), 

1198 'skill': result.get('skill'), 

1199 'skill_name': None, 

1200 'description': None, 

1201 'plan': None, 

1202 'errors': result.get('errors', []), 

1203 'attempts': result.get('attempts', 1), 

1204 'generation_time': result.get('generation_time', 0), 

1205 'step_results': result.get('step_results', {}), 

1206 'vibe_test_passed': result.get('vibe_test_passed', False), 

1207 'vibe_test_results': result.get('vibe_test_results'), 

1208 'function_code': None 

1209 } 

1210 

1211 # Extract skill details 

1212 if result.get('skill'): 

1213 skill = result['skill'] 

1214 if hasattr(skill, '__dict__'): 

1215 formatted_result['skill_name'] = skill.name 

1216 formatted_result['description'] = skill.description 

1217 formatted_result['function_code'] = skill.function_code 

1218 elif isinstance(skill, dict): 

1219 formatted_result['skill_name'] = skill.get('name') 

1220 formatted_result['description'] = skill.get('description') 

1221 formatted_result['function_code'] = skill.get('function_code') 

1222 

1223 # Extract plan details 

1224 if result.get('plan'): 

1225 plan = result['plan'] 

1226 if hasattr(plan, '__dict__'): 

1227 formatted_result['plan'] = { 

1228 'idea': plan.idea, 

1229 'name': plan.name, 

1230 'description': plan.description, 

1231 'role': plan.role 

1232 } 

1233 elif isinstance(plan, dict): 

1234 formatted_result['plan'] = plan 

1235 

1236 formatted_results.append(formatted_result) 

1237 

1238 return doc_generator.generate_documentation(formatted_results)