Compare commits

...
2 Commits
Author SHA1 Message Date
Mervin Praison a2d181dd74 Release v2.6.2 2025-12-27 07:58:18 +00:00
Mervin Praison 7411fdbb52 Release v2.6.1 2025-12-26 19:41:45 +00:00
17 changed files with 2916 additions and 321 deletions

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.6.0" \
"praisonai>=2.6.2" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.6.0" \
"praisonai>=2.6.2" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.6.0" \
"praisonai>=2.6.2" \
"praisonai[ui]" \
"praisonai[crewai]"
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.6.0.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.6.0.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.6.2.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.6.2.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+263 -41
View File
@@ -5,6 +5,7 @@ Provides CLI commands for template management:
- list, search, info, install, cache clear
- run templates directly
- init projects from templates
- Custom templates directory support with precedence
"""
import shutil
@@ -111,7 +112,9 @@ class TemplatesHandler:
[bold]Options:[/bold]
--offline Use only cached templates (no network)
--source <src> Filter by source (local, remote, cached)
--source <src> Filter by source (custom, package, all)
--custom-dir <path> Add custom templates directory
--paths Show template search paths
[bold]Examples:[/bold]
praisonai templates list
@@ -130,21 +133,50 @@ class TemplatesHandler:
.replace("[red]", "").replace("[/red]", ""))
def cmd_list(self, args: List[str]) -> int:
"""List available templates."""
offline = "--offline" in args
source = "all"
"""List available templates from all sources including custom directories."""
source_filter = None
custom_dirs = []
show_paths = "--paths" in args
# Parse --source filter
if "--source" in args:
idx = args.index("--source")
if idx + 1 < len(args):
source = args[idx + 1]
source_filter = args[idx + 1]
# Parse --custom-dir (can be specified multiple times)
i = 0
while i < len(args):
if args[i] == "--custom-dir" and i + 1 < len(args):
custom_dirs.append(args[i + 1])
i += 2
else:
i += 1
try:
from praisonai.templates import list_templates
templates = list_templates(source=source, offline=offline)
from praisonai.templates.discovery import TemplateDiscovery
discovery = TemplateDiscovery(
custom_dirs=custom_dirs if custom_dirs else None,
include_package=True,
include_defaults=True
)
# Show search paths if requested
if show_paths:
print("\nTemplate Search Paths (in priority order):")
for path, source, exists in discovery.get_search_paths():
status = "✓" if exists else "✗"
print(f" {status} [{source}] {path}")
print()
# Discover templates
templates = discovery.list_templates(source_filter=source_filter)
if not templates:
print("No templates found.")
if not show_paths:
print("Use --paths to see search locations.")
return 0
try:
@@ -158,26 +190,31 @@ class TemplatesHandler:
table.add_column("Description")
table.add_column("Source", style="dim")
for t in templates:
for t in sorted(templates, key=lambda x: (x.priority, x.name)):
desc = t.description or ""
table.add_row(
t.name,
t.version,
t.description[:50] + "..." if len(t.description) > 50 else t.description,
t.source or "local"
t.version or "1.0.0",
desc[:50] + "..." if len(desc) > 50 else desc,
t.source
)
console.print(table)
except ImportError:
print(f"{'Name':<25} {'Version':<10} {'Description':<40}")
print(f"{'Name':<25} {'Version':<10} {'Source':<10} {'Description':<35}")
print("-" * 80)
for t in templates:
desc = t.description[:40] + "..." if len(t.description) > 40 else t.description
print(f"{t.name:<25} {t.version:<10} {desc:<40}")
for t in sorted(templates, key=lambda x: (x.priority, x.name)):
desc = (t.description or "")[:35]
if len(t.description or "") > 35:
desc += "..."
print(f"{t.name:<25} {t.version or '1.0.0':<10} {t.source:<10} {desc:<35}")
return 0
except Exception as e:
print(f"[red]Error listing templates: {e}[/red]")
print(f"Error listing templates: {e}")
import traceback
traceback.print_exc()
return 1
def cmd_search(self, args: List[str]) -> int:
@@ -213,17 +250,65 @@ class TemplatesHandler:
return 1
def cmd_info(self, args: List[str]) -> int:
"""Show template details."""
"""Show template details with custom directory support."""
if not args or args[0].startswith("--"):
print("Usage: praisonai templates info <template>")
return 1
uri = args[0]
name_or_uri = args[0]
offline = "--offline" in args
custom_dirs = []
# Parse --custom-dir
i = 0
while i < len(args):
if args[i] == "--custom-dir" and i + 1 < len(args):
custom_dirs.append(args[i + 1])
i += 2
else:
i += 1
try:
from praisonai.templates import load_template
template = load_template(uri, offline=offline)
# First try to find in custom/local directories
from praisonai.templates.discovery import TemplateDiscovery
discovery = TemplateDiscovery(
custom_dirs=custom_dirs if custom_dirs else None,
include_package=True,
include_defaults=True
)
discovered = discovery.find_template(name_or_uri)
if discovered:
# Load from discovered path
from praisonai.templates import load_template
template = load_template(str(discovered.path), offline=offline)
else:
# Fall back to URI resolution
from praisonai.templates import load_template
template = load_template(name_or_uri, offline=offline)
# Check dependency availability
from praisonai.templates.dependency_checker import DependencyChecker
checker = DependencyChecker()
deps = checker.check_template_dependencies(template)
# Build availability strings
def format_dep_list(items, key="available"):
result = []
for item in items:
status = "✓" if item.get(key, False) else "✗"
hint = ""
if not item.get(key, False):
if item.get("install_hint"):
hint = f" ({item['install_hint']})"
result.append(f"{status} {item['name']}{hint}")
return result
tools_status = format_dep_list(deps["tools"])
pkgs_status = format_dep_list(deps["packages"])
env_status = format_dep_list(deps["env"])
try:
from rich.console import Console
@@ -243,23 +328,61 @@ class TemplatesHandler:
**Tags:** {', '.join(template.tags) if template.tags else 'None'}
**Requirements:**
- Tools: {', '.join(template.requires.get('tools', [])) or 'None'}
- Packages: {', '.join(template.requires.get('packages', [])) or 'None'}
- Environment: {', '.join(template.requires.get('env', [])) or 'None'}
**Skills:** {', '.join(template.skills) if template.skills else 'None'}
**Path:** {template.path}
"""
console.print(Panel(Markdown(info), title=f"Template: {template.name}"))
# Print dependency status
all_ok = "✓" if deps["all_satisfied"] else "✗"
console.print(f"\n[bold]Dependencies Status:[/bold] {all_ok}")
if tools_status:
console.print("\n[bold]Required Tools:[/bold]")
for t in tools_status:
color = "green" if t.startswith("✓") else "red"
console.print(f" [{color}]{t}[/{color}]")
if pkgs_status:
console.print("\n[bold]Required Packages:[/bold]")
for p in pkgs_status:
color = "green" if p.startswith("✓") else "red"
console.print(f" [{color}]{p}[/{color}]")
if env_status:
console.print("\n[bold]Required Environment:[/bold]")
for e in env_status:
color = "green" if e.startswith("✓") else "red"
console.print(f" [{color}]{e}[/{color}]")
# Print install hints if any missing
if not deps["all_satisfied"]:
hints = checker.get_install_hints(template)
if hints:
console.print("\n[bold yellow]To fix missing dependencies:[/bold yellow]")
for hint in hints:
console.print(f" • {hint}")
except ImportError:
print(f"Template: {template.name}")
print(f"Version: {template.version}")
print(f"Author: {template.author or 'Unknown'}")
print(f"Description: {template.description}")
print(f"Path: {template.path}")
print(f"\nDependencies: {'All satisfied' if deps['all_satisfied'] else 'Some missing'}")
if tools_status:
print("\nRequired Tools:")
for t in tools_status:
print(f" {t}")
if pkgs_status:
print("\nRequired Packages:")
for p in pkgs_status:
print(f" {p}")
if env_status:
print("\nRequired Environment:")
for e in env_status:
print(f" {e}")
return 0
@@ -342,30 +465,79 @@ class TemplatesHandler:
def cmd_run(self, args: List[str]) -> int:
"""Run a template directly."""
if not args or args[0].startswith("--"):
print("Usage: praisonai templates run <template> [args...]")
print("Usage: praisonai templates run <template> [args...] [--strict-tools] [--offline]")
return 1
uri = args[0]
template_args = args[1:]
offline = "--offline" in args
strict_tools = "--strict-tools" in args
# Parse --tools override
tools_files = []
tools_dirs = []
i = 0
while i < len(args):
if args[i] == "--tools" and i + 1 < len(args):
tools_files.append(args[i + 1])
i += 2
elif args[i] == "--tools-dir" and i + 1 < len(args):
tools_dirs.append(args[i + 1])
i += 2
else:
i += 1
try:
from praisonai.templates import load_template
from praisonai.templates.loader import TemplateLoader
from praisonai.templates.discovery import TemplateDiscovery
# First try to find in custom/local directories
discovery = TemplateDiscovery(
custom_dirs=tools_dirs if tools_dirs else None,
include_package=True,
include_defaults=True
)
discovered = discovery.find_template(uri)
loader = TemplateLoader(offline=offline)
template = loader.load(uri, offline=offline)
# Check requirements
missing = loader.check_requirements(template)
if missing["missing_packages"]:
print(f"Missing packages: {', '.join(missing['missing_packages'])}")
print("Install with: pip install " + " ".join(missing["missing_packages"]))
if missing["missing_env"]:
print(f"Missing environment variables: {', '.join(missing['missing_env'])}")
if discovered:
# Load from discovered path
template = loader.load(str(discovered.path), offline=offline)
else:
# Fall back to URI resolution
template = loader.load(uri, offline=offline)
if missing["missing_packages"] or missing["missing_env"]:
return 1
# Strict mode: fail-fast on missing dependencies
if strict_tools:
from praisonai.templates.dependency_checker import DependencyChecker, StrictModeError
checker = DependencyChecker()
try:
checker.enforce_strict_mode(template)
print("✓ All dependencies satisfied (strict mode)")
except StrictModeError as e:
print(f"✗ Strict mode check failed:\n{e}")
return 1
else:
# Non-strict: warn but continue
missing = loader.check_requirements(template)
if missing["missing_packages"]:
print(f"Warning: Missing packages: {', '.join(missing['missing_packages'])}")
print("Install with: pip install " + " ".join(missing["missing_packages"]))
if missing["missing_env"]:
print(f"Warning: Missing environment variables: {', '.join(missing['missing_env'])}")
# Load tool overrides if specified
tool_registry = None
if tools_files or tools_dirs:
from praisonai.templates.tool_override import create_tool_registry_with_overrides
tool_registry = create_tool_registry_with_overrides(
override_files=tools_files,
override_dirs=tools_dirs,
include_defaults=True
)
print(f"✓ Loaded {len(tool_registry)} tools from overrides")
# Load and run workflow
workflow_config = loader.load_workflow_config(template)
@@ -377,10 +549,60 @@ class TemplatesHandler:
if config:
workflow_config = {**workflow_config, **config}
# Run workflow
from praisonaiagents import Workflow
workflow = Workflow(**workflow_config)
workflow.run()
# Run workflow - determine which class to use based on config structure
if "agents" in workflow_config and "tasks" in workflow_config:
# PraisonAIAgents format (agents + tasks)
from praisonaiagents import Agent, Task, PraisonAIAgents
# Build agents
agents_config = workflow_config.get("agents", [])
agents = []
agent_map = {}
for agent_cfg in agents_config:
agent = Agent(
name=agent_cfg.get("name", "Agent"),
role=agent_cfg.get("role", ""),
goal=agent_cfg.get("goal", ""),
backstory=agent_cfg.get("backstory", ""),
tools=agent_cfg.get("tools", []),
llm=agent_cfg.get("llm"),
verbose=agent_cfg.get("verbose", True)
)
agents.append(agent)
agent_map[agent_cfg.get("name", "Agent")] = agent
# Build tasks
tasks_config = workflow_config.get("tasks", [])
tasks = []
for task_cfg in tasks_config:
agent_name = task_cfg.get("agent", "")
agent = agent_map.get(agent_name, agents[0] if agents else None)
task = Task(
name=task_cfg.get("name", "Task"),
description=task_cfg.get("description", ""),
expected_output=task_cfg.get("expected_output", ""),
agent=agent
)
tasks.append(task)
# Run
praison_agents = PraisonAIAgents(
agents=agents,
tasks=tasks,
process=workflow_config.get("process", "sequential"),
verbose=workflow_config.get("verbose", 1)
)
praison_agents.start()
elif "steps" in workflow_config:
# Workflow format (steps)
from praisonaiagents import Workflow
workflow = Workflow(**workflow_config)
workflow.run()
else:
raise ValueError("Invalid workflow config: must have 'agents'+'tasks' or 'steps'")
print(f"\n✓ Template '{template.name}' completed successfully")
return 0
+31 -1
View File
@@ -31,7 +31,7 @@ class ToolsHandler(CommandHandler):
return "tools"
def get_actions(self) -> List[str]:
return ["list", "info", "search", "help"]
return ["list", "info", "search", "doctor", "help"]
def get_help_text(self) -> str:
return """
@@ -39,6 +39,8 @@ Tools Commands:
praisonai tools list - List all available tools
praisonai tools info <name> - Show tool details
praisonai tools search <query> - Search tools by name/description
praisonai tools doctor - Diagnose tool availability and dependencies
praisonai tools doctor --json - Output diagnosis as JSON
Built-in tools include: internet_search, calculator, file operations, etc.
"""
@@ -213,6 +215,34 @@ Built-in tools include: internet_search, calculator, file operations, etc.
return matches
def action_doctor(self, args: List[str], **kwargs) -> Dict[str, Any]:
"""
Diagnose tool availability and dependencies.
Args:
args: List containing optional flags (--json)
Returns:
Dict with diagnostic results
"""
json_output = "--json" in args
try:
from praisonai.templates.tools_doctor import ToolsDoctor
doctor = ToolsDoctor()
if json_output:
print(doctor.diagnose_json())
else:
print(doctor.diagnose_human())
return doctor.diagnose()
except Exception as e:
self.print_status(f"Error running diagnostics: {e}", "error")
return {}
def execute(self, action: str, action_args: List[str], **kwargs) -> Any:
"""Execute tools command action."""
return super().execute(action, action_args, **kwargs)
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.6.0 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.6.2 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
@@ -11,11 +11,20 @@ __all__ = [
"TemplateCache",
"TemplateRegistry",
"TemplateSecurity",
"TemplateDiscovery",
"DiscoveredTemplate",
"DependencyChecker",
"StrictModeError",
"ToolsDoctor",
"ToolOverrideLoader",
"SecurityError",
"load_template",
"list_templates",
"search_templates",
"install_template",
"clear_cache",
"discover_templates",
"find_template_path",
]
# Lazy loading implementation
@@ -67,5 +76,41 @@ def __getattr__(name):
from .cache import clear_cache
_module_cache[name] = clear_cache
return clear_cache
elif name == "TemplateDiscovery":
from .discovery import TemplateDiscovery
_module_cache[name] = TemplateDiscovery
return TemplateDiscovery
elif name == "DiscoveredTemplate":
from .discovery import DiscoveredTemplate
_module_cache[name] = DiscoveredTemplate
return DiscoveredTemplate
elif name == "discover_templates":
from .discovery import discover_templates
_module_cache[name] = discover_templates
return discover_templates
elif name == "find_template_path":
from .discovery import find_template_path
_module_cache[name] = find_template_path
return find_template_path
elif name == "DependencyChecker":
from .dependency_checker import DependencyChecker
_module_cache[name] = DependencyChecker
return DependencyChecker
elif name == "StrictModeError":
from .dependency_checker import StrictModeError
_module_cache[name] = StrictModeError
return StrictModeError
elif name == "ToolsDoctor":
from .tools_doctor import ToolsDoctor
_module_cache[name] = ToolsDoctor
return ToolsDoctor
elif name == "ToolOverrideLoader":
from .tool_override import ToolOverrideLoader
_module_cache[name] = ToolOverrideLoader
return ToolOverrideLoader
elif name == "SecurityError":
from .tool_override import SecurityError
_module_cache[name] = SecurityError
return SecurityError
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,358 @@
"""
Dependency Checker for Templates.
Checks availability of tools, packages, and environment variables
required by templates. Supports strict mode for fail-fast behavior.
"""
import importlib.util
import os
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, List, Optional, TYPE_CHECKING
if TYPE_CHECKING:
from .loader import TemplateConfig
class StrictModeError(Exception):
"""Raised when strict mode detects missing dependencies."""
def __init__(self, message: str, missing_items: Dict[str, List[str]]):
super().__init__(message)
self.missing_items = missing_items
@dataclass
class DependencyStatus:
"""Status of a single dependency."""
name: str
available: bool
source: Optional[str] = None
install_hint: Optional[str] = None
masked_value: Optional[str] = None
class DependencyChecker:
"""
Checks availability of template dependencies.
Supports checking:
- Tools (from built-in registry, praisonai-tools, custom dirs)
- Packages (via importlib.util.find_spec)
- Environment variables (via os.environ)
"""
# Known tool sources for install hints
TOOL_INSTALL_HINTS = {
"youtube_tool": "pip install praisonai-tools[video]",
"whisper_tool": "pip install praisonai-tools[audio]",
"vision_tool": "pip install praisonai-tools[vision]",
"tavily_search": "pip install tavily-python",
}
def __init__(self, custom_tool_dirs: Optional[List[str]] = None):
"""
Initialize dependency checker.
Args:
custom_tool_dirs: Additional directories to search for tools
"""
self._custom_tool_dirs = custom_tool_dirs or []
self._tool_cache: Dict[str, DependencyStatus] = {}
def check_tool(self, tool_name: str) -> Dict[str, Any]:
"""
Check if a tool is available.
Args:
tool_name: Name of the tool to check
Returns:
Dict with 'available', 'source', and 'install_hint' keys
"""
# Check cache first
if tool_name in self._tool_cache:
status = self._tool_cache[tool_name]
return {
"available": status.available,
"source": status.source,
"install_hint": status.install_hint
}
# Check built-in tools
source = self._check_builtin_tool(tool_name)
if source:
status = DependencyStatus(
name=tool_name,
available=True,
source=source
)
self._tool_cache[tool_name] = status
return {"available": True, "source": source, "install_hint": None}
# Check praisonai-tools
source = self._check_praisonai_tools(tool_name)
if source:
status = DependencyStatus(
name=tool_name,
available=True,
source=source
)
self._tool_cache[tool_name] = status
return {"available": True, "source": source, "install_hint": None}
# Check custom directories
source = self._check_custom_dirs(tool_name)
if source:
status = DependencyStatus(
name=tool_name,
available=True,
source=source
)
self._tool_cache[tool_name] = status
return {"available": True, "source": source, "install_hint": None}
# Tool not found
install_hint = self.TOOL_INSTALL_HINTS.get(tool_name)
if not install_hint:
install_hint = "Check praisonai-tools or define custom tool"
status = DependencyStatus(
name=tool_name,
available=False,
source=None,
install_hint=install_hint
)
self._tool_cache[tool_name] = status
return {"available": False, "source": None, "install_hint": install_hint}
def _check_builtin_tool(self, tool_name: str) -> Optional[str]:
"""Check if tool exists in built-in registry."""
try:
from praisonaiagents.tools import TOOL_MAPPINGS
if tool_name in TOOL_MAPPINGS:
return "builtin"
except ImportError:
pass
return None
def _check_praisonai_tools(self, tool_name: str) -> Optional[str]:
"""Check if tool exists in praisonai-tools package."""
try:
import praisonai_tools
if hasattr(praisonai_tools, tool_name):
return "praisonai-tools"
# Check for get_tool function
if hasattr(praisonai_tools, 'get_tool'):
try:
tool = praisonai_tools.get_tool(tool_name)
if tool:
return "praisonai-tools"
except Exception:
pass
except ImportError:
pass
return None
def _check_custom_dirs(self, tool_name: str) -> Optional[str]:
"""Check if tool exists in custom directories."""
for dir_path in self._custom_tool_dirs:
path = Path(dir_path).expanduser()
if not path.exists():
continue
# Check for tool file
tool_file = path / f"{tool_name}.py"
if tool_file.exists():
return f"custom:{dir_path}"
# Check for tool in any Python file (by function name)
for py_file in path.glob("*.py"):
try:
content = py_file.read_text()
if f"def {tool_name}" in content:
return f"custom:{dir_path}"
except Exception:
pass
return None
def check_package(self, package_name: str) -> Dict[str, Any]:
"""
Check if a Python package is available.
Args:
package_name: Name of the package to check
Returns:
Dict with 'available' and 'install_hint' keys
"""
spec = importlib.util.find_spec(package_name)
if spec is not None:
return {"available": True, "install_hint": None}
return {
"available": False,
"install_hint": f"pip install {package_name}"
}
def check_env_var(self, var_name: str) -> Dict[str, Any]:
"""
Check if an environment variable is set.
Args:
var_name: Name of the environment variable
Returns:
Dict with 'available' and 'masked_value' keys
"""
value = os.environ.get(var_name)
if value:
# Mask the value for security
if len(value) > 8:
masked = value[:4] + "*" * (len(value) - 8) + value[-4:]
else:
masked = "*" * len(value)
return {"available": True, "masked_value": masked}
return {"available": False, "masked_value": None}
def check_template_dependencies(
self,
template: "TemplateConfig"
) -> Dict[str, Any]:
"""
Check all dependencies for a template.
Args:
template: Template configuration to check
Returns:
Dict with 'tools', 'packages', 'env', and 'all_satisfied' keys
"""
result = {
"tools": [],
"packages": [],
"env": [],
"all_satisfied": True
}
# Check tools
tools = template.requires.get("tools", [])
if isinstance(tools, str):
tools = [tools]
for tool_name in tools:
status = self.check_tool(tool_name)
result["tools"].append({
"name": tool_name,
**status
})
if not status["available"]:
result["all_satisfied"] = False
# Check packages
packages = template.requires.get("packages", [])
if isinstance(packages, str):
packages = [packages]
for pkg_name in packages:
status = self.check_package(pkg_name)
result["packages"].append({
"name": pkg_name,
**status
})
if not status["available"]:
result["all_satisfied"] = False
# Check environment variables
env_vars = template.requires.get("env", [])
if isinstance(env_vars, str):
env_vars = [env_vars]
for var_name in env_vars:
status = self.check_env_var(var_name)
result["env"].append({
"name": var_name,
**status
})
if not status["available"]:
result["all_satisfied"] = False
return result
def get_install_hints(self, template: "TemplateConfig") -> List[str]:
"""
Get install hints for missing dependencies.
Args:
template: Template configuration to check
Returns:
List of actionable install hints
"""
hints = []
deps = self.check_template_dependencies(template)
# Tool hints
for tool in deps["tools"]:
if not tool["available"] and tool.get("install_hint"):
hints.append(f"Tool '{tool['name']}': {tool['install_hint']}")
# Package hints
for pkg in deps["packages"]:
if not pkg["available"] and pkg.get("install_hint"):
hints.append(f"Package '{pkg['name']}': {pkg['install_hint']}")
# Env hints
for env in deps["env"]:
if not env["available"]:
hints.append(f"Environment variable '{env['name']}': export {env['name']}=<value>")
return hints
def enforce_strict_mode(self, template: "TemplateConfig") -> bool:
"""
Enforce strict mode - fail if any dependency is missing.
Args:
template: Template configuration to check
Returns:
True if all dependencies satisfied
Raises:
StrictModeError: If any dependency is missing
"""
deps = self.check_template_dependencies(template)
if deps["all_satisfied"]:
return True
# Collect missing items
missing = {
"tools": [t["name"] for t in deps["tools"] if not t["available"]],
"packages": [p["name"] for p in deps["packages"] if not p["available"]],
"env": [e["name"] for e in deps["env"] if not e["available"]]
}
# Build error message
messages = []
if missing["tools"]:
messages.append(f"Missing tools: {', '.join(missing['tools'])}")
if missing["packages"]:
messages.append(f"Missing packages: {', '.join(missing['packages'])}")
if missing["env"]:
messages.append(f"Missing environment variables: {', '.join(missing['env'])}")
# Add hints
hints = self.get_install_hints(template)
if hints:
messages.append("\nTo fix:")
messages.extend(f" - {hint}" for hint in hints)
raise StrictModeError(
"\n".join(messages),
missing_items=missing
)
@@ -0,0 +1,297 @@
"""
Template Discovery
Discovers templates from multiple directories with precedence support.
Supports custom user directories, built-in templates, and package templates.
"""
import os
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
@dataclass
class DiscoveredTemplate:
"""Information about a discovered template."""
name: str
path: Path
source: str # 'custom', 'builtin', 'package'
priority: int # Lower = higher priority
# Optional metadata (loaded lazily)
description: Optional[str] = None
version: Optional[str] = None
author: Optional[str] = None
class TemplateDiscovery:
"""
Discovers templates from multiple directories with precedence.
Search order (highest to lowest priority):
1. User custom directory (~/.praison/templates)
2. XDG config directory (~/.config/praison/templates)
3. Project local directory (./.praison/templates)
4. Built-in package templates (agent_recipes)
Templates in higher priority directories override those in lower priority.
"""
# Default search paths (in priority order)
DEFAULT_SEARCH_PATHS = [
("~/.praison/templates", "custom", 1),
("~/.config/praison/templates", "custom", 2),
("./.praison/templates", "project", 3),
]
TEMPLATE_FILE = "TEMPLATE.yaml"
def __init__(
self,
custom_dirs: Optional[List[str]] = None,
include_package: bool = True,
include_defaults: bool = True
):
"""
Initialize template discovery.
Args:
custom_dirs: Additional custom directories to search (highest priority)
include_package: Whether to include package templates (agent_recipes)
include_defaults: Whether to include default search paths
"""
self.search_paths: List[Tuple[Path, str, int]] = []
# Add custom directories first (highest priority)
if custom_dirs:
for i, dir_path in enumerate(custom_dirs):
expanded = Path(os.path.expanduser(dir_path))
self.search_paths.append((expanded, "custom", i))
# Add default search paths
if include_defaults:
base_priority = len(custom_dirs) if custom_dirs else 0
for path, source, priority in self.DEFAULT_SEARCH_PATHS:
expanded = Path(os.path.expanduser(path))
self.search_paths.append((expanded, source, base_priority + priority))
self.include_package = include_package
self._package_path: Optional[Path] = None
@property
def package_templates_path(self) -> Optional[Path]:
"""Get path to package templates (lazy loaded)."""
if self._package_path is None and self.include_package:
try:
import importlib.resources
try:
# Python 3.9+
ref = importlib.resources.files("agent_recipes") / "templates"
self._package_path = Path(str(ref))
except (TypeError, AttributeError):
# Fallback for older Python
import agent_recipes
self._package_path = Path(agent_recipes.__file__).parent / "templates"
except ImportError:
pass
return self._package_path
def discover_all(self) -> Dict[str, DiscoveredTemplate]:
"""
Discover all templates from all search paths.
Returns:
Dict mapping template name to DiscoveredTemplate.
When duplicates exist, higher priority wins.
"""
templates: Dict[str, DiscoveredTemplate] = {}
# Scan search paths in reverse priority order (lowest first)
# so that higher priority overwrites lower
all_paths = list(self.search_paths)
# Add package templates with lowest priority
if self.include_package and self.package_templates_path:
max_priority = max((p[2] for p in all_paths), default=0) + 1
all_paths.append((self.package_templates_path, "package", max_priority))
# Sort by priority descending (lowest priority first)
all_paths.sort(key=lambda x: x[2], reverse=True)
for dir_path, source, priority in all_paths:
if dir_path.exists() and dir_path.is_dir():
discovered = self._scan_directory(dir_path, source, priority)
# Higher priority overwrites lower
templates.update(discovered)
return templates
def _scan_directory(
self,
directory: Path,
source: str,
priority: int
) -> Dict[str, DiscoveredTemplate]:
"""
Scan a directory for templates (shallow scan).
Only looks at direct subdirectories containing TEMPLATE.yaml.
Does NOT recursively scan nested directories.
Args:
directory: Directory to scan
source: Source identifier ('custom', 'builtin', 'package')
priority: Priority level
Returns:
Dict mapping template name to DiscoveredTemplate
"""
templates = {}
try:
for item in directory.iterdir():
if item.is_dir():
template_file = item / self.TEMPLATE_FILE
workflow_file = item / "workflow.yaml"
# A valid template has TEMPLATE.yaml or workflow.yaml
if template_file.exists() or workflow_file.exists():
template = DiscoveredTemplate(
name=item.name,
path=item,
source=source,
priority=priority
)
# Load basic metadata if TEMPLATE.yaml exists
if template_file.exists():
self._load_template_metadata(template, template_file)
templates[item.name] = template
except PermissionError:
pass # Skip directories we can't read
return templates
def _load_template_metadata(
self,
template: DiscoveredTemplate,
template_file: Path
) -> None:
"""Load basic metadata from TEMPLATE.yaml."""
try:
import yaml
with open(template_file) as f:
data = yaml.safe_load(f) or {}
template.description = data.get("description", "")
template.version = data.get("version", "1.0.0")
template.author = data.get("author")
except Exception:
pass # Metadata loading is optional
def find_template(self, name: str) -> Optional[DiscoveredTemplate]:
"""
Find a template by name.
Searches all directories in priority order and returns the
highest priority match.
Args:
name: Template name to find
Returns:
DiscoveredTemplate if found, None otherwise
"""
all_templates = self.discover_all()
return all_templates.get(name)
def resolve_template_path(self, name: str) -> Optional[Path]:
"""
Resolve a template name to its path.
Args:
name: Template name
Returns:
Path to template directory, or None if not found
"""
template = self.find_template(name)
return template.path if template else None
def list_templates(
self,
source_filter: Optional[str] = None
) -> List[DiscoveredTemplate]:
"""
List all discovered templates.
Args:
source_filter: Optional filter by source ('custom', 'package', etc.)
Returns:
List of DiscoveredTemplate objects
"""
templates = self.discover_all()
if source_filter:
return [t for t in templates.values() if t.source == source_filter]
return list(templates.values())
def get_search_paths(self) -> List[Tuple[str, str, bool]]:
"""
Get all search paths with their status.
Returns:
List of (path, source, exists) tuples
"""
paths = []
for dir_path, source, _ in self.search_paths:
paths.append((str(dir_path), source, dir_path.exists()))
if self.include_package and self.package_templates_path:
paths.append((
str(self.package_templates_path),
"package",
self.package_templates_path.exists()
))
return paths
# Convenience functions
def discover_templates(
custom_dirs: Optional[List[str]] = None
) -> Dict[str, DiscoveredTemplate]:
"""
Discover all templates from default and custom directories.
Args:
custom_dirs: Additional custom directories to search
Returns:
Dict mapping template name to DiscoveredTemplate
"""
discovery = TemplateDiscovery(custom_dirs=custom_dirs)
return discovery.discover_all()
def find_template_path(
name: str,
custom_dirs: Optional[List[str]] = None
) -> Optional[Path]:
"""
Find a template by name and return its path.
Args:
name: Template name
custom_dirs: Additional custom directories to search
Returns:
Path to template directory, or None if not found
"""
discovery = TemplateDiscovery(custom_dirs=custom_dirs)
return discovery.resolve_template_path(name)
+114 -16
View File
@@ -6,11 +6,13 @@ Handles TEMPLATE.yaml parsing, config merging, and skills integration.
"""
import os
import re
import secrets
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Dict, List, Optional, Tuple
from dataclasses import dataclass, field
from .resolver import ResolvedTemplate, TemplateSource
from .resolver import ResolvedTemplate # noqa: F401 - used by registry
from .cache import TemplateCache
from .registry import TemplateRegistry
from .security import TemplateSecurity
@@ -154,6 +156,12 @@ class TemplateLoader:
if isinstance(requires, list):
requires = {"tools": requires}
# Handle workflow - can be inline dict or file reference string
workflow = raw.get("workflow", "workflow.yaml")
# Handle agents - can be inline list or file reference string
agents = raw.get("agents", "agents.yaml")
return TemplateConfig(
name=raw.get("name", template_dir.name),
description=raw.get("description", ""),
@@ -162,8 +170,8 @@ class TemplateLoader:
license=raw.get("license"),
tags=raw.get("tags", []),
requires=requires,
workflow_file=raw.get("workflow", "workflow.yaml"),
agents_file=raw.get("agents", "agents.yaml"),
workflow_file=workflow,
agents_file=agents,
config_schema=raw.get("config", {}),
defaults=raw.get("defaults", {}),
skills=raw.get("skills", []),
@@ -201,16 +209,32 @@ class TemplateLoader:
Returns:
Workflow configuration dict
Supports:
- Inline workflow definition in TEMPLATE.yaml (workflow: {agents: [...], tasks: [...]})
- Separate workflow file reference (workflow: "workflow.yaml")
"""
import yaml
workflow_file = template.path / template.workflow_file
if not workflow_file.exists():
raise ValueError(f"Workflow file not found: {workflow_file}")
with open(workflow_file) as f:
config = yaml.safe_load(f) or {}
# Check if workflow is inline (dict) or a file reference (string)
if isinstance(template.workflow_file, dict):
# Inline workflow definition
config = template.workflow_file
elif isinstance(template.workflow_file, str):
# File reference
workflow_file = template.path / template.workflow_file
if not workflow_file.exists():
# Check if workflow is in raw config
if "workflow" in template.raw and isinstance(template.raw["workflow"], dict):
config = template.raw["workflow"]
else:
raise ValueError(f"Workflow file not found: {workflow_file}")
else:
with open(workflow_file) as f:
config = yaml.safe_load(f) or {}
else:
raise ValueError(f"Invalid workflow configuration type: {type(template.workflow_file)}")
# Substitute variables from template config
config = self._substitute_variables(config, template.defaults)
@@ -251,17 +275,41 @@ class TemplateLoader:
config: Any,
variables: Dict[str, Any]
) -> Any:
"""Recursively substitute variables in config."""
"""
Recursively substitute variables in config with sentinel protection.
This method implements a secure multi-phase substitution strategy:
1. Generate a unique sentinel token for this render
2. Protect user-provided variable values that contain {{...}} syntax
3. Perform template variable substitution on the config
4. Restore protected values
This prevents user input containing {{variable}} syntax from being
interpreted as template variables (injection protection).
"""
if isinstance(config, str):
# Replace {{variable}} patterns
import re
# Generate per-render unique sentinel to prevent collision attacks
sentinel_id = secrets.token_hex(8)
sentinel_prefix = f"__PRAISONAI_SENTINEL_{sentinel_id}_"
# Phase 1: Protect variable values that contain template syntax
protected_values, safe_variables = self._protect_variable_values(
variables, sentinel_prefix
)
# Phase 2: Perform substitution with safe values
pattern = r'\{\{(\w+)\}\}'
def replace(match):
var_name = match.group(1)
return str(variables.get(var_name, match.group(0)))
return str(safe_variables.get(var_name, match.group(0)))
return re.sub(pattern, replace, config)
result = re.sub(pattern, replace, config)
# Phase 3: Restore protected values
result = self._restore_protected_values(result, protected_values)
return result
elif isinstance(config, dict):
return {k: self._substitute_variables(v, variables) for k, v in config.items()}
@@ -271,6 +319,56 @@ class TemplateLoader:
return config
def _protect_variable_values(
self,
variables: Dict[str, Any],
sentinel_prefix: str
) -> Tuple[Dict[str, str], Dict[str, Any]]:
"""
Protect variable values that contain template syntax.
Args:
variables: Original variable dict
sentinel_prefix: Unique prefix for this render
Returns:
Tuple of (protected_values mapping, safe_variables dict)
"""
protected_values = {} # sentinel -> original value
safe_variables = {}
template_pattern = re.compile(r'\{\{.*?\}\}')
for key, value in variables.items():
if isinstance(value, str) and template_pattern.search(value):
# This value contains template syntax - protect it
sentinel = f"{sentinel_prefix}{key}__"
protected_values[sentinel] = value
safe_variables[key] = sentinel
else:
safe_variables[key] = value
return protected_values, safe_variables
def _restore_protected_values(
self,
content: str,
protected_values: Dict[str, str]
) -> str:
"""
Restore protected values after substitution.
Args:
content: Content with sentinel tokens
protected_values: Mapping of sentinel -> original value
Returns:
Content with sentinels replaced by original values
"""
for sentinel, original in protected_values.items():
content = content.replace(sentinel, original)
return content
def get_required_tools(self, template: TemplateConfig) -> List[str]:
"""Get list of required tools for a template."""
tools = template.requires.get("tools", [])
@@ -0,0 +1,331 @@
"""
Tool Override Loader for PraisonAI.
Allows loading custom tools from files, modules, and directories.
Supports runtime tool registration with context manager pattern.
"""
import ast
import importlib.util
import sys
from contextlib import contextmanager
from pathlib import Path
from typing import Any, Callable, Dict, Generator, List, Optional
class SecurityError(Exception):
"""Raised when a security violation is detected."""
pass
class ToolOverrideLoader:
"""
Loads custom tools from various sources.
Supports:
- Python file paths
- Module import paths
- Directories containing tool files
Security:
- Only local paths allowed by default
- Remote URLs rejected
- No auto-execution of arbitrary code
"""
# Default custom tool directories
DEFAULT_TOOL_DIRS = [
"~/.praison/tools",
"~/.config/praison/tools",
]
def __init__(self):
"""Initialize tool override loader."""
self._loaded_tools: Dict[str, Callable] = {}
def get_default_tool_dirs(self) -> List[Path]:
"""
Get default custom tool directories.
Returns:
List of Path objects for default tool directories
"""
return [Path(d).expanduser() for d in self.DEFAULT_TOOL_DIRS]
def load_from_file(self, file_path: str) -> Dict[str, Callable]:
"""
Load tools from a Python file.
Args:
file_path: Path to Python file containing tool functions
Returns:
Dict mapping tool names to callable functions
Raises:
SecurityError: If path is a remote URL
FileNotFoundError: If file doesn't exist
"""
# Security check - reject remote URLs
if file_path.startswith(("http://", "https://", "ftp://")):
raise SecurityError(
f"Remote URLs not allowed for security: {file_path}. "
"Only local file paths are permitted."
)
path = Path(file_path).expanduser().resolve()
if not path.exists():
raise FileNotFoundError(f"Tool file not found: {path}")
if not path.suffix == ".py":
raise ValueError(f"Tool file must be a Python file (.py): {path}")
# Load the module
spec = importlib.util.spec_from_file_location(
f"custom_tools_{path.stem}",
path
)
if spec is None or spec.loader is None:
raise ImportError(f"Could not load module from: {path}")
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
# Extract callable functions (tools)
tools = {}
for name in dir(module):
if name.startswith("_"):
continue
obj = getattr(module, name)
if callable(obj) and not isinstance(obj, type):
tools[name] = obj
self._loaded_tools.update(tools)
return tools
def load_from_module(self, module_path: str) -> Dict[str, Callable]:
"""
Load tools from a module import path.
Args:
module_path: Python module path (e.g., 'mypackage.tools')
Returns:
Dict mapping tool names to callable functions
"""
try:
module = importlib.import_module(module_path)
except ImportError as e:
raise ImportError(f"Could not import module: {module_path}") from e
# Extract callable functions (tools)
tools = {}
for name in dir(module):
if name.startswith("_"):
continue
obj = getattr(module, name)
if callable(obj) and not isinstance(obj, type):
tools[name] = obj
self._loaded_tools.update(tools)
return tools
def load_from_directory(self, dir_path: str) -> Dict[str, Callable]:
"""
Load tools from all Python files in a directory.
Args:
dir_path: Path to directory containing tool files
Returns:
Dict mapping tool names to callable functions
Raises:
SecurityError: If path is a remote URL
"""
# Security check
if dir_path.startswith(("http://", "https://", "ftp://")):
raise SecurityError(
f"Remote URLs not allowed for security: {dir_path}. "
"Only local directory paths are permitted."
)
path = Path(dir_path).expanduser().resolve()
if not path.exists():
raise FileNotFoundError(f"Tool directory not found: {path}")
if not path.is_dir():
raise ValueError(f"Path is not a directory: {path}")
tools = {}
for py_file in path.glob("*.py"):
if py_file.name.startswith("_"):
continue
try:
file_tools = self.load_from_file(str(py_file))
tools.update(file_tools)
except Exception:
# Skip files that fail to load
pass
return tools
def discover_tools_in_directory(self, dir_path: str) -> List[str]:
"""
Discover tool names in a directory without executing code.
Uses AST parsing to find function definitions.
Args:
dir_path: Path to directory to scan
Returns:
List of discovered tool function names
"""
path = Path(dir_path).expanduser().resolve()
if not path.exists() or not path.is_dir():
return []
tool_names = []
for py_file in path.glob("*.py"):
if py_file.name.startswith("_"):
continue
try:
content = py_file.read_text()
tree = ast.parse(content)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
if not node.name.startswith("_"):
tool_names.append(node.name)
except Exception:
# Skip files that fail to parse
pass
return tool_names
@contextmanager
def override_context(
self,
files: Optional[List[str]] = None,
modules: Optional[List[str]] = None,
directories: Optional[List[str]] = None
) -> Generator[Dict[str, Callable], None, None]:
"""
Context manager for temporary tool overrides.
Tools loaded within this context are available only during
the context and are cleaned up afterward.
Args:
files: List of Python file paths to load
modules: List of module paths to import
directories: List of directories to scan
Yields:
Dict of loaded tools
"""
# Store original state
original_tools = self._loaded_tools.copy()
try:
# Load tools from all sources
loaded = {}
if files:
for f in files:
loaded.update(self.load_from_file(f))
if modules:
for m in modules:
loaded.update(self.load_from_module(m))
if directories:
for d in directories:
loaded.update(self.load_from_directory(d))
yield loaded
finally:
# Restore original state
self._loaded_tools = original_tools
def get_loaded_tools(self) -> Dict[str, Callable]:
"""
Get all currently loaded tools.
Returns:
Dict mapping tool names to callable functions
"""
return self._loaded_tools.copy()
def clear_loaded_tools(self) -> None:
"""Clear all loaded tools."""
self._loaded_tools.clear()
def create_tool_registry_with_overrides(
override_files: Optional[List[str]] = None,
override_dirs: Optional[List[str]] = None,
include_defaults: bool = True
) -> Dict[str, Callable]:
"""
Create a tool registry with custom overrides.
Resolution order (highest priority first):
1. Override files (explicit)
2. Override directories
3. Default custom dirs (~/.praison/tools, etc.)
4. Built-in tools
Args:
override_files: Explicit tool files to load
override_dirs: Directories to scan for tools
include_defaults: Whether to include default tool directories
Returns:
Dict mapping tool names to callable functions
"""
registry = {}
loader = ToolOverrideLoader()
# Start with built-in tools (lowest priority)
try:
from praisonaiagents.tools import TOOL_MAPPINGS
registry.update(TOOL_MAPPINGS)
except ImportError:
pass
# Add default custom dirs
if include_defaults:
for dir_path in loader.get_default_tool_dirs():
if dir_path.exists():
try:
tools = loader.load_from_directory(str(dir_path))
registry.update(tools)
except Exception:
pass
# Add override directories
if override_dirs:
for dir_path in override_dirs:
try:
tools = loader.load_from_directory(dir_path)
registry.update(tools)
except Exception:
pass
# Add override files (highest priority)
if override_files:
for file_path in override_files:
try:
tools = loader.load_from_file(file_path)
registry.update(tools)
except Exception:
pass
return registry
@@ -0,0 +1,256 @@
"""
Tools Doctor for PraisonAI.
Diagnoses tool availability, checks dependencies, and reports issues.
Supports both human-readable and JSON output formats.
"""
import importlib.util
import json
import os
from pathlib import Path
from typing import Any, Dict, List, Optional
class ToolsDoctor:
"""
Diagnoses tool availability and dependencies.
Checks:
- praisonai-tools installation
- Built-in tools availability
- Custom tool directories
- Optional dependency packages
"""
# Default custom tool directories
DEFAULT_TOOL_DIRS = [
"~/.praison/tools",
"~/.config/praison/tools",
]
# Known tools and their optional dependencies
TOOL_DEPENDENCIES = {
"youtube_tool": ["pytube", "youtube-transcript-api"],
"whisper_tool": ["openai-whisper", "torch"],
"vision_tool": ["pillow", "opencv-python"],
"tavily_search": ["tavily-python"],
"arxiv_search": ["arxiv"],
"wikipedia_search": ["wikipedia"],
}
def __init__(self, custom_dirs: Optional[List[str]] = None):
"""
Initialize tools doctor.
Args:
custom_dirs: Additional custom tool directories to check
"""
self._custom_dirs = custom_dirs or []
def diagnose(self) -> Dict[str, Any]:
"""
Run full diagnostics.
Returns:
Dict with diagnostic results
"""
result = {
"praisonai_tools_installed": self._check_praisonai_tools(),
"praisonaiagents_installed": self._check_praisonaiagents(),
"builtin_tools": self._get_builtin_tools(),
"praisonai_tools_available": self._get_praisonai_tools_list(),
"custom_tools_dirs": self._check_custom_dirs(),
"tool_dependencies": self._check_tool_dependencies(),
"issues": [],
}
# Collect issues
if not result["praisonaiagents_installed"]:
result["issues"].append({
"severity": "error",
"message": "praisonaiagents not installed",
"hint": "pip install praisonaiagents"
})
if not result["praisonai_tools_installed"]:
result["issues"].append({
"severity": "warning",
"message": "praisonai-tools not installed (optional)",
"hint": "pip install praisonai-tools"
})
# Check for missing dependencies
for tool, deps in result["tool_dependencies"].items():
missing = [d for d in deps if not d["available"]]
if missing:
result["issues"].append({
"severity": "info",
"message": f"Tool '{tool}' has missing optional dependencies",
"hint": f"pip install {' '.join(d['name'] for d in missing)}"
})
return result
def _check_praisonai_tools(self) -> bool:
"""Check if praisonai-tools is installed."""
spec = importlib.util.find_spec("praisonai_tools")
return spec is not None
def _check_praisonaiagents(self) -> bool:
"""Check if praisonaiagents is installed."""
spec = importlib.util.find_spec("praisonaiagents")
return spec is not None
def _get_builtin_tools(self) -> List[str]:
"""Get list of built-in tools."""
tools = []
try:
from praisonaiagents.tools import TOOL_MAPPINGS
tools = list(TOOL_MAPPINGS.keys())
except ImportError:
pass
return tools
def _get_praisonai_tools_list(self) -> List[str]:
"""Get list of tools from praisonai-tools package."""
tools = []
try:
import praisonai_tools
# Get all callable attributes that look like tools
for name in dir(praisonai_tools):
if not name.startswith("_"):
obj = getattr(praisonai_tools, name, None)
if callable(obj):
tools.append(name)
except ImportError:
pass
return tools
def _check_custom_dirs(self) -> List[Dict[str, Any]]:
"""Check custom tool directories."""
dirs = []
# Check default dirs
for dir_path in self.DEFAULT_TOOL_DIRS:
path = Path(dir_path).expanduser()
tool_count = 0
if path.exists():
# Count Python files
tool_count = len(list(path.glob("*.py")))
dirs.append({
"path": str(path),
"exists": path.exists(),
"tool_count": tool_count
})
# Check custom dirs
for dir_path in self._custom_dirs:
path = Path(dir_path).expanduser()
tool_count = 0
if path.exists():
tool_count = len(list(path.glob("*.py")))
dirs.append({
"path": str(path),
"exists": path.exists(),
"tool_count": tool_count,
"custom": True
})
return dirs
def _check_tool_dependencies(self) -> Dict[str, List[Dict[str, Any]]]:
"""Check dependencies for known tools."""
result = {}
for tool, deps in self.TOOL_DEPENDENCIES.items():
result[tool] = []
for dep in deps:
spec = importlib.util.find_spec(dep.replace("-", "_"))
result[tool].append({
"name": dep,
"available": spec is not None
})
return result
def diagnose_json(self) -> str:
"""
Run diagnostics and return JSON output.
Returns:
JSON string with diagnostic results
"""
result = self.diagnose()
return json.dumps(result, indent=2)
def diagnose_human(self) -> str:
"""
Run diagnostics and return human-readable output.
Returns:
Formatted string with diagnostic results
"""
result = self.diagnose()
lines = []
lines.append("=" * 60)
lines.append("PraisonAI Tools Doctor")
lines.append("=" * 60)
lines.append("")
# Core packages
lines.append("Core Packages:")
status = "✓" if result["praisonaiagents_installed"] else "✗"
lines.append(f" {status} praisonaiagents")
status = "✓" if result["praisonai_tools_installed"] else "✗"
lines.append(f" {status} praisonai-tools (optional)")
lines.append("")
# Built-in tools
lines.append(f"Built-in Tools ({len(result['builtin_tools'])}):")
if result["builtin_tools"]:
for tool in result["builtin_tools"][:10]:
lines.append(f" • {tool}")
if len(result["builtin_tools"]) > 10:
lines.append(f" ... and {len(result['builtin_tools']) - 10} more")
else:
lines.append(" (none found)")
lines.append("")
# praisonai-tools
if result["praisonai_tools_available"]:
lines.append(f"praisonai-tools ({len(result['praisonai_tools_available'])}):")
for tool in result["praisonai_tools_available"][:10]:
lines.append(f" • {tool}")
if len(result["praisonai_tools_available"]) > 10:
lines.append(f" ... and {len(result['praisonai_tools_available']) - 10} more")
lines.append("")
# Custom directories
lines.append("Custom Tool Directories:")
for dir_info in result["custom_tools_dirs"]:
status = "✓" if dir_info["exists"] else "✗"
count = f" ({dir_info['tool_count']} tools)" if dir_info["exists"] else ""
custom = " [custom]" if dir_info.get("custom") else ""
lines.append(f" {status} {dir_info['path']}{count}{custom}")
lines.append("")
# Issues
if result["issues"]:
lines.append("Issues Found:")
for issue in result["issues"]:
icon = {"error": "❌", "warning": "⚠️", "info": "ℹ️"}.get(issue["severity"], "•")
lines.append(f" {icon} {issue['message']}")
if issue.get("hint"):
lines.append(f" Hint: {issue['hint']}")
lines.append("")
else:
lines.append("✓ No issues found")
lines.append("")
lines.append("=" * 60)
return "\n".join(lines)
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.6.0"
__version__ = "2.6.2"
+3 -3
View File
@@ -90,13 +90,13 @@ call = [
"openai>=1.54.0",
]
train = []
crewai = ["crewai>=0.157.0", "praisonai-tools>=0.0.22"]
autogen = ["pyautogen==0.2.29", "praisonai-tools>=0.0.22", "crewai"]
crewai = ["crewai>=0.157.0", "praisonai-tools>=0.1.0"]
autogen = ["pyautogen==0.2.29", "praisonai-tools>=0.1.0", "crewai"]
autogen-v4 = [
"autogen-agentchat>=0.4.0",
"autogen-ext[openai]>=0.4.0",
"autogen-core>=0.4.0",
"praisonai-tools>=0.0.22",
"praisonai-tools>=0.1.0",
"crewai"
]
File diff suppressed because it is too large. Load diff
+7 -253
View File
@@ -1020,18 +1020,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1d/8f/c7f227eb42cfeaddce3eb0c96c60cbca37797fa7b34f8e1aeadf6c5c0983/Deprecated-1.2.15-py2.py3-none-any.whl", hash = "sha256:353bc4a8ac4bfc96800ddab349d89c25dec1079f65fd53acdcc1e0b975b21320", size = 9941, upload-time = "2024-11-15T14:42:03.315Z" },
]
[[package]]
name = "deprecation"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" },
]
[[package]]
name = "diskcache"
version = "5.6.3"
@@ -1073,12 +1061,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/7c/e9fcff7623954d86bdc17782036cbf715ecab1bec4847c008557affe1ca8/docstring_parser-0.16-py3-none-any.whl", hash = "sha256:bf0a1387354d3691d102edef7ec124f219ef639982d096e26e3b60aeffa90637", size = 36533, upload-time = "2024-03-15T10:39:41.527Z" },
]
[[package]]
name = "docx2txt"
version = "0.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/7d/60ee3f2b16d9bfdfa72e8599470a2c1a5b759cb113c6fe1006be28359327/docx2txt-0.8.tar.gz", hash = "sha256:2c06d98d7cfe2d3947e5760a57d924e3ff07745b379c8737723922e7009236e5", size = 2814, upload-time = "2019-06-23T19:58:36.94Z" }
[[package]]
name = "duckduckgo-search"
version = "7.1.0"
@@ -1811,15 +1793,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" },
]
[[package]]
name = "iniconfig"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646, upload-time = "2023-01-07T11:08:11.254Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892, upload-time = "2023-01-07T11:08:09.864Z" },
]
[[package]]
name = "instructor"
version = "1.3.3"
@@ -2054,29 +2027,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/a8/17f5e28cecdbd6d48127c22abdb794740803491f422a11905c4569d8e139/kubernetes-31.0.0-py2.py3-none-any.whl", hash = "sha256:bf141e2d380c8520eada8b351f4e319ffee9636328c137aa432bc486ca1200e1", size = 1857013, upload-time = "2024-09-20T03:16:06.05Z" },
]
[[package]]
name = "lancedb"
version = "0.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "deprecation" },
{ name = "numpy" },
{ name = "overrides" },
{ name = "packaging" },
{ name = "pyarrow" },
{ name = "pydantic" },
{ name = "tqdm" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/3e/6b/4df816df8c391723bb30b05ea37c5dbc89a390570584e8620883ef680de3/lancedb-0.22.0-cp39-abi3-macosx_10_15_x86_64.whl", hash = "sha256:6fcc19983a96bf6c299f516eb9987919ab66397dfd8c4e4631a97a89ee177592", size = 31899110, upload-time = "2025-04-25T21:53:19.52Z" },
{ url = "https://files.pythonhosted.org/packages/bf/56/9a9baff82d4328210b76fd97e83c78f018b18ee3f3c475212a7d20925194/lancedb-0.22.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:b449b11590a050cfda2feaabb63c3b58a9de7619b43e9268e3fb3941e849f362", size = 29792233, upload-time = "2025-04-25T21:48:23.434Z" },
{ url = "https://files.pythonhosted.org/packages/60/8a/0a5a43de6cde2566f7aed28eec93849074e9a22d3b6d97de91849653c61e/lancedb-0.22.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1282782e78b83ba65940a3728a22239383000de1c503535175fc1198368caa", size = 31247499, upload-time = "2025-04-25T21:42:14.403Z" },
{ url = "https://files.pythonhosted.org/packages/2e/a2/0215a1310272cad973a504d11f71bface7542d462513fd5b1e6a5ba36440/lancedb-0.22.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3cc4716e31ac28f8a5bc7c7eba24a407097fce73dbf5966e08c1931ac9da19", size = 33743217, upload-time = "2025-04-25T21:36:59.565Z" },
{ url = "https://files.pythonhosted.org/packages/92/8c/97ccecb0095b97d1454b270e1fd97dc055fc0f5e92ee6a47e5a12f081169/lancedb-0.22.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:705f7a97849dea76209357045816dd2b735e5db70293b96627d8c76135873ef2", size = 31255923, upload-time = "2025-04-25T21:41:14.746Z" },
{ url = "https://files.pythonhosted.org/packages/6e/96/10461164455ebedfa6046f21d9b598a71608b610aa51e979643fac45678e/lancedb-0.22.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:726b0d239928776a1b9f019588375f040086d86101cb250206462898890bfcc9", size = 33782554, upload-time = "2025-04-25T21:37:18.562Z" },
{ url = "https://files.pythonhosted.org/packages/86/05/55d75934ab69484f661348bb9e0578cde8e5b1fd72399edd99915b76c2e2/lancedb-0.22.0-cp39-abi3-win_amd64.whl", hash = "sha256:22105db61331b964fefd5d69eb42aa483e8cf0ea5375f9fb0bb67f481f9adfbc", size = 32968874, upload-time = "2025-04-25T21:54:59.567Z" },
]
[[package]]
name = "langchain"
version = "0.3.25"
@@ -2759,15 +2709,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442, upload-time = "2024-08-18T19:48:21.909Z" },
]
[[package]]
name = "nodeenv"
version = "1.9.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
]
[[package]]
name = "numpy"
version = "1.26.4"
@@ -3721,18 +3662,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/05/7d768fa3ca23c9b3e1e09117abeded1501119f1d8de0ab722938c91ab25d/orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825", size = 134944, upload-time = "2024-11-23T19:42:07.842Z" },
]
[[package]]
name = "outcome"
version = "1.3.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" },
]
[[package]]
name = "overrides"
version = "7.7.0"
@@ -4005,15 +3934,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/ae/580600f441f6fc05218bd6c9d5794f4aef072a7d9093b291f1c50a9db8bc/plotly-5.24.1-py3-none-any.whl", hash = "sha256:f67073a1e637eb0dc3e46324d9d51e2fe76e9727c892dde64ddf1e1b51f29089", size = 19054220, upload-time = "2024-09-12T15:36:24.08Z" },
]
[[package]]
name = "pluggy"
version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" },
]
[[package]]
name = "portalocker"
version = "2.7.0"
@@ -4194,9 +4114,9 @@ requires-dist = [
{ name = "playwright", marker = "extra == 'chat'", specifier = ">=1.47.0" },
{ name = "playwright", marker = "extra == 'code'", specifier = ">=1.47.0" },
{ name = "plotly", marker = "extra == 'realtime'", specifier = ">=5.24.0" },
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.22" },
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.1.0" },
{ name = "praisonai-tools", marker = "extra == 'autogen-v4'", specifier = ">=0.1.0" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.1.0" },
{ name = "praisonaiagents", specifier = ">=0.4.0" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
@@ -4227,27 +4147,14 @@ provides-extras = ["ui", "gradio", "api", "agentops", "google", "openai", "anthr
[[package]]
name = "praisonai-tools"
version = "0.0.22"
version = "0.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "beautifulsoup4" },
{ name = "chromadb" },
{ name = "click" },
{ name = "docker" },
{ name = "docx2txt" },
{ name = "lancedb" },
{ name = "langchain" },
{ name = "openai" },
{ name = "pydantic" },
{ name = "pyright" },
{ name = "pytest" },
{ name = "pytube" },
{ name = "requests" },
{ name = "selenium" },
{ name = "praisonaiagents" },
]
sdist = { url = "https://files.pythonhosted.org/packages/12/f4/4bf2e2f2a775b976a4f3c0c7215eefc7b2f83179182527d05adb7c9354ed/praisonai_tools-0.0.22.tar.gz", hash = "sha256:1e41d0d783ea0fb076ff24c4554afc2a2fad799be8168221fe2535c40626f6df", size = 6883241, upload-time = "2025-06-07T00:16:14.16Z" }
sdist = { url = "https://files.pythonhosted.org/packages/c3/28/6d782c17d263885c8d7af4311cf0283c8a996c638b738c0284dd34e82abf/praisonai_tools-0.1.6.tar.gz", hash = "sha256:41cbeb9fce887b4887b13208fc1d8cafc9a4b09e69c1c99ac53b6ab38ac4a347", size = 120054, upload-time = "2025-12-24T13:09:40.992Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/d2/999d4b7bf0fa1debbe4f2c6415483b0b133912cbc1fe165d52c3368e2de2/praisonai_tools-0.0.22-py3-none-any.whl", hash = "sha256:57c19e8862bde43ed55c0b9166f0c77eeac491cbc0670c9f67527ac4337ca502", size = 54278, upload-time = "2025-06-07T00:16:12.121Z" },
{ url = "https://files.pythonhosted.org/packages/25/4b/75a39c79f808af4c131f6ac3ac5fba04d9cb697212af42200121accd16fc/praisonai_tools-0.1.6-py3-none-any.whl", hash = "sha256:bf7a13196d0af3ffbd2c50c3a4effd3915d8ee80e69afceca1a74a272822688b", size = 190107, upload-time = "2025-12-24T13:09:39.467Z" },
]
[[package]]
@@ -4426,48 +4333,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
]
[[package]]
name = "pyarrow"
version = "18.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7f/7b/640785a9062bb00314caa8a387abce547d2a420cf09bd6c715fe659ccffb/pyarrow-18.1.0.tar.gz", hash = "sha256:9386d3ca9c145b5539a1cfc75df07757dff870168c959b473a0bccbc3abc8c73", size = 1118671, upload-time = "2024-11-26T02:01:48.62Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/bb/8d4a1573f66e0684f190dd2b55fd0b97a7214de8882d58a3867e777bf640/pyarrow-18.1.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:e21488d5cfd3d8b500b3238a6c4b075efabc18f0f6d80b29239737ebd69caa6c", size = 29531620, upload-time = "2024-11-26T01:58:27.03Z" },
{ url = "https://files.pythonhosted.org/packages/30/90/893acfad917533b624a97b9e498c0e8393908508a0a72d624fe935e632bf/pyarrow-18.1.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:b516dad76f258a702f7ca0250885fc93d1fa5ac13ad51258e39d402bd9e2e1e4", size = 30836521, upload-time = "2024-11-26T01:58:34.607Z" },
{ url = "https://files.pythonhosted.org/packages/a3/2a/526545a7464b5fb2fa6e2c4bad16ca90e59e1843025c534fd907b7f73e5a/pyarrow-18.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f443122c8e31f4c9199cb23dca29ab9427cef990f283f80fe15b8e124bcc49b", size = 39213905, upload-time = "2024-11-26T01:58:40.558Z" },
{ url = "https://files.pythonhosted.org/packages/8a/77/4b3fab91a30e19e233e738d0c5eca5a8f6dd05758bc349a2ca262c65de79/pyarrow-18.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0a03da7f2758645d17b7b4f83c8bffeae5bbb7f974523fe901f36288d2eab71", size = 40128881, upload-time = "2024-11-26T01:58:45.561Z" },
{ url = "https://files.pythonhosted.org/packages/aa/e2/a88e16c5e45e562449c52305bd3bc2f9d704295322d3434656e7ccac1444/pyarrow-18.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ba17845efe3aa358ec266cf9cc2800fa73038211fb27968bfa88acd09261a470", size = 38627517, upload-time = "2024-11-26T01:58:50.922Z" },
{ url = "https://files.pythonhosted.org/packages/6d/84/8037c20005ccc7b869726465be0957bd9c29cfc88612962030f08292ad06/pyarrow-18.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:3c35813c11a059056a22a3bef520461310f2f7eea5c8a11ef9de7062a23f8d56", size = 40060187, upload-time = "2024-11-26T01:58:56.848Z" },
{ url = "https://files.pythonhosted.org/packages/2a/38/d6435c723ff73df8ae74626ea778262fbcc2b9b0d1a4f3db915b61711b05/pyarrow-18.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9736ba3c85129d72aefa21b4f3bd715bc4190fe4426715abfff90481e7d00812", size = 25118314, upload-time = "2024-11-26T01:59:02.303Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4d/a4988e7d82f4fbc797715db4185939a658eeffb07a25bab7262bed1ea076/pyarrow-18.1.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:eaeabf638408de2772ce3d7793b2668d4bb93807deed1725413b70e3156a7854", size = 29554860, upload-time = "2024-11-26T01:59:06.94Z" },
{ url = "https://files.pythonhosted.org/packages/59/03/3a42c5c1e4bd4c900ab62aa1ff6b472bdb159ba8f1c3e5deadab7222244f/pyarrow-18.1.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:3b2e2239339c538f3464308fd345113f886ad031ef8266c6f004d49769bb074c", size = 30867076, upload-time = "2024-11-26T01:59:11.475Z" },
{ url = "https://files.pythonhosted.org/packages/75/7e/332055ac913373e89256dce9d14b7708f55f7bd5be631456c897f0237738/pyarrow-18.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f39a2e0ed32a0970e4e46c262753417a60c43a3246972cfc2d3eb85aedd01b21", size = 39212135, upload-time = "2024-11-26T01:59:16.045Z" },
{ url = "https://files.pythonhosted.org/packages/8c/64/5099cdb325828722ef7ffeba9a4696f238eb0cdeae227f831c2d77fcf1bd/pyarrow-18.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31e9417ba9c42627574bdbfeada7217ad8a4cbbe45b9d6bdd4b62abbca4c6f6", size = 40125195, upload-time = "2024-11-26T01:59:21.267Z" },
{ url = "https://files.pythonhosted.org/packages/83/88/1938d783727db1b178ff71bc6a6143d7939e406db83a9ec23cad3dad325c/pyarrow-18.1.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:01c034b576ce0eef554f7c3d8c341714954be9b3f5d5bc7117006b85fcf302fe", size = 38641884, upload-time = "2024-11-26T01:59:26.672Z" },
{ url = "https://files.pythonhosted.org/packages/5e/b5/9e14e9f7590e0eaa435ecea84dabb137284a4dbba7b3c337b58b65b76d95/pyarrow-18.1.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f266a2c0fc31995a06ebd30bcfdb7f615d7278035ec5b1cd71c48d56daaf30b0", size = 40076877, upload-time = "2024-11-26T01:59:31.926Z" },
{ url = "https://files.pythonhosted.org/packages/4d/a3/817ac7fe0891a2d66e247e223080f3a6a262d8aefd77e11e8c27e6acf4e1/pyarrow-18.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:d4f13eee18433f99adefaeb7e01d83b59f73360c231d4782d9ddfaf1c3fbde0a", size = 25119811, upload-time = "2024-11-26T01:59:35.669Z" },
{ url = "https://files.pythonhosted.org/packages/6a/50/12829e7111b932581e51dda51d5cb39207a056c30fe31ef43f14c63c4d7e/pyarrow-18.1.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:9f3a76670b263dc41d0ae877f09124ab96ce10e4e48f3e3e4257273cee61ad0d", size = 29514620, upload-time = "2024-11-26T01:59:39.797Z" },
{ url = "https://files.pythonhosted.org/packages/d1/41/468c944eab157702e96abab3d07b48b8424927d4933541ab43788bb6964d/pyarrow-18.1.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:da31fbca07c435be88a0c321402c4e31a2ba61593ec7473630769de8346b54ee", size = 30856494, upload-time = "2024-11-26T01:59:44.725Z" },
{ url = "https://files.pythonhosted.org/packages/68/f9/29fb659b390312a7345aeb858a9d9c157552a8852522f2c8bad437c29c0a/pyarrow-18.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:543ad8459bc438efc46d29a759e1079436290bd583141384c6f7a1068ed6f992", size = 39203624, upload-time = "2024-11-26T01:59:49.189Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f6/19360dae44200e35753c5c2889dc478154cd78e61b1f738514c9f131734d/pyarrow-18.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0743e503c55be0fdb5c08e7d44853da27f19dc854531c0570f9f394ec9671d54", size = 40139341, upload-time = "2024-11-26T01:59:54.849Z" },
{ url = "https://files.pythonhosted.org/packages/bb/e6/9b3afbbcf10cc724312e824af94a2e993d8ace22994d823f5c35324cebf5/pyarrow-18.1.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d4b3d2a34780645bed6414e22dda55a92e0fcd1b8a637fba86800ad737057e33", size = 38618629, upload-time = "2024-11-26T01:59:59.966Z" },
{ url = "https://files.pythonhosted.org/packages/3a/2e/3b99f8a3d9e0ccae0e961978a0d0089b25fb46ebbcfb5ebae3cca179a5b3/pyarrow-18.1.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c52f81aa6f6575058d8e2c782bf79d4f9fdc89887f16825ec3a66607a5dd8e30", size = 40078661, upload-time = "2024-11-26T02:00:04.55Z" },
{ url = "https://files.pythonhosted.org/packages/76/52/f8da04195000099d394012b8d42c503d7041b79f778d854f410e5f05049a/pyarrow-18.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ad4892617e1a6c7a551cfc827e072a633eaff758fa09f21c4ee548c30bcaf99", size = 25092330, upload-time = "2024-11-26T02:00:09.576Z" },
{ url = "https://files.pythonhosted.org/packages/cb/87/aa4d249732edef6ad88899399047d7e49311a55749d3c373007d034ee471/pyarrow-18.1.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84e314d22231357d473eabec709d0ba285fa706a72377f9cc8e1cb3c8013813b", size = 29497406, upload-time = "2024-11-26T02:00:14.469Z" },
{ url = "https://files.pythonhosted.org/packages/3c/c7/ed6adb46d93a3177540e228b5ca30d99fc8ea3b13bdb88b6f8b6467e2cb7/pyarrow-18.1.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f591704ac05dfd0477bb8f8e0bd4b5dc52c1cadf50503858dce3a15db6e46ff2", size = 30835095, upload-time = "2024-11-26T02:00:19.347Z" },
{ url = "https://files.pythonhosted.org/packages/41/d7/ed85001edfb96200ff606943cff71d64f91926ab42828676c0fc0db98963/pyarrow-18.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acb7564204d3c40babf93a05624fc6a8ec1ab1def295c363afc40b0c9e66c191", size = 39194527, upload-time = "2024-11-26T02:00:24.085Z" },
{ url = "https://files.pythonhosted.org/packages/59/16/35e28eab126342fa391593415d79477e89582de411bb95232f28b131a769/pyarrow-18.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74de649d1d2ccb778f7c3afff6085bd5092aed4c23df9feeb45dd6b16f3811aa", size = 40131443, upload-time = "2024-11-26T02:00:29.483Z" },
{ url = "https://files.pythonhosted.org/packages/0c/95/e855880614c8da20f4cd74fa85d7268c725cf0013dc754048593a38896a0/pyarrow-18.1.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:f96bd502cb11abb08efea6dab09c003305161cb6c9eafd432e35e76e7fa9b90c", size = 38608750, upload-time = "2024-11-26T02:00:34.069Z" },
{ url = "https://files.pythonhosted.org/packages/54/9d/f253554b1457d4fdb3831b7bd5f8f00f1795585a606eabf6fec0a58a9c38/pyarrow-18.1.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:36ac22d7782554754a3b50201b607d553a8d71b78cdf03b33c1125be4b52397c", size = 40066690, upload-time = "2024-11-26T02:00:39.603Z" },
{ url = "https://files.pythonhosted.org/packages/2f/58/8912a2563e6b8273e8aa7b605a345bba5a06204549826f6493065575ebc0/pyarrow-18.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:25dbacab8c5952df0ca6ca0af28f50d45bd31c1ff6fcf79e2d120b4a65ee7181", size = 25081054, upload-time = "2024-11-26T02:00:43.611Z" },
{ url = "https://files.pythonhosted.org/packages/82/f9/d06ddc06cab1ada0c2f2fd205ac8c25c2701182de1b9c4bf7a0a44844431/pyarrow-18.1.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6a276190309aba7bc9d5bd2933230458b3521a4317acfefe69a354f2fe59f2bc", size = 29525542, upload-time = "2024-11-26T02:00:48.094Z" },
{ url = "https://files.pythonhosted.org/packages/ab/94/8917e3b961810587ecbdaa417f8ebac0abb25105ae667b7aa11c05876976/pyarrow-18.1.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:ad514dbfcffe30124ce655d72771ae070f30bf850b48bc4d9d3b25993ee0e386", size = 30829412, upload-time = "2024-11-26T02:00:52.458Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e3/3b16c3190f3d71d3b10f6758d2d5f7779ef008c4fd367cedab3ed178a9f7/pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aebc13a11ed3032d8dd6e7171eb6e86d40d67a5639d96c35142bd568b9299324", size = 39119106, upload-time = "2024-11-26T02:00:57.219Z" },
{ url = "https://files.pythonhosted.org/packages/1d/d6/5d704b0d25c3c79532f8c0639f253ec2803b897100f64bcb3f53ced236e5/pyarrow-18.1.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6cf5c05f3cee251d80e98726b5c7cc9f21bab9e9783673bac58e6dfab57ecc8", size = 40090940, upload-time = "2024-11-26T02:01:02.31Z" },
{ url = "https://files.pythonhosted.org/packages/37/29/366bc7e588220d74ec00e497ac6710c2833c9176f0372fe0286929b2d64c/pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:11b676cd410cf162d3f6a70b43fb9e1e40affbc542a1e9ed3681895f2962d3d9", size = 38548177, upload-time = "2024-11-26T02:01:07.371Z" },
{ url = "https://files.pythonhosted.org/packages/c8/11/fabf6ecabb1fe5b7d96889228ca2a9158c4c3bb732e3b8ee3f7f6d40b703/pyarrow-18.1.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:b76130d835261b38f14fc41fdfb39ad8d672afb84c447126b84d5472244cfaba", size = 40043567, upload-time = "2024-11-26T02:01:12.931Z" },
]
[[package]]
name = "pyasn1"
version = "0.6.1"
@@ -4744,45 +4609,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" },
]
[[package]]
name = "pyright"
version = "1.1.400"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nodeenv" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/cb/c306618a02d0ee8aed5fb8d0fe0ecfed0dbf075f71468f03a30b5f4e1fe0/pyright-1.1.400.tar.gz", hash = "sha256:b8a3ba40481aa47ba08ffb3228e821d22f7d391f83609211335858bf05686bdb", size = 3846546, upload-time = "2025-04-24T12:55:18.907Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/a5/5d285e4932cf149c90e3c425610c5efaea005475d5f96f1bfdb452956c62/pyright-1.1.400-py3-none-any.whl", hash = "sha256:c80d04f98b5a4358ad3a35e241dbf2a408eee33a40779df365644f8054d2517e", size = 5563460, upload-time = "2025-04-24T12:55:17.002Z" },
]
[[package]]
name = "pysocks"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
]
[[package]]
name = "pytest"
version = "8.3.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "iniconfig" },
{ name = "packaging" },
{ name = "pluggy" },
{ name = "tomli", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
]
[[package]]
name = "python-dateutil"
version = "2.9.0.post0"
@@ -4838,15 +4664,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/5f/bccb8d210f9a2bb5d5723cfe3034d940501045f8c1e074ec3c2574d2519c/python_socketio-5.12.0-py3-none-any.whl", hash = "sha256:50fe22fd2b0aa634df3e74489e42217b09af2fb22eee45f2c006df36d1d08cb9", size = 77036, upload-time = "2024-12-18T17:54:45.088Z" },
]
[[package]]
name = "pytube"
version = "15.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/e7/16fec46c8d255c4bbc4b185d89c91dc92cdb802836570d8004d0db169c91/pytube-15.0.0.tar.gz", hash = "sha256:076052efe76f390dfa24b1194ff821d4e86c17d41cb5562f3a276a8bcbfc9d1d", size = 67229, upload-time = "2023-05-07T19:39:01.903Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/64/bcf8632ed2b7a36bbf84a0544885ffa1d0b4bcf25cc0903dba66ec5fdad9/pytube-15.0.0-py3-none-any.whl", hash = "sha256:07b9904749e213485780d7eb606e5e5b8e4341aa4dccf699160876da00e12d78", size = 57594, upload-time = "2023-05-07T19:38:59.191Z" },
]
[[package]]
name = "pytz"
version = "2024.2"
@@ -5392,23 +5209,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/c4/231cac7a8385394ebbbb4f1ca662203e9d8c332825ab4f36ffc3ead09a42/scipy-1.16.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f56296fefca67ba605fd74d12f7bd23636267731a72cb3947963e76b8c0a25db", size = 38515076, upload-time = "2025-06-22T16:21:45.694Z" },
]
[[package]]
name = "selenium"
version = "4.32.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "trio" },
{ name = "trio-websocket" },
{ name = "typing-extensions" },
{ name = "urllib3", extra = ["socks"] },
{ name = "websocket-client" },
]
sdist = { url = "https://files.pythonhosted.org/packages/54/2d/fafffe946099033ccf22bf89e12eede14c1d3c5936110c5f6f2b9830722c/selenium-4.32.0.tar.gz", hash = "sha256:b9509bef4056f4083772abb1ae19ff57247d617a29255384b26be6956615b206", size = 870997, upload-time = "2025-05-02T20:35:27.325Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/37/d07ed9d13e571b2115d4ed6956d156c66816ceec0b03b2e463e80d09f572/selenium-4.32.0-py3-none-any.whl", hash = "sha256:c4d9613f8a45693d61530c9660560fadb52db7d730237bc788ddedf442391f97", size = 9369668, upload-time = "2025-05-02T20:35:24.726Z" },
]
[[package]]
name = "semantic-version"
version = "2.10.0"
@@ -5546,15 +5346,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002, upload-time = "2021-11-16T18:38:34.792Z" },
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
]
[[package]]
name = "soupsieve"
version = "2.6"
@@ -6033,38 +5824,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/35/a5/95d40e832d38584a17bf5882a58b69ecf90f864b0b048dbc02b50146cbc1/trimesh-4.7.0-py3-none-any.whl", hash = "sha256:198d5b30c97d991fd47069cb4e7058ef8431de77a467148f0efde25f171ce73b", size = 708809, upload-time = "2025-07-09T19:01:28.622Z" },
]
[[package]]
name = "trio"
version = "0.28.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
{ name = "outcome" },
{ name = "sniffio" },
{ name = "sortedcontainers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b3/73/57efab729506a8d4b89814f1e356ec8f3369de0ed4fd7e7616974d09646d/trio-0.28.0.tar.gz", hash = "sha256:4e547896fe9e8a5658e54e4c7c5fa1db748cbbbaa7c965e7d40505b928c73c05", size = 580318, upload-time = "2024-12-25T17:00:59.83Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/04/9954a59e1fb6732f5436225c9af963811d7b24ea62a8bf96991f2cb8c26e/trio-0.28.0-py3-none-any.whl", hash = "sha256:56d58977acc1635735a96581ec70513cc781b8b6decd299c487d3be2a721cd94", size = 486317, upload-time = "2024-12-25T17:00:57.665Z" },
]
[[package]]
name = "trio-websocket"
version = "0.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "trio" },
{ name = "wsproto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dd/36/abad2385853077424a11b818d9fd8350d249d9e31d583cb9c11cd4c85eda/trio-websocket-0.11.1.tar.gz", hash = "sha256:18c11793647703c158b1f6e62de638acada927344d534e3c7628eedcb746839f", size = 26511, upload-time = "2023-09-26T23:24:58.753Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/48/be/a9ae5f50cad5b6f85bd2574c2c923730098530096e170c1ce7452394d7aa/trio_websocket-0.11.1-py3-none-any.whl", hash = "sha256:520d046b0d030cf970b8b2b2e00c4c2245b3807853ecd44214acd33d74581638", size = 17408, upload-time = "2023-09-26T23:24:56.788Z" },
]
[[package]]
name = "triton"
version = "2.0.0"
@@ -6177,11 +5936,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369, upload-time = "2024-12-22T07:47:28.074Z" },
]
[package.optional-dependencies]
socks = [
{ name = "pysocks" },
]
[[package]]
name = "uv"
version = "0.7.3"