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
« 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."""
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
12class SkillDocumentationGenerator:
13 """Generates navigatable HTML documentation for all skills with individual pages."""
15 def __init__(self, model: str = None, analysis_model: str = None, output_dir: str = "skill_docs"):
16 """Initialize the documentation generator.
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")
29 # Create output directory if it doesn't exist
30 self.output_dir.mkdir(parents=True, exist_ok=True)
32 def load_all_skills(self) -> Dict[str, Dict]:
33 """Load all skills from the skills_data directory.
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
49 def generate_skill_page(self, skill_data: Dict, is_new: bool = False) -> str:
50 """Generate an individual HTML page for a skill.
52 Args:
53 skill_data: The skill's data dictionary
54 is_new: Whether this is a newly generated skill
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)
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>"
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>"
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>"
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>'
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>'
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>
377 <div style="text-align: center; margin: 30px 0;">
378 {edit_button}
379 </div>
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>
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>
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>
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>
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>
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>
430 <div id="view-panel">
431 <div class="section">
432 <h2>📝 Description</h2>
433 <p>{description}</p>
434 </div>
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>
442 <div class="section">
443 <h2>⚙️ Parameters</h2>
444 {params_html}
445 </div>
447 <div class="section">
448 <h2>💻 Implementation</h2>
449 {code_html}
450 </div>
451 </div>
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>
460 <script>
461 const skillData = {json.dumps(skill_data, indent=2)};
462 const isBuiltIn = {str(verified).lower()};
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 }}
473 function cancelEdit() {{
474 document.getElementById('edit-panel').style.display = 'none';
475 document.getElementById('view-panel').style.display = 'block';
476 clearMessage();
477 }}
479 async function testSkill() {{
480 const formData = collectFormData();
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 }});
492 const data = await response.json();
493 const output = document.getElementById('test-output');
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 }}
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 }}
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 }}
536 async function saveSkill() {{
537 if (isBuiltIn) {{
538 showMessage('Built-in skills cannot be edited', 'error');
539 return;
540 }}
542 const formData = collectFormData();
543 formData.last_modified = new Date().toISOString();
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 }});
552 const data = await response.json();
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 }}
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 }}
575 function clearMessage() {{
576 document.getElementById('message-area').innerHTML = '';
577 }}
579 // Form submission handler
580 document.getElementById('edit-form').addEventListener('submit', function(e) {{
581 e.preventDefault();
582 saveSkill();
583 }});
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 }}
600 // Check API availability on page load
601 checkAPIAvailability();
602 </script>
603</body>
604</html>"""
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.
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
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))
626 # Sort skills within each role
627 for role in skills_by_role:
628 skills_by_role[role].sort(key=lambda x: x[0])
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 """
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 += '...'
648 new_badge = '<span class="badge new">NEW</span>' if is_new else ''
649 verified_badge = '<span class="badge verified">✓</span>' if verified else ''
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 """
664 skills_html += """
665 </div>
666 </div>
667 """
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))
674 # Generate charts if we have generation results
675 charts_html = ""
676 if generation_results:
677 charts_html = self.generate_report_charts(generation_results)
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>
836 <div class="search-box">
837 <input type="text" id="skillSearch" placeholder="Search skills..." onkeyup="filterSkills()">
838 </div>
840 {charts_html}
842 {skills_html}
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>
850 <script>
851 function filterSkills() {{
852 const input = document.getElementById('skillSearch');
853 const filter = input.value.toLowerCase();
854 const cards = document.getElementsByClassName('skill-card');
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>"""
865 def generate_error_report(self, failed_results: List[Dict]) -> str:
866 """Generate a separate error report for failed generations.
868 Args:
869 failed_results: List of failed generation results
871 Returns:
872 HTML content for the error report
873 """
874 if not failed_results:
875 return ""
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', {})
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"
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 """
904 for error in errors:
905 errors_html += f"<li>{error}</li>"
907 errors_html += """
908 </ul>
909 </div>
910 """
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 """
925 errors_html += "</div>"
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>
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>
1009 {errors_html}
1011 <a href="index.html" class="nav-button">← Back to Skills Documentation</a>
1012 </div>
1013</body>
1014</html>"""
1016 def generate_report_charts(self, generation_results: List[Dict]) -> str:
1017 """Generate charts for the generation report.
1019 Args:
1020 generation_results: List of generation results
1022 Returns:
1023 HTML containing the charts
1024 """
1025 if not generation_results:
1026 return ""
1028 # Prepare data
1029 successful = sum(1 for r in generation_results if r.get('success', False))
1030 failed = len(generation_results) - successful
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 )
1045 chart1_html = fig1.to_html(full_html=False, include_plotlyjs=False, div_id="success-pie")
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 """
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, '🔧')
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 """
1103 def escape_html(self, text: str) -> str:
1104 """Escape HTML special characters."""
1105 return text.replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')
1107 def generate_documentation(self, generation_results: List[Dict] = None) -> str:
1108 """Generate complete documentation for all skills.
1110 Args:
1111 generation_results: Optional list of recent generation results
1113 Returns:
1114 Path to the generated documentation
1115 """
1116 # Load all skills
1117 all_skills = self.load_all_skills()
1119 # Identify new skills from generation results
1120 new_skills = []
1121 failed_results = []
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)
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)
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)
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)
1153 return str(self.output_dir / "index.html")
1156class SkillGenerationReporter:
1157 """Handles reporting for skill generation sessions."""
1159 def __init__(self, model: str = None, analysis_model: str = None):
1160 """Initialize the reporter.
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 = []
1170 def add_result(self, result: Dict):
1171 """Add a generation result to the reporter.
1173 Args:
1174 result: Generation result dictionary
1175 """
1176 self.results.append(result)
1178 def generate_report(self, output_dir: str = "skill_docs") -> str:
1179 """Generate the complete report.
1181 Args:
1182 output_dir: Directory to save the documentation
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 )
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 }
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')
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
1236 formatted_results.append(formatted_result)
1238 return doc_generator.generate_documentation(formatted_results)