Coverage for src/ollamapy/skill_editor/api.py: 59%

239 statements  

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

1"""Flask API for interactive skill editing.""" 

2 

3from flask import Flask, request, jsonify, send_from_directory 

4from flask_cors import CORS 

5from pathlib import Path 

6import json 

7import os 

8from datetime import datetime 

9from typing import Dict, Any, List, Optional 

10 

11from ..skills import SkillRegistry, Skill 

12from .validator import SkillValidator 

13 

14 

15class SkillEditorAPI: 

16 """Flask-based API for skill editing operations.""" 

17 

18 def __init__(self, skills_directory: Optional[str] = None, port: int = 5000): 

19 """Initialize the skill editor API. 

20 

21 Args: 

22 skills_directory: Directory containing skill JSON files 

23 port: Port to run the Flask server on 

24 """ 

25 self.app = Flask(__name__) 

26 CORS(self.app) # Enable CORS for frontend requests 

27 

28 # Initialize skill registry 

29 self.registry = SkillRegistry(skills_directory) 

30 self.validator = SkillValidator() 

31 self.port = port 

32 

33 # Set up routes 

34 self._setup_routes() 

35 

36 def _setup_routes(self): 

37 """Set up all API routes.""" 

38 

39 @self.app.route("/api/skills", methods=["GET"]) 

40 def get_all_skills(): 

41 """Get all skills.""" 

42 try: 

43 skills = self.registry.get_all_skills() 

44 skill_data = {} 

45 for name, skill in skills.items(): 

46 skill_data[name] = skill.to_dict() 

47 return jsonify({"success": True, "skills": skill_data}) 

48 except Exception as e: 

49 return jsonify({"success": False, "error": str(e)}), 500 

50 

51 @self.app.route("/api/skills/<skill_name>", methods=["GET"]) 

52 def get_skill(skill_name): 

53 """Get a specific skill.""" 

54 try: 

55 if skill_name not in self.registry.skills: 

56 return jsonify({"success": False, "error": "Skill not found"}), 404 

57 

58 skill = self.registry.skills[skill_name] 

59 return jsonify({"success": True, "skill": skill.to_dict()}) 

60 except Exception as e: 

61 return jsonify({"success": False, "error": str(e)}), 500 

62 

63 @self.app.route("/api/skills/<skill_name>/download", methods=["GET"]) 

64 def download_skill(skill_name): 

65 """Download a specific skill as JSON.""" 

66 try: 

67 if skill_name not in self.registry.skills: 

68 return jsonify({"success": False, "error": "Skill not found"}), 404 

69 

70 skill = self.registry.skills[skill_name] 

71 skill_data = skill.to_dict() 

72 

73 # Create response with proper JSON content type and download headers 

74 response = jsonify(skill_data) 

75 response.headers["Content-Disposition"] = ( 

76 f'attachment; filename="{skill_name}.json"' 

77 ) 

78 response.headers["Content-Type"] = "application/json" 

79 return response 

80 except Exception as e: 

81 return jsonify({"success": False, "error": str(e)}), 500 

82 

83 @self.app.route("/api/skills/<skill_name>", methods=["PUT"]) 

84 def update_skill(skill_name): 

85 """Update an existing skill.""" 

86 try: 

87 skill_data = request.json 

88 if not skill_data: 

89 return jsonify({"success": False, "error": "No data provided"}), 400 

90 

91 # Check if skill exists 

92 if skill_name not in self.registry.skills: 

93 return jsonify({"success": False, "error": "Skill not found"}), 404 

94 

95 existing_skill = self.registry.skills[skill_name] 

96 

97 # Protect built-in skills 

98 if existing_skill.verified: 

99 return ( 

100 jsonify( 

101 {"success": False, "error": "Cannot modify built-in skill"} 

102 ), 

103 403, 

104 ) 

105 

106 # Validate the skill data 

107 validation_result = self.validator.validate_skill_data(skill_data) 

108 if not validation_result.is_valid: 

109 return ( 

110 jsonify( 

111 { 

112 "success": False, 

113 "error": "Validation failed", 

114 "validation_errors": validation_result.errors, 

115 } 

116 ), 

117 400, 

118 ) 

119 

120 # Update timestamps 

121 skill_data["last_modified"] = datetime.now().isoformat() 

122 skill_data["name"] = skill_name # Ensure name consistency 

123 

124 # Create updated skill 

125 updated_skill = Skill.from_dict(skill_data) 

126 

127 # Register the updated skill 

128 success = self.registry.register_skill(updated_skill) 

129 if not success: 

130 return ( 

131 jsonify( 

132 { 

133 "success": False, 

134 "error": "Failed to register updated skill", 

135 } 

136 ), 

137 500, 

138 ) 

139 

140 return jsonify( 

141 {"success": True, "message": "Skill updated successfully"} 

142 ) 

143 

144 except Exception as e: 

145 return jsonify({"success": False, "error": str(e)}), 500 

146 

147 @self.app.route("/api/skills/<skill_name>", methods=["DELETE"]) 

148 def delete_skill(skill_name): 

149 """Delete a skill.""" 

150 try: 

151 if skill_name not in self.registry.skills: 

152 return jsonify({"success": False, "error": "Skill not found"}), 404 

153 

154 skill = self.registry.skills[skill_name] 

155 

156 # Protect built-in skills 

157 if skill.verified: 

158 return ( 

159 jsonify( 

160 {"success": False, "error": "Cannot delete built-in skill"} 

161 ), 

162 403, 

163 ) 

164 

165 # Remove from registry 

166 del self.registry.skills[skill_name] 

167 if skill_name in self.registry.compiled_functions: 

168 del self.registry.compiled_functions[skill_name] 

169 

170 # Remove file 

171 skill_file = self.registry.skills_dir / f"{skill_name}.json" 

172 if skill_file.exists(): 

173 skill_file.unlink() 

174 

175 return jsonify( 

176 {"success": True, "message": "Skill deleted successfully"} 

177 ) 

178 

179 except Exception as e: 

180 return jsonify({"success": False, "error": str(e)}), 500 

181 

182 @self.app.route("/api/skills", methods=["POST"]) 

183 def create_skill(): 

184 """Create a new skill.""" 

185 try: 

186 skill_data = request.json 

187 if not skill_data: 

188 return jsonify({"success": False, "error": "No data provided"}), 400 

189 

190 # Validate required fields 

191 required_fields = ["name", "description", "function_code"] 

192 for field in required_fields: 

193 if field not in skill_data: 

194 return ( 

195 jsonify( 

196 { 

197 "success": False, 

198 "error": f"Missing required field: {field}", 

199 } 

200 ), 

201 400, 

202 ) 

203 

204 skill_name = skill_data["name"] 

205 

206 # Check if skill already exists 

207 if skill_name in self.registry.skills: 

208 return ( 

209 jsonify({"success": False, "error": "Skill already exists"}), 

210 409, 

211 ) 

212 

213 # Set defaults for optional fields 

214 skill_data.setdefault("vibe_test_phrases", []) 

215 skill_data.setdefault("parameters", {}) 

216 skill_data.setdefault("verified", False) 

217 skill_data.setdefault("scope", "local") 

218 skill_data.setdefault("role", "general") 

219 skill_data.setdefault("tags", []) 

