| import typer | |
| import os | |
| from pathlib import Path | |
| from rich.console import Console | |
| from rich.table import Table | |
| from rich.panel import Panel | |
| from rich.columns import Columns | |
| from skillnet_ai.creator import SkillCreator | |
| from skillnet_ai.downloader import SkillDownloader | |
| from skillnet_ai.evaluator import SkillEvaluator, EvaluatorConfig | |
| from skillnet_ai.searcher import SkillNetSearcher | |
| from skillnet_ai.analyzer import SkillRelationshipAnalyzer | |
| app = typer.Typer(help="SkillNet AI CLI Tool") | |
| console = Console() | |
| API_KEY = os.getenv("API_KEY") | |
| BASE_URL = os.getenv("BASE_URL") or "https://api.openai.com/v1" | |
| def search( | |
| q: str = typer.Argument(..., help="The search query (keywords or natural language description)."), | |
| mode: str = typer.Option("keyword", help="Search mode: 'keyword' (exact/fuzzy) or 'vector' (semantic AI)."), | |
| category: str = typer.Option(None, help="Filter results by category (e.g., 'Development')."), | |
| limit: int = typer.Option(20, help="Maximum number of results to return."), | |
| # Keyword specific options | |
| page: int = typer.Option(1, help="Page number (only for keyword mode)."), | |
| min_stars: int = typer.Option(0, help="Minimum star rating (only for keyword mode)."), | |
| sort_by: str = typer.Option("stars", help="Sort criteria: 'stars' or 'recent' (only for keyword mode)."), | |
| # Vector specific options | |
| threshold: float = typer.Option(0.8, help="Similarity threshold 0.0-1.0 (only for vector mode)."), | |
| ): | |
| """ | |
| Search for skills on SkillNet using Keyword match or Vector (AI) semantic search. | |
| """ | |
| try: | |
| # Initialize Searcher (Ensure URL points to your actual API) | |
| searcher = SkillNetSearcher() | |
| # Visual feedback during API call | |
| with console.status(f"[bold green]Searching SkillNet ({mode} mode)..."): | |
| results = searcher.search( | |
| q=q, | |
| mode=mode, # type: ignore | |
| category=category, | |
| limit=limit, | |
| page=page, | |
| min_stars=min_stars, | |
| sort_by=sort_by, | |
| threshold=threshold | |
| ) | |
| # Handle Empty Results | |
| if not results: | |
| console.print(f"[yellow]No results found for query: '{q}'[/yellow]") | |
| return | |
| # Build Output Table | |
| table = Table(title=f"Search Results: {q} ({len(results)} items)", show_lines=True) | |
| # Define Columns | |
| table.add_column("Name", style="cyan", no_wrap=True) | |
| table.add_column("Category", style="magenta") | |
| table.add_column("Stars", justify="right", style="green") | |
| table.add_column("Description", style="white") | |
| table.add_column("Evaluation", justify="left", style="yellow") | |
| table.add_column("URL", style="dim blue", overflow="fold") # Added URL column | |
| for item in results: | |
| name = item.skill_name | |
| cat = item.category if item.category else "N/A" | |
| stars = str(item.stars) | |
| desc = item.skill_description if item.skill_description else "" | |
| url = item.skill_url if item.skill_url else "N/A" | |
| if url != "N/A": | |
| url = f"[link={url}]{url}[/link]" | |
| # Truncate long descriptions for display | |
| short_desc = (desc[:100] + '...') if len(desc) > 100 else desc | |
| eval_lines = [] | |
| if item.evaluation and isinstance(item.evaluation, dict): | |
| metrics = { | |
| "safety": "Safety", | |
| "executability": "Executability", | |
| "completeness": "Completeness", | |
| "maintainability": "Maintainability", | |
| "cost_awareness": "Cost-Awareness" | |
| } | |
| for key, display_name in metrics.items(): | |
| level = item.evaluation.get(key, {}).get("level", "N/A") | |
| eval_lines.append(f"{display_name}: {level}") | |
| eval_full_str = "\n".join(eval_lines) if eval_lines else "N/A" | |
| # Prepare row data | |
| row_data = [ | |
| name, | |
| cat, | |
| stars, | |
| short_desc, | |
| eval_full_str, | |
| url | |
| ] | |
| table.add_row(*row_data) | |
| console.print(table) | |
| # Suggest next step | |
| console.print("\n[dim]Tip: Use 'skillnet download <skill_url>' to get a skill.[/dim]") | |
| except Exception as e: | |
| console.print(f"[bold red]Error during search:[/bold red] {str(e)}") | |
| # Optional: Print full traceback for debugging | |
| # console.print_exception() | |
| raise typer.Exit(code=1) | |
| def download( | |
| url: str = typer.Argument(..., help="The GitHub URL of the specific skill folder (e.g., https://github.com/owner/repo/tree/main/skills/math_solver)."), | |
| target_dir: str = typer.Option(".", "--target-dir", "-d", help="Local directory to install the skill into."), | |
| token: str = typer.Option(None, "--token", "-t", envvar="GITHUB_TOKEN", help="GitHub Personal Access Token (for private repos or higher rate limits)."), | |
| ): | |
| """ | |
| Download and install a specific skill directly from a GitHub repository subdirectory. | |
| """ | |
| # 1. Initialize Downloader | |
| # Checks CLI option first, then environment variable GITHUB_TOKEN | |
| downloader = SkillDownloader(api_token=token) | |
| try: | |
| # 2. Visual Feedback | |
| console.print(f"[dim]Target directory: {os.path.abspath(target_dir)}[/dim]") | |
| with console.status(f"[bold green]Downloading skill from GitHub...[/bold green]", spinner="dots"): | |
| installed_path = downloader.download(folder_url=url, target_dir=target_dir) | |
| # 3. Handle Results | |
| if installed_path: | |
| # Success | |
| folder_name = os.path.basename(installed_path) | |
| table = Table(title="Installation Successful", show_header=False, box=None) | |
| table.add_row("[bold cyan]Skill:[/bold cyan]", folder_name) | |
| table.add_row("[bold cyan]Location:[/bold cyan]", installed_path) | |
| console.print(table) | |
| console.print(f"\n[green]✅ {folder_name} is ready to use.[/green]") | |
| else: | |
| # Failure (Logic handled inside class, but we catch the None return) | |
| console.print("[bold red]❌ Download Failed.[/bold red]") | |
| console.print("Possible reasons:") | |
| console.print("1. Please check your network settings and ensure connection to GitHub is working properly.") | |
| console.print("2. The URL format is incorrect (must point to a specific skill folder, not just the repo root).") | |
| console.print("3. The repository is private and no token was provided.") | |
| console.print("4. GitHub API rate limits exceeded (try providing a token).") | |
| raise typer.Exit(code=1) | |
| except Exception as e: | |
| console.print(f"[bold red]An unexpected error occurred:[/bold red] {str(e)}") | |
| raise typer.Exit(code=1) | |
| def create( | |
| # Input sources (mutually exclusive) | |
| trajectory_file: Path = typer.Argument(None, exists=True, readable=True, help="Path to trajectory/log file."), | |
| github: str = typer.Option(None, "--github", "-g", help="GitHub repository URL (e.g., https://github.com/owner/repo)."), | |
| office: Path = typer.Option(None, "--office", "-o", exists=True, readable=True, help="Path to office document (PDF, PPT, Word)."), | |
| prompt: str = typer.Option(None, "--prompt", "-p", help="Direct description to generate skill from."), | |
| # Output options | |
| output_dir: Path = typer.Option(Path("./generated_skills"), "--output-dir", "-d", help="Directory to save generated skills."), | |
| # Model options | |
| model: str = typer.Option("gpt-4o", "--model", "-m", help="LLM model to use (e.g., gpt-4o, gpt-3.5-turbo)."), | |
| max_files: int = typer.Option(50, "--max-files", help="Max code files to analyze (--github only)."), | |
| ): | |
| """ | |
| Create executable Skill packages using AI. | |
| Supports four modes: | |
| - From trajectory: skillnet create trajectory.txt | |
| - From GitHub: skillnet create --github https://github.com/owner/repo | |
| - From Office doc: skillnet create --office document.pdf | |
| - From prompt: skillnet create --prompt "Create a skill for..." | |
| """ | |
| # 1. Validate Environment | |
| if not API_KEY: | |
| console.print("[bold red]Error:[/bold red] API_KEY environment variable is not set.") | |
| console.print("Please export API_KEY or set it in your environment.") | |
| raise typer.Exit(code=1) | |
| # 2. Determine mode based on provided options | |
| mode_count = sum([ | |
| bool(github), | |
| bool(trajectory_file), | |
| bool(office), | |
| bool(prompt) | |
| ]) | |
| if mode_count == 0: | |
| console.print("[bold red]Error:[/bold red] Must specify one input source.") | |
| console.print("\nUsage examples:") | |
| console.print(" skillnet create trajectory.txt") | |
| console.print(" skillnet create --github https://github.com/owner/repo") | |
| console.print(" skillnet create --office document.pdf") | |
| console.print(' skillnet create --prompt "Create a skill for web scraping"') | |
| raise typer.Exit(code=1) | |
| if mode_count > 1: | |
| console.print("[bold red]Error:[/bold red] Only one input source can be specified at a time.") | |
| raise typer.Exit(code=1) | |
| # 3. Route to appropriate handler | |
| if github: | |
| _create_from_github(github, output_dir, model, max_files) | |
| elif trajectory_file: | |
| _create_from_trajectory(trajectory_file, output_dir, model) | |
| elif office: | |
| _create_from_office(office, output_dir, model) | |
| elif prompt: | |
| _create_from_prompt(prompt, output_dir, model) | |
| def _create_from_trajectory(trajectory_file: Path, output_dir: Path, model: str): | |
| """Internal function to create skill from trajectory file.""" | |
| try: | |
| # Read Trajectory Content | |
| console.print(f"[dim]Reading trajectory from: {trajectory_file}[/dim]") | |
| with open(trajectory_file, "r", encoding="utf-8") as f: | |
| trajectory_content = f.read() | |
| if not trajectory_content.strip(): | |
| console.print("[bold red]Error:[/bold red] Trajectory file is empty.") | |
| raise typer.Exit(code=1) | |
| # Initialize Creator | |
| creator = SkillCreator( | |
| api_key=API_KEY, | |
| base_url=BASE_URL, | |
| model=model | |
| ) | |
| # Run Generation with Spinner | |
| with console.status("[bold green]AI is analyzing trajectory and generating skills...[/bold green]", spinner="dots"): | |
| created_paths = creator.create_from_trajectory( | |
| trajectory=trajectory_content, | |
| output_dir=str(output_dir) | |
| ) | |
| # Report Results | |
| if created_paths: | |
| console.print(f"\n[bold green]Success! Generated {len(created_paths)} skill(s):[/bold green]") | |
| table = Table(show_header=True, header_style="bold magenta") | |
| table.add_column("Skill Name", style="cyan") | |
| table.add_column("Location", style="white") | |
| for path in created_paths: | |
| skill_name = os.path.basename(path) | |
| table.add_row(skill_name, str(path)) | |
| console.print(table) | |
| console.print(f"\n[dim]Files saved to: {os.path.abspath(output_dir)}[/dim]") | |
| else: | |
| console.print("\n[yellow]Analysis complete, but no clear skills were identified in this trajectory.[/yellow]") | |
| except Exception as e: | |
| console.print(f"\n[bold red]Creation Failed:[/bold red] {str(e)}") | |
| raise typer.Exit(code=1) | |
| def _create_from_github(github_url: str, output_dir: Path, model: str, max_files: int): | |
| """Internal function to create skill from GitHub repository.""" | |
| try: | |
| console.print(f"[dim]Creating skill from GitHub: {github_url}[/dim]") | |
| # Initialize Creator | |
| creator = SkillCreator( | |
| api_key=API_KEY, | |
| base_url=BASE_URL, | |
| model=model | |
| ) | |
| # Run Generation with Spinner | |
| with console.status("[bold green]Fetching repository and generating skill...[/bold green]", spinner="dots"): | |
| created_paths = creator.create_from_github( | |
| github_url=github_url, | |
| output_dir=str(output_dir), | |
| api_token=os.getenv("GITHUB_TOKEN"), | |
| max_files=max_files | |
| ) | |
| # Report Results | |
| if created_paths: | |
| console.print(f"\n[bold green]Success! Generated {len(created_paths)} skill(s) from GitHub:[/bold green]") | |
| table = Table(show_header=True, header_style="bold magenta") | |
| table.add_column("Skill Name", style="cyan") | |
| table.add_column("Location", style="white") | |
| for path in created_paths: | |
| skill_name = os.path.basename(path) | |
| table.add_row(skill_name, str(path)) | |
| console.print(table) | |
| console.print(f"\n[dim]Files saved to: {os.path.abspath(output_dir)}[/dim]") | |
| console.print("\n[dim]Tip: Use 'skillnet evaluate <skill_path>' to evaluate the generated skill.[/dim]") | |
| else: | |
| console.print("\n[yellow]Failed to generate skill from the GitHub repository.[/yellow]") | |
| except Exception as e: | |
| console.print(f"\n[bold red]GitHub Skill Creation Failed:[/bold red] {str(e)}") | |
| raise typer.Exit(code=1) | |
| def _create_from_office(office_file: Path, output_dir: Path, model: str): | |
| """Internal function to create skill from office document.""" | |
| try: | |
| console.print(f"[dim]Creating skill from office document: {office_file}[/dim]") | |
| # Initialize Creator | |
| creator = SkillCreator( | |
| api_key=API_KEY, | |
| base_url=BASE_URL, | |
| model=model | |
| ) | |
| # Run Generation with Spinner | |
| with console.status("[bold green]Extracting content and generating skill...[/bold green]", spinner="dots"): | |
| created_paths = creator.create_from_office( | |
| file_path=str(office_file), | |
| output_dir=str(output_dir) | |
| ) | |
| # Report Results | |
| if created_paths: | |
| console.print(f"\n[bold green]Success! Generated {len(created_paths)} skill(s) from document:[/bold green]") | |
| table = Table(show_header=True, header_style="bold magenta") | |
| table.add_column("Skill Name", style="cyan") | |
| table.add_column("Location", style="white") | |
| for path in created_paths: | |
| skill_name = os.path.basename(path) | |
| table.add_row(skill_name, str(path)) | |
| console.print(table) | |
| console.print(f"\n[dim]Files saved to: {os.path.abspath(output_dir)}[/dim]") | |
| console.print("\n[dim]Tip: Use 'skillnet evaluate <skill_path>' to evaluate the generated skill.[/dim]") | |
| else: | |
| console.print("\n[yellow]Failed to generate skill from the document.[/yellow]") | |
| except ImportError as e: | |
| console.print(f"\n[bold red]Missing Dependency:[/bold red] {str(e)}") | |
| console.print("\n[dim]Install office document support with:[/dim]") | |
| console.print(" pip install PyPDF2 pycryptodome python-docx python-pptx") | |
| raise typer.Exit(code=1) | |
| except Exception as e: | |
| console.print(f"\n[bold red]Office Skill Creation Failed:[/bold red] {str(e)}") | |
| raise typer.Exit(code=1) | |
| def _create_from_prompt(user_prompt: str, output_dir: Path, model: str): | |
| """Internal function to create skill from user's prompt description.""" | |
| try: | |
| console.print(f"[dim]Creating skill from user prompt...[/dim]") | |
| # Initialize Creator | |
| creator = SkillCreator( | |
| api_key=API_KEY, | |
| base_url=BASE_URL, | |
| model=model | |
| ) | |
| # Run Generation with Spinner | |
| with console.status("[bold green]AI is generating your custom skill...[/bold green]", spinner="dots"): | |
| created_paths = creator.create_from_prompt( | |
| user_input=user_prompt, | |
| output_dir=str(output_dir) | |
| ) | |
| # Report Results | |
| if created_paths: | |
| console.print(f"\n[bold green]Success! Generated {len(created_paths)} skill(s) from your description:[/bold green]") | |
| table = Table(show_header=True, header_style="bold magenta") | |
| table.add_column("Skill Name", style="cyan") | |
| table.add_column("Location", style="white") | |
| for path in created_paths: | |
| skill_name = os.path.basename(path) | |
| table.add_row(skill_name, str(path)) | |
| console.print(table) | |
| console.print(f"\n[dim]Files saved to: {os.path.abspath(output_dir)}[/dim]") | |
| console.print("\n[dim]Tip: Use 'skillnet evaluate <skill_path>' to evaluate the generated skill.[/dim]") | |
| else: | |
| console.print("\n[yellow]Failed to generate skill from your description.[/yellow]") | |
| except Exception as e: | |
| console.print(f"\n[bold red]Prompt-based Skill Creation Failed:[/bold red] {str(e)}") | |
| raise typer.Exit(code=1) | |
| def evaluate( | |
| target: str = typer.Argument(..., help="Path to a local skill directory OR a GitHub URL."), | |
| # Optional metadata overrides (useful if not auto-detected) | |
| name: str = typer.Option(None, help="Name of the skill (overrides auto-detection)."), | |
| category: str = typer.Option(None, help="Category of the skill (e.g., 'Data Analysis')."), | |
| description: str = typer.Option(None, help="Short description of what the skill does."), | |
| # Config options | |
| model: str = typer.Option("gpt-4o", "--model", "-m", help="LLM model to use."), | |
| max_workers: int = typer.Option(5, help="Concurrency for batch operations (not used for single eval)."), | |
| ): | |
| """ | |
| Evaluate the quality, safety, and completeness of a skill using AI. | |
| Target can be a local folder path or a GitHub URL (e.g., https://github.com/user/repo/tree/main/skill). | |
| """ | |
| # 1. Validate Environment | |
| if not API_KEY: | |
| console.print("[bold red]Error:[/bold red] API_KEY environment variable is not set.") | |
| raise typer.Exit(code=1) | |
| # 2. Configure Evaluator | |
| config = EvaluatorConfig( | |
| api_key=API_KEY, | |
| base_url=BASE_URL, | |
| model=model, | |
| max_workers=max_workers | |
| ) | |
| evaluator = SkillEvaluator(config) | |
| try: | |
| # 3. Determine Mode (URL vs Local Path) and Run Evaluation | |
| is_url = target.startswith("http://") or target.startswith("https://") | |
| with console.status(f"[bold green]Evaluating skill ({'Remote' if is_url else 'Local'})...[/bold green]", spinner="dots"): | |
| if is_url: | |
| result = evaluator.evaluate_from_url( | |
| url=target, | |
| name=name, | |
| category=category, | |
| description=description | |
| ) | |
| else: | |
| result = evaluator.evaluate_from_path( | |
| path=target, | |
| name=name, | |
| category=category, | |
| description=description | |
| ) | |
| # 4. Display Results | |
| if "error" in result: | |
| console.print(f"[bold red]Evaluation Failed:[/bold red] {result['error']}") | |
| raise typer.Exit(code=1) | |
| _display_evaluation_report(target, result) | |
| except Exception as e: | |
| console.print(f"[bold red]An unexpected error occurred:[/bold red] {str(e)}") | |
| raise typer.Exit(code=1) | |
| def _display_evaluation_report(target_name: str, data: dict): | |
| """Helper to render the JSON evaluation result into a nice Rich UI.""" | |
| console.print(f"\n[bold underline]Evaluation Report: {os.path.basename(target_name)}[/bold underline]\n") | |
| # Dimensions to display | |
| dimensions = ["safety", "completeness", "executability", "maintainability", "cost_awareness"] | |
| # Create a grid of panels | |
| panels = [] | |
| for dim in dimensions: | |
| info = data.get(dim, {}) | |
| level = info.get("level", "Unknown") | |
| reason = info.get("reason", "No details provided.") | |
| # Color coding based on level | |
| color = "white" | |
| if "Excellent" in level: color = "green" | |
| elif "Good" in level: color = "blue" | |
| elif "Fair" in level: color = "yellow" | |
| elif "Poor" in level: color = "red" | |
| panel_content = f"[bold]{level}[/bold]\n\n[dim]{reason}[/dim]" | |
| panels.append(Panel(panel_content, title=f"[{color}]{dim.title()}[/{color}]", expand=True)) | |
| console.print(Columns(panels, equal=True, expand=True)) | |
| # Display Score if present | |
| overall_score = data.get("overall_score") | |
| if overall_score: | |
| score_color = "green" if overall_score >= 8 else "yellow" if overall_score >= 5 else "red" | |
| console.print(f"\n[bold]Overall Score:[/bold] [{score_color}]{overall_score}/10[/{score_color}]") | |
| # Summary | |
| summary = data.get("summary") | |
| if summary: | |
| console.print(Panel(summary, title="Executive Summary", border_style="cyan")) | |
| def analyze( | |
| skills_dir: Path = typer.Argument(..., exists=True, file_okay=False, help="Directory containing multiple skill folders to analyze."), | |
| save: bool = typer.Option(True, "--save/--no-save", help="Save the result to relationships.json in the directory."), | |
| model: str = typer.Option("gpt-4o", "--model", "-m", help="LLM model to use."), | |
| ): | |
| """ | |
| Analyze and map relationships (similar_to, belong_to, compose_with, depend_on) between local skills. | |
| This command scans all subdirectories in the target folder, reads their descriptions, | |
| and uses AI to build a knowledge graph of how the skills relate to each other. | |
| """ | |
| # 1. Validate Environment | |
| if not API_KEY: | |
| console.print("[bold red]Error:[/bold red] API_KEY environment variable is not set.") | |
| raise typer.Exit(code=1) | |
| try: | |
| # 2. Initialize Analyzer | |
| analyzer = SkillRelationshipAnalyzer( | |
| api_key=API_KEY, | |
| base_url=BASE_URL, | |
| model=model | |
| ) | |
| # 3. Visual Feedback & Execution | |
| console.print(f"[dim]Scanning directory: {os.path.abspath(skills_dir)}[/dim]") | |
| results = [] | |
| with console.status("[bold green]Reading skills and analyzing relationships...[/bold green]", spinner="earth"): | |
| results = analyzer.analyze_local_skills( | |
| skills_dir=str(skills_dir), | |
| save_to_file=save | |
| ) | |
| # 4. Handle Empty Results | |
| if not results: | |
| console.print("\n[yellow]No strong relationships detected among the skills found.[/yellow]") | |
| console.print("[dim]Make sure the directory contains subfolders with valid SKILL.md or README.md files.[/dim]") | |
| return | |
| # 5. Render Results Table | |
| console.print(f"\n[bold green]Analysis Complete! Found {len(results)} relationships:[/bold green]\n") | |
| table = Table(show_header=True, header_style="bold magenta", title="Skill Relationship Graph") | |
| table.add_column("Source Skill", style="cyan", no_wrap=True) | |
| table.add_column("Relationship", style="bold white", justify="center") | |
| table.add_column("Target Skill", style="cyan", no_wrap=True) | |
| table.add_column("Reasoning", style="dim") | |
| for edge in results: | |
| # Color code the relationship types for better readability | |
| rel_type = edge.get('type', 'unknown') | |
| rel_style = "white" | |
| arrow = "->" | |
| if rel_type == "depend_on": | |
| rel_style = "red" # Critical dependency | |
| arrow = "DEPEND ON" | |
| elif rel_type == "belong_to": | |
| rel_style = "blue" # Hierarchy | |
| arrow = "BELONG TO" | |
| elif rel_type == "compose_with": # pairs_with | |
| rel_style = "green" # Collaboration | |
| arrow = "COMPOSE WITH" | |
| elif rel_type == "similar_to": | |
| rel_style = "yellow" # Alternative | |
| arrow = "SIMILAR TO" | |
| table.add_row( | |
| edge.get('source', 'Unknown'), | |
| f"[{rel_style}]{arrow}[/{rel_style}]", | |
| edge.get('target', 'Unknown'), | |
| edge.get('reason', '') | |
| ) | |
| console.print(table) | |
| if save: | |
| console.print(f"\n[dim]Relationship data saved to: {os.path.join(skills_dir, 'relationships.json')}[/dim]") | |
| except Exception as e: | |
| console.print(f"\n[bold red]Analysis Failed:[/bold red] {str(e)}") | |
| raise typer.Exit(code=1) | |
| if __name__ == "__main__": | |
| app() | |
Xet Storage Details
- Size:
- 25.5 kB
- Xet hash:
- be3631daa28b524ec325f31f38a06b20218ba73357fe2652a44d353f5d5953ee
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.