mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 17:31:52 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2d181dd74 |
No files matched your search
@@ -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.1" \
|
||||
"praisonai>=2.6.2" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -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.1" \
|
||||
"praisonai>=2.6.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -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.1" \
|
||||
"praisonai>=2.6.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -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.1.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.6.1.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]"
|
||||
|
||||
@@ -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,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)
|
||||
@@ -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.1 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)
|
||||
@@ -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 @@
|
||||
__version__ = "2.6.1"
|
||||
__version__ = "2.6.2"
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user