220 skill_data.setdefault("execution_count", 0) 

221 skill_data.setdefault("success_rate", 100.0) 

222 skill_data.setdefault("average_execution_time", 0.0) 

223 skill_data["created_at"] = datetime.now().isoformat() 

224 skill_data["last_modified"] = datetime.now().isoformat() 

225 

226 # Validate the skill data 

227 validation_result = self.validator.validate_skill_data(skill_data) 

228 if not validation_result.is_valid: 

229 return ( 

230 jsonify( 

231 { 

232 "success": False, 

233 "error": "Validation failed", 

234 "validation_errors": validation_result.errors, 

235 } 

236 ), 

237 400, 

238 ) 

239 

240 # Create skill 

241 new_skill = Skill.from_dict(skill_data) 

242 

243 # Register the skill 

244 success = self.registry.register_skill(new_skill) 

245 if not success: 

246 return ( 

247 jsonify( 

248 {"success": False, "error": "Failed to register skill"} 

249 ), 

250 500, 

251 ) 

252 

253 return jsonify( 

254 { 

255 "success": True, 

256 "message": "Skill created successfully", 

257 "skill_name": skill_name, 

258 } 

259 ) 

260 

261 except Exception as e: 

262 return jsonify({"success": False, "error": str(e)}), 500 

263 

264 @self.app.route("/api/skills/validate", methods=["POST"]) 

265 def validate_skill(): 

266 """Validate skill data without saving.""" 

267 try: 

268 skill_data = request.json 

269 if not skill_data: 

270 return jsonify({"success": False, "error": "No data provided"}), 400 

271 

272 validation_result = self.validator.validate_skill_data(skill_data) 

273 

274 return jsonify( 

275 { 

276 "success": True, 

277 "is_valid": validation_result.is_valid, 

278 "errors": validation_result.errors, 

279 "warnings": validation_result.warnings, 

280 } 

281 ) 

282 

283 except Exception as e: 

284 return jsonify({"success": False, "error": str(e)}), 500 

285 

286 @self.app.route("/api/skills/test", methods=["POST"]) 

287 def test_skill(): 

288 """Test a skill without saving it.""" 

289 try: 

290 request_data = request.json 

291 skill_data = request_data.get("skill_data") 

292 test_input = request_data.get("test_input", {}) 

293 

294 if not skill_data: 

295 return ( 

296 jsonify({"success": False, "error": "No skill data provided"}), 

297 400, 

298 ) 

299 

300 # Validate first 

301 validation_result = self.validator.validate_skill_data(skill_data) 

302 if not validation_result.is_valid: 

303 return ( 

304 jsonify( 

305 { 

306 "success": False, 

307 "error": "Skill validation failed", 

308 "validation_errors": validation_result.errors, 

309 } 

310 ), 

311 400, 

312 ) 

313 

314 # Create temporary skill instance 

315 temp_skill = Skill.from_dict(skill_data) 

316 

317 # Clear logs before testing 

318 self.registry.clear_logs() 

319 

320 # Try to compile and execute 

321 try: 

322 func = self.registry._compile_skill_function(temp_skill) 

323 

324 # Execute with test input 

325 if temp_skill.parameters and test_input: 

326 func(**test_input) 

327 else: 

328 func() 

329 

330 # Get execution logs 

331 logs = self.registry.get_logs() 

332 

333 return jsonify( 

334 { 

335 "success": True, 

336 "execution_successful": True, 

337 "output": logs, 

338 "message": "Skill executed successfully", 

339 } 

340 ) 

341 

342 except Exception as exec_error: 

343 return jsonify( 

344 { 

345 "success": True, 

346 "execution_successful": False, 

347 "error": str(exec_error), 

348 "message": "Skill compilation or execution failed", 

349 } 

350 ) 

351 

352 except Exception as e: 

353 return jsonify({"success": False, "error": str(e)}), 500 

354 

355 @self.app.route("/api/skills/roles", methods=["GET"]) 

356 def get_skill_roles(): 

357 """Get all available skill roles.""" 

358 roles = [ 

359 "general", 

360 "text_processing", 

361 "mathematics", 

362 "data_analysis", 

363 "file_operations", 

364 "web_utilities", 

365 "time_date", 

366 "formatting", 

367 "validation", 

368 "emotional_response", 

369 "information", 

370 "advanced", 

371 ] 

372 return jsonify({"success": True, "roles": roles}) 

373 

374 @self.app.route("/api/skills/export", methods=["GET"]) 

375 def export_skills(): 

376 """Export all skills as JSON.""" 

377 try: 

378 skills = self.registry.get_all_skills() 

379 skill_data = {} 

380 for name, skill in skills.items(): 

381 skill_data[name] = skill.to_dict() 

382 

383 return jsonify( 

384 { 

385 "success": True, 

386 "export_date": datetime.now().isoformat(), 

387 "skills_count": len(skill_data), 

388 "skills": skill_data, 

389 } 

390 ) 

391 except Exception as e: 

392 return jsonify({"success": False, "error": str(e)}), 500 

393 

394 @self.app.route("/api/skills/import", methods=["POST"]) 

395 def import_skills(): 

396 """Import skills from JSON data with schema validation.""" 

397 try: 

398 import_data = request.json 

399 if not import_data: 

400 return ( 

401 jsonify({"success": False, "error": "No data provided"}), 

402 400, 

403 ) 

404 

405 imported_count = 0 

406 errors = [] 

407 

408 # Handle single skill import 

409 if "name" in import_data: 

410 skills_to_import = {import_data["name"]: import_data} 

411 # Handle multiple skills import 

412 elif "skills" in import_data: 

413 skills_to_import = import_data["skills"] 

414 else: 

415 return ( 

416 jsonify( 

417 { 

418 "success": False, 

419 "error": "Invalid import data format. Expected single skill or skills collection.", 

420 } 

421 ), 

422 400, 

423 ) 

424 

425 # Import JSON schema validation 

426 import jsonschema 

427 

428 schema_path = Path(__file__).parent.parent / "skill_schema.json" 

429 if schema_path.exists(): 

430 with open(schema_path, "r") as f: 

431 schema = json.load(f) 

432 else: 

433 schema = None 

434 

435 for skill_name, skill_data in skills_to_import.items(): 

436 try: 

437 # Skip if skill already exists 

438 if skill_name in self.registry.skills: 

439 errors.append( 

440 f"Skill '{skill_name}' already exists, skipped" 

441 ) 

442 continue 

443 

444 # Validate against JSON schema if available 

445 if schema: 

446 try: 

447 jsonschema.validate(skill_data, schema) 

448 except jsonschema.ValidationError as ve: 

449 errors.append( 

450 f"Skill '{skill_name}' schema validation failed: {ve.message} at path {'.'.join(str(x) for x in ve.absolute_path)}" 

451 ) 

452 continue 

453 except jsonschema.SchemaError as se: 

454 errors.append( 

455 f"Schema error for skill '{skill_name}': {se.message}" 

456 ) 

457 continue 

458 

459 # Validate skill data with existing validator 

460 validation_result = self.validator.validate_skill_data( 

461 skill_data 

462 ) 

463 if not validation_result.is_valid: 

464 errors.append( 

465 f"Skill '{skill_name}' validation failed: {', '.join(validation_result.errors)}" 

466 ) 

