Add comprehensive documentation and 28 example tools
- Updated README with full feature overview and examples - Added docs/INSTALL.md with detailed setup instructions - Added docs/PROVIDERS.md with 12 profiled providers - Added docs/EXAMPLES.md with all 28 tool configurations - Added examples/install.py for one-command tool installation - Updated pyproject.toml with better metadata - Added urwid TUI with provider descriptions in dropdown - Profiled all providers for speed/accuracy/cost 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
This commit is contained in:
+150
-27
@@ -1,9 +1,40 @@
|
||||
"""Provider abstraction for AI CLI tools."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
import yaml
|
||||
|
||||
|
||||
# Default providers config location
|
||||
PROVIDERS_FILE = Path.home() / ".smarttools" / "providers.yaml"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Provider:
|
||||
"""Definition of an AI provider."""
|
||||
name: str
|
||||
command: str
|
||||
description: str = ""
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"name": self.name,
|
||||
"command": self.command,
|
||||
"description": self.description,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "Provider":
|
||||
return cls(
|
||||
name=data["name"],
|
||||
command=data["command"],
|
||||
description=data.get("description", ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -14,29 +45,137 @@ class ProviderResult:
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
def call_provider(provider: str, args: str, prompt: str, timeout: int = 300) -> ProviderResult:
|
||||
# Default providers that come pre-configured
|
||||
# Profiled with 4-task test: Math, Code, Reasoning, Extract (Dec 2025)
|
||||
DEFAULT_PROVIDERS = [
|
||||
# TOP PICKS - best value/performance ratio
|
||||
Provider("opencode-deepseek", "$HOME/.opencode/bin/opencode run --model deepseek/deepseek-chat", "13s 4/4 | BEST VALUE, cheap, fast, accurate"),
|
||||
Provider("opencode-pickle", "$HOME/.opencode/bin/opencode run --model opencode/big-pickle", "13s 4/4 | BEST FREE, accurate"),
|
||||
Provider("claude-haiku", "claude -p --model haiku", "14s 4/4 | fast, accurate, best paid option"),
|
||||
Provider("codex", "codex exec -", "14s 4/4 | reliable, auto-routes"),
|
||||
|
||||
# CLAUDE - all accurate, paid, good for code
|
||||
Provider("claude", "claude -p", "18s 4/4 | auto-routes to best model"),
|
||||
Provider("claude-opus", "claude -p --model opus", "18s 4/4 | highest quality, expensive"),
|
||||
Provider("claude-sonnet", "claude -p --model sonnet", "21s 4/4 | balanced quality/speed"),
|
||||
|
||||
# OPENCODE - additional models
|
||||
Provider("opencode-nano", "$HOME/.opencode/bin/opencode run --model opencode/gpt-5-nano", "24s 4/4 | GPT-5 Nano, reliable"),
|
||||
Provider("opencode-reasoner", "$HOME/.opencode/bin/opencode run --model deepseek/deepseek-reasoner", "33s 4/4 | complex reasoning, cheap"),
|
||||
Provider("opencode-grok", "$HOME/.opencode/bin/opencode run --model opencode/grok-code", "11s 2/4 | fastest but unreliable, FREE"),
|
||||
|
||||
# GEMINI - slow CLI but good for large docs (1M token context)
|
||||
Provider("gemini-flash", "gemini --model gemini-2.5-flash", "28s 4/4 | use this for quick tasks"),
|
||||
Provider("gemini", "gemini --model gemini-2.5-pro", "91s 3/4 | slow CLI, best for large docs/PDFs"),
|
||||
|
||||
# Mock for testing
|
||||
Provider("mock", "mock", "Mock provider for testing"),
|
||||
]
|
||||
|
||||
|
||||
def get_providers_file() -> Path:
|
||||
"""Get the providers config file, creating default if needed."""
|
||||
if not PROVIDERS_FILE.exists():
|
||||
PROVIDERS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_providers(DEFAULT_PROVIDERS)
|
||||
return PROVIDERS_FILE
|
||||
|
||||
|
||||
def load_providers() -> List[Provider]:
|
||||
"""Load all defined providers."""
|
||||
providers_file = get_providers_file()
|
||||
|
||||
try:
|
||||
data = yaml.safe_load(providers_file.read_text())
|
||||
if not data or "providers" not in data:
|
||||
return DEFAULT_PROVIDERS.copy()
|
||||
return [Provider.from_dict(p) for p in data["providers"]]
|
||||
except Exception:
|
||||
return DEFAULT_PROVIDERS.copy()
|
||||
|
||||
|
||||
def save_providers(providers: List[Provider]):
|
||||
"""Save providers to config file."""
|
||||
PROVIDERS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = {"providers": [p.to_dict() for p in providers]}
|
||||
PROVIDERS_FILE.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False))
|
||||
|
||||
|
||||
def get_provider(name: str) -> Optional[Provider]:
|
||||
"""Get a provider by name."""
|
||||
providers = load_providers()
|
||||
for p in providers:
|
||||
if p.name == name:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def add_provider(provider: Provider) -> bool:
|
||||
"""Add or update a provider."""
|
||||
providers = load_providers()
|
||||
|
||||
# Update if exists, otherwise add
|
||||
for i, p in enumerate(providers):
|
||||
if p.name == provider.name:
|
||||
providers[i] = provider
|
||||
save_providers(providers)
|
||||
return True
|
||||
|
||||
providers.append(provider)
|
||||
save_providers(providers)
|
||||
return True
|
||||
|
||||
|
||||
def delete_provider(name: str) -> bool:
|
||||
"""Delete a provider by name."""
|
||||
providers = load_providers()
|
||||
original_len = len(providers)
|
||||
providers = [p for p in providers if p.name != name]
|
||||
|
||||
if len(providers) < original_len:
|
||||
save_providers(providers)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def call_provider(provider_name: str, prompt: str, timeout: int = 300) -> ProviderResult:
|
||||
"""
|
||||
Call an AI CLI tool with the given prompt.
|
||||
Call an AI provider with the given prompt.
|
||||
|
||||
Args:
|
||||
provider: The CLI command (e.g., 'codex', 'claude', 'gemini')
|
||||
args: Additional arguments for the provider (e.g., '-p')
|
||||
provider_name: Name of the provider to use
|
||||
prompt: The prompt to send
|
||||
timeout: Maximum execution time in seconds
|
||||
|
||||
Returns:
|
||||
ProviderResult with the response text or error
|
||||
"""
|
||||
# Check if provider exists
|
||||
if not shutil.which(provider):
|
||||
# Handle mock provider specially
|
||||
if provider_name.lower() == "mock":
|
||||
return mock_provider(prompt)
|
||||
|
||||
# Look up provider
|
||||
provider = get_provider(provider_name)
|
||||
if not provider:
|
||||
return ProviderResult(
|
||||
text="",
|
||||
success=False,
|
||||
error=f"Provider '{provider}' not found. Is it installed and in PATH?"
|
||||
error=f"Provider '{provider_name}' not found. Use 'smarttools providers' to manage providers."
|
||||
)
|
||||
|
||||
# Build command
|
||||
cmd = f"{provider} {args}".strip()
|
||||
# Parse command (expand environment variables)
|
||||
cmd = os.path.expandvars(provider.command)
|
||||
|
||||
# Check if base command exists
|
||||
base_cmd = cmd.split()[0]
|
||||
# Expand ~ for the which check
|
||||
base_cmd_expanded = os.path.expanduser(base_cmd)
|
||||
if not shutil.which(base_cmd_expanded) and not os.path.isfile(base_cmd_expanded):
|
||||
return ProviderResult(
|
||||
text="",
|
||||
success=False,
|
||||
error=f"Command '{base_cmd}' not found. Is it installed and in PATH?"
|
||||
)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -92,19 +231,3 @@ def mock_provider(prompt: str) -> ProviderResult:
|
||||
f"This is a mock response. Use a real provider for actual output.",
|
||||
success=True
|
||||
)
|
||||
|
||||
|
||||
def get_provider_func(provider: str):
|
||||
"""
|
||||
Get the appropriate provider function.
|
||||
|
||||
Args:
|
||||
provider: Provider name ('mock' for mock, anything else for real)
|
||||
|
||||
Returns:
|
||||
Callable that takes (prompt) or (provider, args, prompt)
|
||||
"""
|
||||
if provider.lower() == "mock":
|
||||
return lambda prompt, **kwargs: mock_provider(prompt)
|
||||
else:
|
||||
return lambda prompt, args="", **kwargs: call_provider(provider, args, prompt, **kwargs)
|
||||
|
||||
Reference in New Issue
Block a user