467 continue 

468 

469 # Create and register skill 

470 skill = Skill.from_dict(skill_data) 

471 success = self.registry.register_skill(skill) 

472 if success: 

473 imported_count += 1 

474 else: 

475 errors.append(f"Failed to register skill '{skill_name}'") 

476 

477 except Exception as skill_error: 

478 errors.append( 

479 f"Error importing skill '{skill_name}': {str(skill_error)}" 

480 ) 

481 

482 return jsonify( 

483 { 

484 "success": True, 

485 "imported_count": imported_count, 

486 "errors": errors, 

487 } 

488 ) 

489 

490 except Exception as e: 

491 return jsonify({"success": False, "error": str(e)}), 500 

492 

493 @self.app.route("/") 

494 def serve_index(): 

495 """Serve the main editor interface.""" 

496 return self._get_editor_html() 

497 

498 @self.app.route("/skill/<skill_name>") 

499 def serve_skill_editor(skill_name): 

500 """Serve the skill editor interface.""" 

501 return self._get_skill_editor_html(skill_name) 

502 

503 @self.app.route("/new-skill") 

504 def serve_new_skill(): 

505 """Serve the new skill creation interface.""" 

506 return self._get_new_skill_html() 

507 

508 def _get_editor_html(self) -> str: 

509 """Generate the main editor interface HTML.""" 

510 return """<!DOCTYPE html> 

511<html lang="en"> 

512<head> 

513 <meta charset="utf-8"> 

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

515 <title>Skill Editor - OllamaPy</title> 

516 <style> 

517 body { 

518 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 

519 margin: 0; 

520 padding: 20px; 

521 background: #f5f7fa; 

522 } 

523 .header { 

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

525 color: white; 

526 padding: 30px; 

527 border-radius: 10px; 

528 margin-bottom: 30px; 

529 text-align: center; 

530 } 

531 .skills-grid { 

532 display: grid; 

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

534 gap: 20px; 

535 margin-bottom: 30px; 

536 } 

537 .skill-card { 

538 background: white; 

539 padding: 20px; 

540 border-radius: 8px; 

541 box-shadow: 0 2px 10px rgba(0,0,0,0.1); 

542 transition: transform 0.2s; 

543 } 

544 .skill-card:hover { 

545 transform: translateY(-2px); 

546 } 

547 .skill-title { 

548 margin: 0 0 10px 0; 

549 color: #333; 

550 } 

551 .skill-description { 

552 color: #666; 

553 margin: 10px 0; 

554 font-size: 14px; 

555 } 

556 .skill-meta { 

557 display: flex; 

558 justify-content: space-between; 

559 align-items: center; 

560 margin-top: 15px; 

561 } 

562 .badge { 

563 padding: 4px 8px; 

564 border-radius: 12px; 

565 font-size: 12px; 

566 font-weight: bold; 

567 } 

568 .badge.verified { 

569 background: #28a745; 

570 color: white; 

571 } 

572 .badge.unverified { 

573 background: #ffc107; 

574 color: #000; 

575 } 

576 .btn { 

577 background: #667eea; 

578 color: white; 

579 border: none; 

580 padding: 8px 16px; 

581 border-radius: 4px; 

582 cursor: pointer; 

583 text-decoration: none; 

584 display: inline-block; 

585 font-size: 14px; 

586 } 

587 .btn:hover { 

588 background: #5a67d8; 

589 } 

590 .btn-danger { 

591 background: #dc3545; 

592 } 

593 .btn-danger:hover { 

594 background: #c82333; 

595 } 

596 .new-skill-btn { 

597 position: fixed; 

598 bottom: 30px; 

599 right: 30px; 

600 background: #28a745; 

601 color: white; 

602 border: none; 

603 width: 60px; 

604 height: 60px; 

605 border-radius: 50%; 

606 font-size: 24px; 

607 cursor: pointer; 

608 box-shadow: 0 4px 12px rgba(0,0,0,0.15); 

609 } 

610 .import-btn { 

611 position: fixed; 

612 bottom: 30px; 

613 right: 110px; 

614 background: #007bff; 

615 color: white; 

616 border: none; 

617 width: 60px; 

618 height: 60px; 

619 border-radius: 50%; 

620 font-size: 18px; 

621 cursor: pointer; 

622 box-shadow: 0 4px 12px rgba(0,0,0,0.15); 

623 } 

624 .btn-download { 

625 background: #17a2b8; 

626 font-size: 12px; 

627 padding: 4px 8px; 

628 } 

629 .btn-download:hover { 

630 background: #138496; 

631 } 

632 .import-controls { 

633 background: white; 

634 padding: 20px; 

635 border-radius: 8px; 

636 box-shadow: 0 2px 10px rgba(0,0,0,0.1); 

637 margin-bottom: 20px; 

638 display: none; 

639 } 

640 .file-input { 

641 margin: 10px 0; 

642 } 

643 .import-status { 

644 margin-top: 10px; 

645 padding: 10px; 

646 border-radius: 4px; 

647 } 

648 .import-success { 

649 background: #d4edda; 

650 color: #155724; 

651 } 

652 .import-error { 

653 background: #f8d7da; 

654 color: #721c24; 

655 } 

656 .loading { 

657 text-align: center; 

658 padding: 40px; 

659 } 

660 </style> 

661</head> 

662<body> 

663 <div class="header"> 

664 <h1>🔧 Skill Editor</h1> 

665 <p>Manage and edit your OllamaPy skills</p> 

666 </div> 

667  

668 <div id="import-controls" class="import-controls"> 

669 <h3>📥 Import Skill</h3> 

670 <p>Select a JSON file to import a skill with validation:</p> 

671 <input type="file" id="skill-file" class="file-input" accept=".json" /> 

672 <div> 

673 <button onclick="importSkill()" class="btn">Import Skill</button> 

674 <button onclick="toggleImport()" class="btn btn-secondary">Cancel</button> 

675 </div> 

676 <div id="import-status"></div> 

677 </div> 

678  

679 <div id="loading" class="loading">Loading skills...</div> 

680 <div id="skills-container" class="skills-grid" style="display: none;"></div> 

681  

682 <button class="import-btn" onclick="toggleImport()" title="Import Skill">📥</button> 

683 <button class="new-skill-btn" onclick="location.href='/new-skill'" title="Create New Skill">+</button> 

684  

685 <script> 

686 async function loadSkills() { 

687 try { 

688 const response = await fetch('/api/skills'); 

689 const data = await response.json(); 

690  

691 document.getElementById('loading').style.display = 'none'; 

692  

693 if (data.success) { 

694 displaySkills(data.skills); 

695 } else { 

696 alert('Failed to load skills: ' + data.error); 

697 } 

698 } catch (error) { 

699 document.getElementById('loading').style.display = 'none'; 

700 alert('Error loading skills: ' + error.message); 

701 } 

702 } 

703  

704 function displaySkills(skills) { 

705 const container = document.getElementById('skills-container'); 

706 container.innerHTML = ''; 

707  

708 Object.entries(skills).forEach(([name, skill]) => { 

709 const card = document.createElement('div'); 

710 card.className = 'skill-card'; 

711  

712 const verifiedBadge = skill.verified ?  

713 '<span class="badge verified">VERIFIED</span>' :  

714 '<span class="badge unverified">CUSTOM</span>'; 

715  

716 card.innerHTML = ` 

717 <h3 class="skill-title">${name}</h3> 

718 <p class="skill-description">${skill.description || 'No description'}</p> 

719 <div class="skill-meta"> 

720 ${verifiedBadge} 

721 <div> 

722 <button onclick="downloadSkill('${name}')" class="btn btn-download">📥 Download</button> 

723 <a href="/skill/${name}" class="btn">Edit</a> 

724 ${!skill.verified ? `<button onclick="deleteSkill('${name}')" class="btn btn-danger">Delete</button>` : ''} 

725 </div> 

726 </div> 

727 `; 

728  

729 container.appendChild(card); 

730 }); 

731  

732 container.style.display = 'grid'; 

733 } 

734  

735 async function deleteSkill(skillName) { 

736 if (!confirm(`Are you sure you want to delete the skill "${skillName}"?`)) { 

737 return; 

738 } 

739  

740 try { 

741 const response = await fetch(`/api/skills/${skillName}`, { 

742 method: 'DELETE' 

743 }); 

744 const data = await response.json(); 

745  

746 if (data.success) { 

747 alert('Skill deleted successfully'); 

748 loadSkills(); // Reload the skills 

749 } else { 

750 alert('Failed to delete skill: ' + data.error); 

751 } 

752 } catch (error) { 

753 alert('Error deleting skill: ' + error.message); 

754 } 

755 } 

756  

757 async function downloadSkill(skillName) { 

758 try { 

759 const response = await fetch(`/api/skills/${skillName}/download`); 

760 const blob = await response.blob(); 

761  

762 if (response.ok) { 

763 const url = window.URL.createObjectURL(blob); 

764 const a = document.createElement('a'); 

765 a.href = url; 

766 a.download = `${skillName}.json`; 

767 document.body.appendChild(a); 

768 a.click(); 

769 document.body.removeChild(a); 

770 window.URL.revokeObjectURL(url); 

771 } else { 

772 const error = await response.json(); 

773 alert('Failed to download skill: ' + error.error); 

774 } 

775 } catch (error) { 

776 alert('Error downloading skill: ' + error.message); 

777 } 

778 } 

779  

780 function toggleImport() { 

781 const controls = document.getElementById('import-controls'); 

782 const isVisible = controls.style.display !== 'none'; 

783 controls.style.display = isVisible ? 'none' : 'block'; 

784 if (!isVisible) { 

785 document.getElementById('import-status').innerHTML = ''; 

786 document.getElementById('skill-file').value = ''; 

787 } 

788 } 

789  

790 async function importSkill() { 

791 const fileInput = document.getElementById('skill-file'); 

792 const statusDiv = document.getElementById('import-status'); 

793  

794 if (!fileInput.files || fileInput.files.length === 0) { 

795 statusDiv.innerHTML = '<div class="import-error">Please select a JSON file to import.</div>'; 

796 return; 

797 } 

798  

799 const file = fileInput.files[0]; 

800  

801 try { 

802 const fileText = await file.text(); 

803 const skillData = JSON.parse(fileText); 

804  

805 const response = await fetch('/api/skills/import', { 

806 method: 'POST', 

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

808 body: JSON.stringify(skillData) 

809 }); 

810  

811 const result = await response.json(); 

812  

813 if (result.success) { 

814 if (result.imported_count > 0) { 

815 statusDiv.innerHTML = `<div class="import-success">Successfully imported ${result.imported_count} skill(s)!</div>`; 

816 loadSkills(); // Reload skills list 

817 setTimeout(toggleImport, 2000); // Hide import panel after success 

818 } else { 

819 statusDiv.innerHTML = `<div class="import-error">No skills were imported. Errors: ${result.errors.join(', ')}</div>`; 

820 } 

821 } else { 

822 statusDiv.innerHTML = `<div class="import-error">Import failed: ${result.error}</div>`; 

823 } 

824  

825 if (result.errors && result.errors.length > 0) { 

826 const errorList = result.errors.map(err => `<li>${err}</li>`).join(''); 

827 statusDiv.innerHTML += `<div class="import-error"><strong>Validation Errors:</strong><ul>${errorList}</ul></div>`; 

828 } 

829  

830 } catch (error) { 

831 if (error instanceof SyntaxError) { 

832 statusDiv.innerHTML = '<div class="import-error">Invalid JSON file format. Please check your file syntax.</div>'; 

833 } else { 

834 statusDiv.innerHTML = `<div class="import-error">Error importing skill: ${error.message}</div>`; 

835 } 

836 } 

837 } 

838  

839 // Load skills when page loads 

840 loadSkills(); 

841 </script> 

842</body> 

843</html>""" 

844 

845 def _get_skill_editor_html(self, skill_name: str) -> str: 

846 """Generate the skill editor interface HTML.""" 

847 return f"""<!DOCTYPE html> 

848<html lang="en"> 

849<head> 

850 <meta charset="utf-8"> 

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

852 <title>Edit Skill: {skill_name} - OllamaPy</title> 

853 <style> 

854 body {{ 

855 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 

856 margin: 0; 

857 padding: 20px; 

858 background: #f5f7fa; 

859 }} 

860 .container {{ 

861 max-width: 1000px; 

862 margin: 0 auto; 

863 background: white; 

864 border-radius: 10px; 

865 box-shadow: 0 4px 20px rgba(0,0,0,0.1); 

866 }} 

867 .header {{ 

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

869 color: white; 

870 padding: 30px; 

871 border-radius: 10px 10px 0 0; 

872 }} 

873 .form-section {{ 

874 padding: 30px; 

875 }} 

876 .form-group {{ 

877 margin-bottom: 20px; 

878 }} 

879 .form-group label {{ 

880 display: block; 

881 margin-bottom: 8px; 

882 font-weight: 600; 

883 color: #333; 

884 }} 

885 .form-control {{ 

886 width: 100%; 

887 padding: 12px; 

888 border: 2px solid #e2e8f0; 

889 border-radius: 6px; 

890 font-size: 14px; 

891 transition: border-color 0.2s; 

892 }} 

893 .form-control:focus {{ 

894 outline: none; 

895 border-color: #667eea; 

896 }} 

897 textarea.form-control {{ 

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

899 resize: vertical; 

900 }} 

901 .btn {{ 

902 background: #667eea; 

903 color: white; 

904 border: none; 

905 padding: 12px 24px; 

906 border-radius: 6px; 

907 cursor: pointer; 

908 font-size: 16px; 

909 margin-right: 10px; 

910 transition: background 0.2s; 

911 }} 

912 .btn:hover {{ 

913 background: #5a67d8; 

914 }} 

915 .btn-secondary {{ 

916 background: #6c757d; 

917 }} 

918 .btn-secondary:hover {{ 

919 background: #5a6268; 

920 }} 

921 .btn-success {{ 

922 background: #28a745; 

923 }} 

924 .btn-success:hover {{ 

925 background: #218838; 

926 }} 

927 .parameter-editor {{ 

928 border: 2px solid #e2e8f0; 

929 border-radius: 6px; 

930 padding: 20px; 

931 margin-top: 10px; 

932 }} 

933 .parameter-item {{ 

934 display: flex; 

935 gap: 10px; 

936 margin-bottom: 15px; 

937 align-items: end; 

938 }} 

939 .parameter-item input, .parameter-item select {{ 

940 flex: 1; 

941 padding: 8px; 

942 border: 1px solid #ddd; 

943 border-radius: 4px; 

944 }} 

945 .parameter-item button {{ 

946 background: #dc3545; 

947 color: white; 

948 border: none; 

949 padding: 8px 12px; 

950 border-radius: 4px; 

951 cursor: pointer; 

952 }} 

953 .test-section {{ 

954 background: #f8f9fa; 

955 padding: 20px; 

956 border-radius: 6px; 

957 margin-top: 20px; 

958 }} 

959 .test-output {{ 

960 background: #2d3748; 

961 color: #e2e8f0; 

962 padding: 15px; 

963 border-radius: 6px; 

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

965 white-space: pre-wrap; 

966 margin-top: 10px; 

967 max-height: 300px; 

968 overflow-y: auto; 

969 }} 

970 .loading {{ 

971 text-align: center; 

972 padding: 40px; 

973 }} 

974 .error {{ 

975 background: #f8d7da; 

976 color: #721c24; 

977 padding: 10px; 

978 border-radius: 4px; 

979 margin: 10px 0; 

980 }} 

981 .success {{ 

982 background: #d4edda; 

983 color: #155724; 

984 padding: 10px; 

985 border-radius: 4px; 

986 margin: 10px 0; 

987 }} 

988 </style> 

989</head> 

990<body> 

991 <div class="container"> 

992 <div class="header"> 

993 <h1>✏️ Edit Skill: {skill_name}</h1> 

994 <p>Modify skill properties and test your changes</p> 

995 </div> 

996  

997 <div id="loading" class="loading">Loading skill data...</div> 

998  

999 <div id="editor-form" class="form-section" style="display: none;"> 

1000 <form id="skill-form"> 

1001 <div class="form-group"> 

1002 <label>Skill Name</label> 

1003 <input type="text" id="skill-name" class="form-control" readonly> 

1004 </div> 

1005  

1006 <div class="form-group"> 

1007 <label>Description</label> 

1008 <textarea id="skill-description" class="form-control" rows="3" placeholder="Describe when this skill should be used"></textarea> 

1009 </div> 

1010  

1011 <div class="form-group"> 

1012 <label>Role</label> 

1013 <select id="skill-role" class="form-control"> 

1014 <option value="general">General</option> 

1015 <option value="text_processing">Text Processing</option> 

1016 <option value="mathematics">Mathematics</option> 

1017 <option value="data_analysis">Data Analysis</option> 

1018 <option value="file_operations">File Operations</option> 

1019 <option value="web_utilities">Web Utilities</option> 

1020 <option value="time_date">Time & Date</option> 

1021 <option value="formatting">Formatting</option> 

1022 <option value="validation">Validation</option> 

1023 <option value="emotional_response">Emotional Response</option> 

1024 <option value="information">Information</option> 

1025 <option value="advanced">Advanced</option> 

1026 </select> 

1027 </div> 

1028  

1029 <div class="form-group"> 

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

1031 <textarea id="vibe-phrases" class="form-control" rows="5" placeholder="Enter test phrases that should trigger this skill..."></textarea> 

1032 </div> 

1033  

1034 <div class="form-group"> 

1035 <label>Parameters</label> 

1036 <div class="parameter-editor"> 

1037 <div id="parameters-container"> 

1038 <!-- Parameters will be added here dynamically --> 

1039 </div> 

1040 <button type="button" onclick="addParameter()" class="btn btn-secondary">Add Parameter</button> 

1041 </div> 

1042 </div> 

1043  

1044 <div class="form-group"> 

1045 <label>Function Code</label> 

1046 <textarea id="function-code" class="form-control" rows="15" placeholder="def execute(param1=None):&#10; from ollamapy.actions import log&#10; log('[YourSkill] Your implementation here')"></textarea> 

1047 </div> 

1048  

1049 <div class="test-section"> 

1050 <h3>Test Skill</h3> 

1051 <button type="button" onclick="testSkill()" class="btn btn-success">Test Execution</button> 

1052 <div id="test-output" class="test-output" style="display: none;"></div> 

1053 </div> 

1054  

1055 <div style="margin-top: 30px;"> 

1056 <button type="submit" class="btn">Save Skill</button> 

1057 <button type="button" onclick="location.href='/'" class="btn btn-secondary">Cancel</button> 

1058 </div> 

1059 </form> 

1060 </div> 

1061  

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

1063 </div> 

1064  

1065 <script> 

1066 let skillData = null; 

1067  

1068 async function loadSkill() {{ 

1069 try {{ 

1070 const response = await fetch('/api/skills/{skill_name}'); 

1071 const data = await response.json(); 

1072  

1073 document.getElementById('loading').style.display = 'none'; 

1074  

1075 if (data.success) {{ 

1076 skillData = data.skill; 

1077 populateForm(skillData); 

1078 document.getElementById('editor-form').style.display = 'block'; 

1079 }} else {{ 

1080 showError('Failed to load skill: ' + data.error); 

1081 }} 

1082 }} catch (error) {{ 

1083 document.getElementById('loading').style.display = 'none'; 

1084 showError('Error loading skill: ' + error.message); 

1085 }} 

1086 }} 

1087  

1088 function populateForm(skill) {{ 

1089 document.getElementById('skill-name').value = skill.name; 

1090 document.getElementById('skill-description').value = skill.description || ''; 

1091 document.getElementById('skill-role').value = skill.role || 'general'; 

1092 document.getElementById('vibe-phrases').value = (skill.vibe_test_phrases || []).join('\\n'); 

1093 document.getElementById('function-code').value = skill.function_code || ''; 

1094  

1095 // Populate parameters 

1096 const container = document.getElementById('parameters-container'); 

1097 container.innerHTML = ''; 

1098 if (skill.parameters) {{ 

1099 Object.entries(skill.parameters).forEach(([name, info]) => {{ 

1100 addParameter(name, info.type, info.required, info.description); 

1101 }}); 

1102 }} 

1103 }} 

1104  

1105 function addParameter(name = '', type = 'string', required = false, description = '') {{ 

1106 const container = document.getElementById('parameters-container'); 

1107 const div = document.createElement('div'); 

1108 div.className = 'parameter-item'; 

1109 div.innerHTML = ` 

1110 <input type="text" placeholder="Parameter name" value="${{name}}" onchange="updateParameterPreview()"> 

1111 <select onchange="updateParameterPreview()"> 

1112 <option value="string" ${{type === 'string' ? 'selected' : ''}}>String</option> 

1113 <option value="number" ${{type === 'number' ? 'selected' : ''}}>Number</option> 

1114 <option value="boolean" ${{type === 'boolean' ? 'selected' : ''}}>Boolean</option> 

1115 </select> 

1116 <label><input type="checkbox" ${{required ? 'checked' : ''}} onchange="updateParameterPreview()"> Required</label> 

1117 <input type="text" placeholder="Description" value="${{description}}" onchange="updateParameterPreview()"> 

1118 <button type="button" onclick="removeParameter(this)">Remove</button> 

1119 `; 

1120 container.appendChild(div); 

1121 }} 

1122  

1123 function removeParameter(button) {{ 

1124 button.parentElement.remove(); 

1125 updateParameterPreview(); 

1126 }} 

1127  

1128 function updateParameterPreview() {{ 

1129 // This could be used to show a preview of the parameters 

1130 }} 

1131  

1132 function collectParameters() {{ 

1133 const parameters = {{}}; 

1134 const items = document.querySelectorAll('.parameter-item'); 

1135  

1136 items.forEach(item => {{ 

1137 const inputs = item.querySelectorAll('input'); 

1138 const select = item.querySelector('select'); 

1139 const name = inputs[0].value.trim(); 

1140  

1141 if (name) {{ 

1142 parameters[name] = {{ 

1143 type: select.value, 

1144 required: inputs[1].checked, 

1145 description: inputs[2].value.trim() 

1146 }}; 

1147 }} 

1148 }}); 

1149  

1150 return parameters; 

1151 }} 

1152  

1153 async function testSkill() {{ 

1154 const skillData = {{ 

1155 name: document.getElementById('skill-name').value, 

1156 description: document.getElementById('skill-description').value, 

1157 role: document.getElementById('skill-role').value, 

1158 vibe_test_phrases: document.getElementById('vibe-phrases').value.split('\\n').filter(p => p.trim()), 

1159 parameters: collectParameters(), 

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

1161 verified: false, 

1162 scope: 'local' 

1163 }}; 

1164  

1165 try {{ 

1166 const response = await fetch('/api/skills/test', {{ 

1167 method: 'POST', 

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

1169 body: JSON.stringify({{skill_data: skillData, test_input: {{}}}}) 

1170 }}); 

1171  

1172 const data = await response.json(); 

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

1174  

1175 if (data.success) {{ 

1176 if (data.execution_successful) {{ 

1177 output.textContent = 'Test passed!\\n\\nOutput:\\n' + data.output.join('\\n'); 

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

1179 }} else {{ 

1180 output.textContent = 'Test failed:\\n' + data.error; 

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

1182 }} 

1183 }} else {{ 

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

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

1186 }} 

1187  

1188 output.style.display = 'block'; 

1189 }} catch (error) {{ 

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

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

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

1193 output.style.display = 'block'; 

1194 }} 

1195 }} 

1196  

1197 document.getElementById('skill-form').addEventListener('submit', async (e) => {{ 

1198 e.preventDefault(); 

1199  

1200 // Check if skill is verified (built-in) 

1201 if (skillData && skillData.verified) {{ 

1202 showError('Cannot modify built-in skills'); 

1203 return; 

1204 }} 

1205  

1206 const formData = {{ 

1207 name: document.getElementById('skill-name').value, 

1208 description: document.getElementById('skill-description').value, 

1209 role: document.getElementById('skill-role').value, 

1210 vibe_test_phrases: document.getElementById('vibe-phrases').value.split('\\n').filter(p => p.trim()), 

1211 parameters: collectParameters(), 

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

1213 // Preserve existing fields 

1214 ...skillData, 

1215 last_modified: new Date().toISOString() 

1216 }}; 

1217  

1218 try {{ 

1219 const response = await fetch('/api/skills/{skill_name}', {{ 

1220 method: 'PUT', 

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

1222 body: JSON.stringify(formData) 

1223 }}); 

1224  

1225 const data = await response.json(); 

1226  

1227 if (data.success) {{ 

1228 showSuccess('Skill updated successfully!'); 

1229 // Reload skill data 

1230 loadSkill(); 

1231 }} else {{ 

1232 showError('Failed to update skill: ' + data.error); 

1233 if (data.validation_errors) {{ 

1234 showError('Validation errors: ' + data.validation_errors.join(', ')); 

1235 }} 

1236 }} 

1237 }} catch (error) {{ 

1238 showError('Error updating skill: ' + error.message); 

1239 }} 

1240 }}); 

1241  

1242 function showError(message) {{ 

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

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

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

1246 }} 

1247  

1248 function showSuccess(message) {{ 

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

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

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

1252 }} 

1253  

1254 // Load skill when page loads 

1255 loadSkill(); 

1256 </script> 

1257</body> 

1258</html>""" 

1259 

1260 def _get_new_skill_html(self) -> str: 

1261 """Generate the new skill creation interface HTML.""" 

1262 return """<!DOCTYPE html> 

1263<html lang="en"> 

1264<head> 

1265 <meta charset="utf-8"> 

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

1267 <title>Create New Skill - OllamaPy</title> 

1268 <style> 

1269 body { 

1270 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 

1271 margin: 0; 

1272 padding: 20px; 

1273 background: #f5f7fa; 

1274 } 

1275 .container { 

1276 max-width: 1000px; 

1277 margin: 0 auto; 

1278 background: white; 

1279 border-radius: 10px; 

1280 box-shadow: 0 4px 20px rgba(0,0,0,0.1); 

1281 } 

1282 .header { 

1283 background: linear-gradient(135deg, #28a745 0%, #20c997 100%); 

1284 color: white; 

1285 padding: 30px; 

1286 border-radius: 10px 10px 0 0; 

1287 } 

1288 .form-section { 

1289 padding: 30px; 

1290 } 

1291 .form-group { 

1292 margin-bottom: 20px; 

1293 } 

1294 .form-group label { 

1295 display: block; 

1296 margin-bottom: 8px; 

1297 font-weight: 600; 

1298 color: #333; 

1299 } 

1300 .form-control { 

1301 width: 100%; 

1302 padding: 12px; 

1303 border: 2px solid #e2e8f0; 

1304 border-radius: 6px; 

1305 font-size: 14px; 

1306 transition: border-color 0.2s; 

1307 } 

1308 .form-control:focus { 

1309 outline: none; 

1310 border-color: #28a745; 

1311 } 

1312 textarea.form-control { 

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

1314 resize: vertical; 

1315 } 

1316 .btn { 

1317 background: #28a745; 

1318 color: white; 

1319 border: none; 

1320 padding: 12px 24px; 

1321 border-radius: 6px; 

1322 cursor: pointer; 

1323 font-size: 16px; 

1324 margin-right: 10px; 

1325 transition: background 0.2s; 

1326 } 

1327 .btn:hover { 

1328 background: #218838; 

1329 } 

1330 .btn-secondary { 

1331 background: #6c757d; 

1332 } 

1333 .btn-secondary:hover { 

1334 background: #5a6268; 

1335 } 

1336 .btn-success { 

1337 background: #17a2b8; 

1338 } 

1339 .btn-success:hover { 

1340 background: #138496; 

1341 } 

1342 .parameter-editor { 

1343 border: 2px solid #e2e8f0; 

1344 border-radius: 6px; 

1345 padding: 20px; 

1346 margin-top: 10px; 

1347 } 

1348 .parameter-item { 

1349 display: flex; 

1350 gap: 10px; 

1351 margin-bottom: 15px; 

1352 align-items: end; 

1353 } 

1354 .parameter-item input, .parameter-item select { 

1355 flex: 1; 

1356 padding: 8px; 

1357 border: 1px solid #ddd; 

1358 border-radius: 4px; 

1359 } 

1360 .parameter-item button { 

1361 background: #dc3545; 

1362 color: white; 

1363 border: none; 

1364 padding: 8px 12px; 

1365 border-radius: 4px; 

1366 cursor: pointer; 

1367 } 

1368 .template-section { 

1369 background: #e7f5ff; 

1370 padding: 20px; 

1371 border-radius: 6px; 

1372 margin-bottom: 20px; 

1373 } 

1374 .template-btn { 

1375 background: #007bff; 

1376 color: white; 

1377 border: none; 

1378 padding: 8px 16px; 

1379 border-radius: 4px; 

1380 cursor: pointer; 

1381 margin-right: 10px; 

1382 margin-bottom: 10px; 

1383 } 

1384 .test-section { 

1385 background: #f8f9fa; 

1386 padding: 20px; 

1387 border-radius: 6px; 

1388 margin-top: 20px; 

1389 } 

1390 .test-output { 

1391 background: #2d3748; 

1392 color: #e2e8f0; 

1393 padding: 15px; 

1394 border-radius: 6px; 

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

1396 white-space: pre-wrap; 

1397 margin-top: 10px; 

1398 max-height: 300px; 

1399 overflow-y: auto; 

1400 } 

1401 .error { 

1402 background: #f8d7da; 

1403 color: #721c24; 

1404 padding: 10px; 

1405 border-radius: 4px; 

1406 margin: 10px 0; 

1407 } 

1408 .success { 

1409 background: #d4edda; 

1410 color: #155724; 

1411 padding: 10px; 

1412 border-radius: 4px; 

1413 margin: 10px 0; 

1414 } 

1415 </style> 

1416</head> 

1417<body> 

1418 <div class="container"> 

1419 <div class="header"> 

1420 <h1>➕ Create New Skill</h1> 

1421 <p>Build a custom skill for your OllamaPy system</p> 

1422 </div> 

1423  

1424 <div class="form-section"> 

1425 <div class="template-section"> 

1426 <h3>Quick Start Templates</h3> 

1427 <p>Choose a template to get started faster:</p> 

1428 <button class="template-btn" onclick="loadTemplate('simple')">Simple Action</button> 

1429 <button class="template-btn" onclick="loadTemplate('calculation')">Mathematical Function</button> 

1430 <button class="template-btn" onclick="loadTemplate('file_operation')">File Operation</button> 

1431 <button class="template-btn" onclick="loadTemplate('api_call')">API Call</button> 

1432 </div> 

1433  

1434 <form id="skill-form"> 

1435 <div class="form-group"> 

1436 <label>Skill Name *</label> 

1437 <input type="text" id="skill-name" class="form-control" required placeholder="e.g., myCustomSkill"> 

1438 <small>Use camelCase or snake_case. No spaces or special characters.</small> 

1439 </div> 

1440  

1441 <div class="form-group"> 

1442 <label>Description *</label> 

1443 <textarea id="skill-description" class="form-control" rows="3" required placeholder="Describe when this skill should be used. Be specific about the trigger conditions."></textarea> 

1444 </div> 

1445  

1446 <div class="form-group"> 

1447 <label>Role</label> 

1448 <select id="skill-role" class="form-control"> 

1449 <option value="general">General</option> 

1450 <option value="text_processing">Text Processing</option> 

1451 <option value="mathematics">Mathematics</option> 

1452 <option value="data_analysis">Data Analysis</option> 

1453 <option value="file_operations">File Operations</option> 

1454 <option value="web_utilities">Web Utilities</option> 

1455 <option value="time_date">Time & Date</option> 

1456 <option value="formatting">Formatting</option> 

1457 <option value="validation">Validation</option> 

1458 <option value="advanced">Advanced</option> 

1459 </select> 

1460 </div> 

1461  

1462 <div class="form-group"> 

1463 <label>Vibe Test Phrases</label> 

1464 <textarea id="vibe-phrases" class="form-control" rows="5" placeholder="Enter test phrases that should trigger this skill (one per line)&#10;e.g., 'calculate the square of 5'&#10; 'what is 5 squared?'"></textarea> 

1465 <small>These help the AI know when to use your skill. Include various ways users might ask for this functionality.</small> 

1466 </div> 

1467  

1468 <div class="form-group"> 

1469 <label>Parameters</label> 

1470 <div class="parameter-editor"> 

1471 <div id="parameters-container"> 

1472 <!-- Parameters will be added here dynamically --> 

1473 </div> 

1474 <button type="button" onclick="addParameter()" class="btn btn-secondary">Add Parameter</button> 

1475 </div> 

1476 </div> 

1477  

1478 <div class="form-group"> 

1479 <label>Function Code *</label> 

1480 <textarea id="function-code" class="form-control" rows="15" required placeholder="def execute():&#10; # Import the log function to output results&#10; from ollamapy.skills import log&#10; &#10; # Your implementation here&#10; result = 'Hello, World!'&#10; log(f'[MySkill] Result: {result}')"></textarea> 

1481 <small>Your function must be named 'execute' and use log() to output results that the AI can see.</small> 

1482 </div> 

1483  

1484 <div class="test-section"> 

1485 <h3>Test Skill</h3> 

1486 <button type="button" onclick="testSkill()" class="btn btn-success">Test Execution</button> 

1487 <div id="test-output" class="test-output" style="display: none;"></div> 

1488 </div> 

1489  

1490 <div style="margin-top: 30px;"> 

1491 <button type="submit" class="btn">Create Skill</button> 

1492 <button type="button" onclick="location.href='/'" class="btn btn-secondary">Cancel</button> 

1493 </div> 

1494 </form> 

1495 </div> 

1496  

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

1498 </div> 

1499  

1500 <script> 

1501 function loadTemplate(templateType) { 

1502 const templates = { 

1503 simple: { 

1504 name: 'sayHello', 

1505 description: 'Use when the user wants to be greeted or says hello', 

1506 role: 'general', 

1507 vibe_phrases: ['say hello\\nhello there\\ngreet me'], 

1508 parameters: {}, 

1509 code: `def execute():\n from ollamapy.skills import log\n log('[SayHello] Hello! Nice to meet you!')` 

1510 }, 

1511 calculation: { 

1512 name: 'powerOf', 

1513 description: 'Calculate a number raised to a power (exponentiation)', 

1514 role: 'mathematics', 

1515 vibe_phrases: ['2 to the power of 3\\nwhat is 5 raised to 2\\ncalculate 4^3'], 

1516 parameters: { 

1517 base: {type: 'number', required: true, description: 'The base number'}, 

1518 exponent: {type: 'number', required: true, description: 'The exponent'} 

1519 }, 

1520 code: `def execute(base=None, exponent=None):\n from ollamapy.skills import log\n \n if base is None or exponent is None:\n log('[PowerOf] Error: Both base and exponent are required')\n return\n \n try:\n result = base ** exponent\n log(f'[PowerOf] {base} to the power of {exponent} = {result}')\n except Exception as e:\n log(f'[PowerOf] Error calculating power: {e}')` 

1521 }, 

1522 file_operation: { 

1523 name: 'countLines', 

1524 description: 'Count the number of lines in a text file', 

1525 role: 'file_operations', 

1526 vibe_phrases: ['count lines in file\\nhow many lines are in this file\\nline count of file'], 

1527 parameters: { 

1528 file_path: {type: 'string', required: true, description: 'Path to the file to count lines in'} 

1529 }, 

1530 code: `def execute(file_path=None):\n from ollamapy.skills import log\n \n if not file_path:\n log('[CountLines] Error: File path is required')\n return\n \n try:\n with open(file_path, 'r') as f:\n line_count = sum(1 for line in f)\n log(f'[CountLines] File {file_path} has {line_count} lines')\n except FileNotFoundError:\n log(f'[CountLines] Error: File not found: {file_path}')\n except Exception as e:\n log(f'[CountLines] Error reading file: {e}')` 

1531 }, 

1532 api_call: { 

1533 name: 'getRandomFact', 

1534 description: 'Get a random interesting fact from an API', 

1535 role: 'web_utilities', 

1536 vibe_phrases: ['tell me a random fact\\nget me an interesting fact\\nshare a fun fact'], 

1537 parameters: {}, 

1538 code: `def execute():\n from ollamapy.skills import log\n import json\n \n try:\n # This is a placeholder - replace with actual API call\n facts = [\n "Honey never spoils. Archaeologists have found edible honey in ancient Egyptian tombs.",\n "A group of flamingos is called a 'flamboyance'.",\n "Octopuses have three hearts and blue blood."\n ]\n \n import random\n fact = random.choice(facts)\n log(f'[RandomFact] Here\\'s a random fact: {fact}')\n \n except Exception as e:\n log(f'[RandomFact] Error getting fact: {e}')` 

1539 } 

1540 }; 

1541  

1542 const template = templates[templateType]; 

1543 if (!template) return; 

1544  

1545 document.getElementById('skill-name').value = template.name; 

1546 document.getElementById('skill-description').value = template.description; 

1547 document.getElementById('skill-role').value = template.role; 

1548 document.getElementById('vibe-phrases').value = template.vibe_phrases; 

1549 document.getElementById('function-code').value = template.code; 

1550  

1551 // Clear existing parameters and add template parameters 

1552 document.getElementById('parameters-container').innerHTML = ''; 

1553 if (template.parameters) { 

1554 Object.entries(template.parameters).forEach(([name, info]) => { 

1555 addParameter(name, info.type, info.required, info.description); 

1556 }); 

1557 } 

1558 } 

1559  

1560 function addParameter(name = '', type = 'string', required = false, description = '') { 

1561 const container = document.getElementById('parameters-container'); 

1562 const div = document.createElement('div'); 

1563 div.className = 'parameter-item'; 

1564 div.innerHTML = ` 

1565 <input type="text" placeholder="Parameter name" value="${name}"> 

1566 <select> 

1567 <option value="string" ${type === 'string' ? 'selected' : ''}>String</option> 

1568 <option value="number" ${type === 'number' ? 'selected' : ''}>Number</option> 

1569 <option value="boolean" ${type === 'boolean' ? 'selected' : ''}>Boolean</option> 

1570 </select> 

1571 <label><input type="checkbox" ${required ? 'checked' : ''}> Required</label> 

1572 <input type="text" placeholder="Description" value="${description}"> 

1573 <button type="button" onclick="removeParameter(this)">Remove</button> 

1574 `; 

1575 container.appendChild(div); 

1576 } 

1577  

1578 function removeParameter(button) { 

1579 button.parentElement.remove(); 

1580 } 

1581  

1582 function collectParameters() { 

1583 const parameters = {}; 

1584 const items = document.querySelectorAll('.parameter-item'); 

1585  

1586 items.forEach(item => { 

1587 const inputs = item.querySelectorAll('input'); 

1588 const select = item.querySelector('select'); 

1589 const name = inputs[0].value.trim(); 

1590  

1591 if (name) { 

1592 parameters[name] = { 

1593 type: select.value, 

1594 required: inputs[1].checked, 

1595 description: inputs[2].value.trim() 

1596 }; 

1597 } 

1598 }); 

1599  

1600 return parameters; 

1601 } 

1602  

1603 async function testSkill() { 

1604 const skillData = { 

1605 name: document.getElementById('skill-name').value, 

1606 description: document.getElementById('skill-description').value, 

1607 role: document.getElementById('skill-role').value, 

1608 vibe_test_phrases: document.getElementById('vibe-phrases').value.split('\\n').filter(p => p.trim()), 

1609 parameters: collectParameters(), 

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

1611 verified: false, 

1612 scope: 'local' 

1613 }; 

1614  

1615 try { 

1616 const response = await fetch('/api/skills/test', { 

1617 method: 'POST', 

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

1619 body: JSON.stringify({skill_data: skillData, test_input: {}}) 

1620 }); 

1621  

1622 const data = await response.json(); 

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

1624  

1625 if (data.success) { 

1626 if (data.execution_successful) { 

1627 output.textContent = 'Test passed!\\n\\nOutput:\\n' + data.output.join('\\n'); 

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

1629 } else { 

1630 output.textContent = 'Test failed:\\n' + data.error; 

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

1632 } 

1633 } else { 

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

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

1636 } 

1637  

1638 output.style.display = 'block'; 

1639 } catch (error) { 

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

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

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

1643 output.style.display = 'block'; 

1644 } 

1645 } 

1646  

1647 document.getElementById('skill-form').addEventListener('submit', async (e) => { 

1648 e.preventDefault(); 

1649  

1650 const skillName = document.getElementById('skill-name').value.trim(); 

1651 if (!skillName.match(/^[a-zA-Z_][a-zA-Z0-9_]*$/)) { 

1652 showError('Skill name must be a valid identifier (no spaces or special characters except underscore)'); 

1653 return; 

1654 } 

1655  

1656 const formData = { 

1657 name: skillName, 

1658 description: document.getElementById('skill-description').value, 

1659 role: document.getElementById('skill-role').value, 

1660 vibe_test_phrases: document.getElementById('vibe-phrases').value.split('\\n').filter(p => p.trim()), 

1661 parameters: collectParameters(), 

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

1663 verified: false, 

1664 scope: 'local', 

1665 tags: [] 

1666 }; 

1667  

1668 try { 

1669 const response = await fetch('/api/skills', { 

1670 method: 'POST', 

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

1672 body: JSON.stringify(formData) 

1673 }); 

1674  

1675 const data = await response.json(); 

1676  

1677 if (data.success) { 

1678 showSuccess('Skill created successfully!'); 

1679 setTimeout(() => { 

1680 location.href = '/skill/' + skillName; 

1681 }, 2000); 

1682 } else { 

1683 showError('Failed to create skill: ' + data.error); 

1684 if (data.validation_errors) { 

1685 showError('Validation errors: ' + data.validation_errors.join(', ')); 

1686 } 

1687 } 

1688 } catch (error) { 

1689 showError('Error creating skill: ' + error.message); 

1690 } 

1691 }); 

1692  

1693 function showError(message) { 

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

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

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

1697 } 

1698  

1699 function showSuccess(message) { 

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

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

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

1703 } 

1704 </script> 

1705</body> 

1706</html>""" 

1707 

1708 def run(self): 

1709 """Run the Flask development server.""" 

1710 print(f"🚀 Starting Skill Editor API on http://localhost:{self.port}") 

1711 print(f"💾 Skills directory: {self.registry.skills_dir}") 

1712 print(f"📊 Loaded {len(self.registry.skills)} skills") 

1713 self.app.run(debug=True, port=self.port, host="0.0.0.0")