mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-10 11:41:32 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c86f660f66 | ||
|
|
a2d181dd74 | ||
|
|
7411fdbb52 |
No files matched your search
@@ -1994,19 +1994,23 @@ manager.delete_checkpoint("deploy-v1")
|
||||
|
||||
### Tools CLI:
|
||||
```bash
|
||||
# List all available tools
|
||||
praisonai tools list
|
||||
|
||||
# Get info about a specific tool
|
||||
praisonai tools info internet_search
|
||||
|
||||
# Search for tools
|
||||
praisonai tools search "web"
|
||||
|
||||
# Show all commands
|
||||
praisonai tools help
|
||||
praisonai tools doctor
|
||||
praisonai tools resolve shell_tool
|
||||
praisonai tools discover
|
||||
praisonai tools show-sources
|
||||
praisonai tools show-sources --template ai-video-editor
|
||||
```
|
||||
|
||||
| Command | Example | Docs |
|
||||
|---------|---------|------|
|
||||
| `tools list` | [example](examples/tools/) | [docs](https://docs.praison.ai/docs/cli/tools) |
|
||||
| `tools resolve` | [example](examples/tools/example_tools_resolve.py) | [docs](https://docs.praison.ai/docs/cli/tools-resolve) |
|
||||
| `tools discover` | [example](examples/tools/example_tools_discover.py) | [docs](https://docs.praison.ai/docs/cli/tools-discover) |
|
||||
| `tools show-sources` | [example](examples/tools/example_tools_sources.py) | [docs](https://docs.praison.ai/docs/cli/tools-show-sources) |
|
||||
|
||||
### Handoff CLI:
|
||||
```bash
|
||||
# Enable agent-to-agent task delegation
|
||||
|
||||
@@ -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.3" \
|
||||
"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.0" \
|
||||
"praisonai>=2.6.3" \
|
||||
"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.0" \
|
||||
"praisonai>=2.6.3" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# Templates CLI
|
||||
|
||||
```bash
|
||||
praisonai templates list
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates search video
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates info ai-video-editor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4 --offline
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --output edited.mp4 --strict-tools
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --tools ./my_tools.py
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --tools-dir ./tools
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --tools-source praisonai_tools.video
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates run ai-video-editor --input video.mp4 --no-template-tools-py
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates init my-project --template ai-video-editor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates cache clear
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai templates cache list
|
||||
```
|
||||
@@ -0,0 +1,104 @@
|
||||
# Templates Module
|
||||
|
||||
## Loading Templates
|
||||
|
||||
```python
|
||||
from praisonai.templates.loader import TemplateLoader
|
||||
|
||||
loader = TemplateLoader()
|
||||
|
||||
# Load from local path
|
||||
template = loader.load("./my-template")
|
||||
|
||||
# Load from URI
|
||||
template = loader.load("ai-video-editor")
|
||||
|
||||
# Load offline
|
||||
template = loader.load("ai-video-editor", offline=True)
|
||||
|
||||
# Check requirements
|
||||
missing = loader.check_requirements(template)
|
||||
```
|
||||
|
||||
## Template Discovery
|
||||
|
||||
```python
|
||||
from praisonai.templates.discovery import TemplateDiscovery
|
||||
|
||||
discovery = TemplateDiscovery(
|
||||
include_package=True,
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Find template
|
||||
result = discovery.find_template("ai-video-editor")
|
||||
|
||||
# List all templates
|
||||
templates = discovery.list_templates()
|
||||
```
|
||||
|
||||
## Template Config
|
||||
|
||||
```python
|
||||
from praisonai.templates.loader import TemplateConfig
|
||||
|
||||
# Access template properties
|
||||
template.name
|
||||
template.description
|
||||
template.version
|
||||
template.author
|
||||
template.requires
|
||||
template.workflow_file
|
||||
template.agents_file
|
||||
template.path
|
||||
```
|
||||
|
||||
## Tool Override Integration
|
||||
|
||||
```python
|
||||
from praisonai.templates.tool_override import (
|
||||
create_tool_registry_with_overrides,
|
||||
resolve_tools,
|
||||
)
|
||||
|
||||
# Build registry with template sources
|
||||
tools_sources = template.requires.get("tools_sources", [])
|
||||
|
||||
registry = create_tool_registry_with_overrides(
|
||||
tools_sources=tools_sources,
|
||||
template_dir=str(template.path),
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Resolve tool names
|
||||
tools = resolve_tools(["shell_tool"], registry=registry)
|
||||
```
|
||||
|
||||
## TEMPLATE.yaml Schema
|
||||
|
||||
```yaml
|
||||
name: my-template
|
||||
version: "1.0.0"
|
||||
description: Template description
|
||||
|
||||
requires:
|
||||
tools: [shell_tool]
|
||||
packages: [praisonai-tools]
|
||||
env: [OPENAI_API_KEY]
|
||||
tools_sources:
|
||||
- praisonai_tools.video
|
||||
- ./local_tools.py
|
||||
|
||||
workflow: workflow.yaml
|
||||
agents: agents.yaml
|
||||
|
||||
config:
|
||||
input:
|
||||
type: string
|
||||
required: true
|
||||
output:
|
||||
type: string
|
||||
|
||||
defaults:
|
||||
preset: podcast
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
# Tools CLI
|
||||
|
||||
```bash
|
||||
praisonai tools list
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools info shell_tool
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools info internet_search
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools search "web"
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools search "file"
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools doctor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools doctor --json
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve shell_tool
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve internet_search --template ai-video-editor
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve my_tool --tools ./my_tools.py
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools resolve my_tool --tools-dir ./tools
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools discover
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools discover --include praisonai_tools.video
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools discover --entrypoints
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools show-sources
|
||||
```
|
||||
|
||||
```bash
|
||||
praisonai tools show-sources --template ai-video-editor
|
||||
```
|
||||
@@ -0,0 +1,90 @@
|
||||
# Tools Module
|
||||
|
||||
## Tool Registry
|
||||
|
||||
```python
|
||||
from praisonai.templates.tool_override import (
|
||||
create_tool_registry_with_overrides,
|
||||
resolve_tools,
|
||||
)
|
||||
|
||||
# Create registry with defaults
|
||||
registry = create_tool_registry_with_overrides(include_defaults=True)
|
||||
|
||||
# Create registry with overrides
|
||||
registry = create_tool_registry_with_overrides(
|
||||
override_files=["my_tools.py"],
|
||||
override_dirs=["./tools"],
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Create registry with tools_sources
|
||||
registry = create_tool_registry_with_overrides(
|
||||
tools_sources=["praisonai_tools.video"],
|
||||
template_dir="./my-template",
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
# Resolve tool names to callables
|
||||
tools = resolve_tools(
|
||||
["shell_tool", "internet_search"],
|
||||
registry=registry,
|
||||
)
|
||||
```
|
||||
|
||||
## Tool Override Loader
|
||||
|
||||
```python
|
||||
from praisonai.templates.tool_override import ToolOverrideLoader
|
||||
|
||||
loader = ToolOverrideLoader()
|
||||
|
||||
# Load from file
|
||||
tools = loader.load_from_file("my_tools.py")
|
||||
|
||||
# Load from directory
|
||||
tools = loader.load_from_directory("./tools")
|
||||
|
||||
# Load from module
|
||||
tools = loader.load_from_module("praisonai_tools.video")
|
||||
|
||||
# Get default tool directories
|
||||
dirs = loader.get_default_tool_dirs()
|
||||
```
|
||||
|
||||
## Resolution Order
|
||||
|
||||
1. CLI `--tools` files (highest priority)
|
||||
2. CLI `--tools-dir` directories
|
||||
3. Template `tools_sources` (from TEMPLATE.yaml)
|
||||
4. Template-local `tools.py`
|
||||
5. Default dirs (`~/.praison/tools`, `~/.config/praison/tools`)
|
||||
6. Package discovery (`praisonai_tools`)
|
||||
7. Built-in tools (lowest priority)
|
||||
|
||||
## Creating Custom Tools
|
||||
|
||||
```python
|
||||
from praisonai_tools import tool
|
||||
|
||||
@tool
|
||||
def my_custom_tool(query: str) -> str:
|
||||
"""My custom tool description.
|
||||
|
||||
Args:
|
||||
query: The search query
|
||||
"""
|
||||
return f"Result for: {query}"
|
||||
```
|
||||
|
||||
## Template tools_sources
|
||||
|
||||
```yaml
|
||||
# TEMPLATE.yaml
|
||||
requires:
|
||||
tools: [shell_tool]
|
||||
tools_sources:
|
||||
- praisonai_tools.video
|
||||
- ./local_tools.py
|
||||
- ./tools_dir/
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
"""Example: Discover tools from installed packages."""
|
||||
|
||||
# Discover from praisonaiagents built-in tools
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
print("Built-in tools from praisonaiagents:")
|
||||
for name in list(TOOL_MAPPINGS.keys())[:10]:
|
||||
print(f" - {name}")
|
||||
print(f" ... and {len(TOOL_MAPPINGS) - 10} more")
|
||||
except ImportError:
|
||||
print("praisonaiagents not installed")
|
||||
|
||||
# Discover from praisonai_tools
|
||||
try:
|
||||
import praisonai_tools
|
||||
print("\nTools from praisonai_tools:")
|
||||
|
||||
# Check for video module
|
||||
try:
|
||||
from praisonai_tools import video
|
||||
print(" - praisonai_tools.video (video editing)")
|
||||
except ImportError:
|
||||
pass
|
||||
except ImportError:
|
||||
print("praisonai_tools not installed")
|
||||
@@ -0,0 +1,18 @@
|
||||
"""Example: Resolve tool names to callable tools."""
|
||||
from praisonai.templates.tool_override import (
|
||||
create_tool_registry_with_overrides,
|
||||
resolve_tools,
|
||||
)
|
||||
|
||||
# Create registry with defaults
|
||||
registry = create_tool_registry_with_overrides(include_defaults=True)
|
||||
|
||||
print(f"Registry contains {len(registry)} tools")
|
||||
|
||||
# Resolve tool names
|
||||
tool_names = ["shell_tool", "internet_search"]
|
||||
resolved = resolve_tools(tool_names, registry=registry)
|
||||
|
||||
print(f"\nResolved {len(resolved)} tools:")
|
||||
for tool in resolved:
|
||||
print(f" - {getattr(tool, '__name__', type(tool).__name__)}")
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Example: Configure tool sources for templates."""
|
||||
from praisonai.templates.tool_override import create_tool_registry_with_overrides
|
||||
|
||||
# Create registry with tools_sources
|
||||
registry = create_tool_registry_with_overrides(
|
||||
tools_sources=["praisonai_tools.video"],
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
print(f"Registry with tools_sources: {len(registry)} tools")
|
||||
|
||||
# List some tools
|
||||
for name in list(registry.keys())[:10]:
|
||||
print(f" - {name}")
|
||||
@@ -1745,7 +1745,11 @@ Your Goal: {self.goal}"""
|
||||
|
||||
# Special handling for MCP tools
|
||||
# Check if tools is an MCP instance with the requested function name
|
||||
from ..mcp.mcp import MCP
|
||||
MCP = None
|
||||
try:
|
||||
from ..mcp.mcp import MCP
|
||||
except ImportError:
|
||||
pass # MCP not available
|
||||
|
||||
# Helper function to execute MCP tool
|
||||
def _execute_mcp_tool(mcp_instance, func_name, args):
|
||||
@@ -1780,7 +1784,7 @@ Your Goal: {self.goal}"""
|
||||
return False, None
|
||||
|
||||
# Check if tools is a single MCP instance
|
||||
if isinstance(self.tools, MCP):
|
||||
if MCP is not None and isinstance(self.tools, MCP):
|
||||
logging.debug(f"Looking for MCP tool {function_name}")
|
||||
found, result = _execute_mcp_tool(self.tools, function_name, arguments)
|
||||
if found:
|
||||
@@ -1789,7 +1793,7 @@ Your Goal: {self.goal}"""
|
||||
# Check if tools is a list that may contain MCP instances
|
||||
if isinstance(self.tools, (list, tuple)):
|
||||
for tool in self.tools:
|
||||
if isinstance(tool, MCP):
|
||||
if MCP is not None and isinstance(tool, MCP):
|
||||
logging.debug(f"Looking for MCP tool {function_name} in MCP instance")
|
||||
found, result = _execute_mcp_tool(tool, function_name, arguments)
|
||||
if found:
|
||||
@@ -2211,8 +2215,12 @@ Your Goal: {self.goal}"""
|
||||
|
||||
# Convert MCP tool objects to OpenAI format if needed
|
||||
if tool_param is not None:
|
||||
from ..mcp.mcp import MCP
|
||||
if isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
|
||||
MCP = None
|
||||
try:
|
||||
from ..mcp.mcp import MCP
|
||||
except ImportError:
|
||||
pass
|
||||
if MCP is not None and isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
|
||||
# Single MCP instance
|
||||
logging.debug("Converting single MCP tool to OpenAI format")
|
||||
openai_tool = tool_param.to_openai_tool()
|
||||
@@ -2227,7 +2235,7 @@ Your Goal: {self.goal}"""
|
||||
# List that may contain MCP instances - convert each MCP to OpenAI format
|
||||
converted_tools = []
|
||||
for t in tool_param:
|
||||
if isinstance(t, MCP) and hasattr(t, 'to_openai_tool'):
|
||||
if MCP is not None and isinstance(t, MCP) and hasattr(t, 'to_openai_tool'):
|
||||
logging.debug("Converting MCP instance in list to OpenAI format")
|
||||
openai_tools = t.to_openai_tool()
|
||||
if isinstance(openai_tools, list):
|
||||
@@ -3162,8 +3170,12 @@ Write the complete compiled report:"""
|
||||
|
||||
# Convert MCP tools if needed
|
||||
if tool_param is not None:
|
||||
from ..mcp.mcp import MCP
|
||||
if isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
|
||||
MCP = None
|
||||
try:
|
||||
from ..mcp.mcp import MCP
|
||||
except ImportError:
|
||||
pass
|
||||
if MCP is not None and isinstance(tool_param, MCP) and hasattr(tool_param, 'to_openai_tool'):
|
||||
openai_tool = tool_param.to_openai_tool()
|
||||
if openai_tool:
|
||||
if isinstance(openai_tool, list):
|
||||
|
||||
@@ -104,6 +104,24 @@ class LLM:
|
||||
OLLAMA_TOOL_USAGE_PROMPT = "Please analyze the request and use the available tools to help answer the question. Start by identifying what information you need."
|
||||
OLLAMA_FINAL_ANSWER_PROMPT = "Based on the tool results above, please provide the final answer to the original question."
|
||||
|
||||
# Force tool usage prompt template (used when model ignores tools)
|
||||
FORCE_TOOL_USAGE_PROMPT = """You MUST use one of the available tools to answer this question.
|
||||
Do NOT provide a direct answer. Respond ONLY with a tool call.
|
||||
|
||||
Available tools: {tool_names}
|
||||
|
||||
Respond with a JSON object in this format:
|
||||
{{"name": "tool_name", "arguments": {{"arg1": value1, "arg2": value2}}}}"""
|
||||
|
||||
# Tool call repair prompt template (used when tool call is malformed)
|
||||
TOOL_CALL_REPAIR_PROMPT = """Your previous tool call was malformed: {error}
|
||||
|
||||
Please try again. Available tools:
|
||||
{tool_schemas}
|
||||
|
||||
Respond with ONLY a valid JSON tool call in this format:
|
||||
{{"name": "tool_name", "arguments": {{"arg1": value1, "arg2": value2}}}}"""
|
||||
|
||||
# Ollama iteration threshold for summary generation
|
||||
OLLAMA_SUMMARY_ITERATION_THRESHOLD = 1
|
||||
|
||||
@@ -294,6 +312,16 @@ class LLM:
|
||||
# Auto-detect XML tool format for known models, or allow manual override
|
||||
self.xml_tool_format = extra_settings.get('xml_tool_format', 'auto')
|
||||
|
||||
# Tool calling reliability settings (for weak models like Ollama)
|
||||
# These are auto-configured for Ollama providers if not explicitly set
|
||||
self._max_tool_repairs_explicit = 'max_tool_repairs' in extra_settings
|
||||
self._force_tool_usage_explicit = 'force_tool_usage' in extra_settings
|
||||
self.max_tool_repairs = extra_settings.get('max_tool_repairs', 0) # Will be set to 2 for Ollama if not explicit
|
||||
self.force_tool_usage = extra_settings.get('force_tool_usage', 'never') # Will be set to 'auto' for Ollama if not explicit
|
||||
|
||||
# Apply Ollama-specific defaults after model is set
|
||||
self._apply_ollama_defaults()
|
||||
|
||||
# Token tracking
|
||||
self.last_token_metrics: Optional[TokenMetrics] = None
|
||||
self.session_token_metrics: Optional[TokenMetrics] = None
|
||||
@@ -347,6 +375,16 @@ class LLM:
|
||||
self._console = Console()
|
||||
return self._console
|
||||
|
||||
def _apply_ollama_defaults(self):
|
||||
"""Apply Ollama-specific defaults for tool calling reliability."""
|
||||
if self._is_ollama_provider():
|
||||
# Apply defaults only if not explicitly set by user
|
||||
if not self._max_tool_repairs_explicit:
|
||||
self.max_tool_repairs = 2
|
||||
if not self._force_tool_usage_explicit:
|
||||
self.force_tool_usage = 'auto'
|
||||
logging.debug(f"[OLLAMA_RELIABILITY] Applied Ollama defaults: max_tool_repairs={self.max_tool_repairs}, force_tool_usage={self.force_tool_usage}")
|
||||
|
||||
def _is_ollama_provider(self) -> bool:
|
||||
"""Detect if this is an Ollama provider regardless of naming convention"""
|
||||
if not self.model:
|
||||
@@ -562,14 +600,150 @@ class LLM:
|
||||
def _format_ollama_tool_result_message(self, function_name: str, tool_result: Any) -> Dict[str, str]:
|
||||
"""
|
||||
Format tool result message for Ollama provider.
|
||||
Simplified approach without hardcoded regex extraction.
|
||||
Enhanced to instruct model to use the result for final answer.
|
||||
"""
|
||||
tool_result_str = str(tool_result)
|
||||
return {
|
||||
"role": "user",
|
||||
"content": f"The {function_name} function returned: {tool_result_str}"
|
||||
"content": f"""Tool execution complete.
|
||||
Function: {function_name}
|
||||
Result: {tool_result_str}
|
||||
|
||||
Now provide your final answer using this result."""
|
||||
}
|
||||
|
||||
def _get_tool_names_for_prompt(self, formatted_tools: Optional[List]) -> str:
|
||||
"""Extract tool names from formatted tools for prompts."""
|
||||
if not formatted_tools:
|
||||
return "None"
|
||||
names = []
|
||||
for tool in formatted_tools:
|
||||
if isinstance(tool, dict) and 'function' in tool:
|
||||
names.append(tool['function'].get('name', 'unknown'))
|
||||
return ', '.join(names) if names else "None"
|
||||
|
||||
def _get_tool_schemas_for_prompt(self, formatted_tools: Optional[List]) -> str:
|
||||
"""Generate compact tool schemas for repair prompts."""
|
||||
if not formatted_tools:
|
||||
return "None"
|
||||
schemas = []
|
||||
for tool in formatted_tools:
|
||||
if isinstance(tool, dict) and 'function' in tool:
|
||||
func = tool['function']
|
||||
name = func.get('name', 'unknown')
|
||||
params = func.get('parameters', {})
|
||||
required = params.get('required', [])
|
||||
props = params.get('properties', {})
|
||||
param_strs = []
|
||||
for pname, pinfo in props.items():
|
||||
ptype = pinfo.get('type', 'any')
|
||||
req = '*' if pname in required else ''
|
||||
param_strs.append(f"{pname}{req}: {ptype}")
|
||||
schemas.append(f"- {name}({', '.join(param_strs)})")
|
||||
return '\n'.join(schemas) if schemas else "None"
|
||||
|
||||
def _should_force_tool_usage(self, response_text: str, tool_calls: Optional[List], formatted_tools: Optional[List], iteration_count: int) -> bool:
|
||||
"""
|
||||
Determine if we should force tool usage based on settings and context.
|
||||
|
||||
Returns True if:
|
||||
- force_tool_usage == 'always' and tools exist and no tool_calls
|
||||
- force_tool_usage == 'auto' and is Ollama and first iteration and tools exist and no tool_calls
|
||||
"""
|
||||
if not formatted_tools:
|
||||
return False
|
||||
if tool_calls:
|
||||
return False
|
||||
|
||||
if self.force_tool_usage == 'never':
|
||||
return False
|
||||
elif self.force_tool_usage == 'always':
|
||||
return True
|
||||
elif self.force_tool_usage == 'auto':
|
||||
# Auto mode: only for Ollama on first iteration when model ignores tools
|
||||
return self._is_ollama_provider() and iteration_count == 0
|
||||
return False
|
||||
|
||||
def _try_parse_tool_call_json(self, response_text: str, iteration_count: int) -> tuple:
|
||||
"""
|
||||
Try to parse tool call from JSON response text.
|
||||
|
||||
Returns:
|
||||
tuple: (tool_calls list or None, parse_error string or None)
|
||||
"""
|
||||
if not response_text or not response_text.strip():
|
||||
return None, None
|
||||
|
||||
try:
|
||||
response_json = json.loads(response_text.strip())
|
||||
if isinstance(response_json, dict) and "name" in response_json:
|
||||
tool_calls = [{
|
||||
"id": f"tool_{iteration_count}",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": response_json["name"],
|
||||
"arguments": json.dumps(response_json.get("arguments", {}))
|
||||
}
|
||||
}]
|
||||
return tool_calls, None
|
||||
elif isinstance(response_json, list):
|
||||
tool_calls = []
|
||||
for idx, tool_json in enumerate(response_json):
|
||||
if isinstance(tool_json, dict) and "name" in tool_json:
|
||||
tool_calls.append({
|
||||
"id": f"tool_{iteration_count}_{idx}",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_json["name"],
|
||||
"arguments": json.dumps(tool_json.get("arguments", {}))
|
||||
}
|
||||
})
|
||||
return tool_calls if tool_calls else None, None
|
||||
else:
|
||||
return None, "Response is not a valid tool call format (missing 'name' field)"
|
||||
except json.JSONDecodeError as e:
|
||||
return None, f"JSON parse error: {str(e)}"
|
||||
|
||||
def _validate_tool_call(self, tool_call: Dict, formatted_tools: Optional[List]) -> Optional[str]:
|
||||
"""
|
||||
Validate a tool call against available tools.
|
||||
|
||||
Returns:
|
||||
Error message string if invalid, None if valid
|
||||
"""
|
||||
if not formatted_tools:
|
||||
return None
|
||||
|
||||
try:
|
||||
func_info = tool_call.get('function', {})
|
||||
tool_name = func_info.get('name', '')
|
||||
arguments_str = func_info.get('arguments', '{}')
|
||||
|
||||
# Check if tool name exists
|
||||
available_names = [t['function']['name'] for t in formatted_tools if isinstance(t, dict) and 'function' in t]
|
||||
if tool_name not in available_names:
|
||||
return f"Unknown tool '{tool_name}'. Available: {', '.join(available_names)}"
|
||||
|
||||
# Try to parse arguments
|
||||
try:
|
||||
arguments = json.loads(arguments_str) if isinstance(arguments_str, str) else arguments_str
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Invalid arguments JSON: {str(e)}"
|
||||
|
||||
# Find the tool schema and validate required args
|
||||
for tool in formatted_tools:
|
||||
if isinstance(tool, dict) and tool.get('function', {}).get('name') == tool_name:
|
||||
params = tool['function'].get('parameters', {})
|
||||
required = params.get('required', [])
|
||||
missing = [r for r in required if r not in arguments]
|
||||
if missing:
|
||||
return f"Missing required arguments for '{tool_name}': {', '.join(missing)}"
|
||||
break
|
||||
|
||||
return None # Valid
|
||||
except Exception as e:
|
||||
return f"Validation error: {str(e)}"
|
||||
|
||||
def _process_stream_delta(self, delta, response_text: str, tool_calls: List[Dict], formatted_tools: Optional[List] = None) -> tuple:
|
||||
"""
|
||||
Process a streaming delta chunk to extract content and tool calls.
|
||||
@@ -1637,6 +1811,18 @@ class LLM:
|
||||
if tool_calls:
|
||||
logging.debug(f"Parsed {len(tool_calls)} tool call(s) from XML format")
|
||||
|
||||
# Force tool usage logic: if model ignores tools but should use them
|
||||
if self._should_force_tool_usage(response_text, tool_calls, formatted_tools, iteration_count):
|
||||
tool_names = self._get_tool_names_for_prompt(formatted_tools)
|
||||
force_prompt = self.FORCE_TOOL_USAGE_PROMPT.format(tool_names=tool_names)
|
||||
logging.debug(f"[OLLAMA_RELIABILITY] Force tool usage triggered. Adding prompt for tools: {tool_names}")
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": force_prompt
|
||||
})
|
||||
iteration_count += 1
|
||||
continue
|
||||
|
||||
# For Ollama, if response is empty but we have tools, prompt for tool usage
|
||||
if self._is_ollama_provider() and (not response_text or response_text.strip() == "") and formatted_tools and iteration_count == 0:
|
||||
messages.append({
|
||||
@@ -1646,6 +1832,37 @@ class LLM:
|
||||
iteration_count += 1
|
||||
continue
|
||||
|
||||
# Tool call repair logic: validate and repair malformed tool calls
|
||||
repair_attempt_count = getattr(self, '_current_repair_count', 0)
|
||||
if tool_calls and self.max_tool_repairs > 0 and repair_attempt_count < self.max_tool_repairs:
|
||||
# Validate each tool call
|
||||
validation_errors = []
|
||||
for tc in tool_calls:
|
||||
error = self._validate_tool_call(tc, formatted_tools)
|
||||
if error:
|
||||
validation_errors.append(error)
|
||||
|
||||
if validation_errors:
|
||||
# Tool call is invalid, attempt repair
|
||||
error_msg = "; ".join(validation_errors)
|
||||
tool_schemas = self._get_tool_schemas_for_prompt(formatted_tools)
|
||||
repair_prompt = self.TOOL_CALL_REPAIR_PROMPT.format(
|
||||
error=error_msg,
|
||||
tool_schemas=tool_schemas
|
||||
)
|
||||
logging.debug(f"[OLLAMA_RELIABILITY] Tool call repair attempt {repair_attempt_count + 1}/{self.max_tool_repairs}: {error_msg}")
|
||||
messages.append({
|
||||
"role": "user",
|
||||
"content": repair_prompt
|
||||
})
|
||||
self._current_repair_count = repair_attempt_count + 1
|
||||
iteration_count += 1
|
||||
tool_calls = None # Clear invalid tool calls
|
||||
continue
|
||||
|
||||
# Reset repair count on successful tool call
|
||||
self._current_repair_count = 0
|
||||
|
||||
# Handle tool calls - Sequential tool calling logic
|
||||
if tool_calls and execute_tool_fn:
|
||||
# Convert tool_calls to a serializable format for all providers
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Evaluation Example - Testing agent accuracy and performance
|
||||
*
|
||||
* Run: npx ts-node examples/features/evaluation.ts
|
||||
*/
|
||||
|
||||
import { Agent, accuracyEval, performanceEval } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create an agent to evaluate
|
||||
const agent = new Agent({
|
||||
instructions: "You are a math tutor. Answer math questions accurately.",
|
||||
llm: "gpt-4o-mini",
|
||||
verbose: false
|
||||
});
|
||||
|
||||
console.log("=== Accuracy Evaluation ===");
|
||||
|
||||
// Test accuracy with expected outputs
|
||||
const testCases = [
|
||||
{ input: "What is 2 + 2?", expected: "4" },
|
||||
{ input: "What is 10 * 5?", expected: "50" },
|
||||
{ input: "What is 100 / 4?", expected: "25" }
|
||||
];
|
||||
|
||||
for (const test of testCases) {
|
||||
const response = await agent.chat(test.input);
|
||||
const passed = response.includes(test.expected);
|
||||
console.log(` ${passed ? '✅' : '❌'} "${test.input}" → Expected: ${test.expected}`);
|
||||
}
|
||||
|
||||
console.log("\n=== Performance Evaluation ===");
|
||||
|
||||
// Measure response time
|
||||
const iterations = 3;
|
||||
const times: number[] = [];
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const start = Date.now();
|
||||
await agent.chat("What is 5 + 5?");
|
||||
const duration = Date.now() - start;
|
||||
times.push(duration);
|
||||
console.log(` Run ${i + 1}: ${duration}ms`);
|
||||
}
|
||||
|
||||
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
console.log(`\n Average: ${avgTime.toFixed(0)}ms`);
|
||||
console.log(` Min: ${Math.min(...times)}ms`);
|
||||
console.log(` Max: ${Math.max(...times)}ms`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Guardrails Example - Input/output validation for agents
|
||||
*
|
||||
* Run: npx ts-node examples/features/guardrails.ts
|
||||
*/
|
||||
|
||||
import { Agent, LLMGuardrail } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create an LLM-based guardrail
|
||||
const contentGuardrail = new LLMGuardrail({
|
||||
name: "ContentSafety",
|
||||
criteria: "Content must be professional, non-offensive, and factually accurate",
|
||||
llm: "gpt-4o-mini",
|
||||
threshold: 0.7,
|
||||
verbose: true
|
||||
});
|
||||
|
||||
// Create agent
|
||||
const agent = new Agent({
|
||||
instructions: "You are a helpful assistant that provides accurate information.",
|
||||
verbose: true
|
||||
});
|
||||
|
||||
// Example: Check content before responding
|
||||
const userInput = "Tell me about artificial intelligence";
|
||||
|
||||
// Validate input using check()
|
||||
const inputCheck = await contentGuardrail.check(userInput);
|
||||
console.log("Input validation:", inputCheck);
|
||||
|
||||
if (inputCheck.status === 'passed') {
|
||||
const response = await agent.chat(userInput);
|
||||
|
||||
// Validate output
|
||||
const outputCheck = await contentGuardrail.check(response);
|
||||
console.log("Output validation:", outputCheck);
|
||||
|
||||
if (outputCheck.status === 'passed') {
|
||||
console.log("\n✅ Response passed guardrails");
|
||||
} else {
|
||||
console.log("\n❌ Response failed guardrails:", outputCheck.reasoning);
|
||||
}
|
||||
} else {
|
||||
console.log("❌ Input failed guardrails:", inputCheck.reasoning);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Knowledge Base Example - RAG with agents
|
||||
*
|
||||
* Run: npx ts-node examples/features/knowledge.ts
|
||||
*/
|
||||
|
||||
import { Agent, BaseKnowledgeBase, type Knowledge } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create knowledge base
|
||||
const kb = new BaseKnowledgeBase();
|
||||
|
||||
// Add knowledge entries
|
||||
kb.addKnowledge({
|
||||
id: "overview",
|
||||
type: "documentation",
|
||||
content: `
|
||||
PraisonAI is a TypeScript SDK for building AI agents.
|
||||
It supports multiple LLM providers including OpenAI, Anthropic, and Google.
|
||||
Key features include:
|
||||
- Simple Agent API with instructions
|
||||
- Multi-agent orchestration with Agents class
|
||||
- Workflow-based execution
|
||||
- Tool integration with auto-schema generation
|
||||
- Session persistence with db() factory
|
||||
`,
|
||||
metadata: { source: "docs", topic: "overview" }
|
||||
});
|
||||
|
||||
kb.addKnowledge({
|
||||
id: "agent-usage",
|
||||
type: "documentation",
|
||||
content: `
|
||||
The Agent class is the primary entry point.
|
||||
Create an agent with: new Agent({ instructions: "..." })
|
||||
Use agent.chat("message") to interact.
|
||||
Tools can be plain functions passed in the tools array.
|
||||
`,
|
||||
metadata: { source: "docs", topic: "agent" }
|
||||
});
|
||||
|
||||
// Search the knowledge base
|
||||
console.log("=== Searching Knowledge Base ===");
|
||||
const results = kb.searchKnowledge("agent");
|
||||
console.log("Found", results.length, "relevant entries");
|
||||
results.forEach((r: Knowledge, i: number) => {
|
||||
console.log(`\n[${i + 1}] ID: ${r.id}`);
|
||||
console.log(String(r.content).substring(0, 100) + "...");
|
||||
});
|
||||
|
||||
// Create agent that uses knowledge
|
||||
const knowledgeContext = results.map((r: Knowledge) => r.content).join('\n\n');
|
||||
const agent = new Agent({
|
||||
instructions: `You are a helpful assistant. Use the following knowledge to answer questions:
|
||||
|
||||
${knowledgeContext}`,
|
||||
verbose: true
|
||||
});
|
||||
|
||||
console.log("\n=== Agent Response ===");
|
||||
await agent.chat("How do I create an agent in PraisonAI?");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Memory Example - Using memory for agent context
|
||||
*
|
||||
* Run: npx ts-node examples/features/memory.ts
|
||||
*/
|
||||
|
||||
import { Agent, Memory } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create a memory instance
|
||||
const memory = new Memory({
|
||||
maxEntries: 100,
|
||||
maxTokens: 50000
|
||||
});
|
||||
|
||||
// Add some context to memory
|
||||
await memory.add("User prefers concise answers", "system", { priority: "high" });
|
||||
await memory.add("User is working on a TypeScript project", "system", { topic: "programming" });
|
||||
|
||||
// Search memory
|
||||
const results = await memory.search("TypeScript");
|
||||
console.log("Memory search results:", results);
|
||||
|
||||
// Get recent entries
|
||||
const recent = memory.getRecent(5);
|
||||
console.log("Recent entries:", recent);
|
||||
|
||||
// Create agent
|
||||
const agent = new Agent({
|
||||
instructions: "You are a helpful assistant. Use context from memory.",
|
||||
verbose: true
|
||||
});
|
||||
|
||||
await agent.chat("Help me with my project");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Observability Example - Tracing and monitoring agents
|
||||
*
|
||||
* Run: npx ts-node examples/features/observability.ts
|
||||
*/
|
||||
|
||||
import { Agent, setObservabilityAdapter, ConsoleObservabilityAdapter } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Set up console observability (logs all traces)
|
||||
const adapter = new ConsoleObservabilityAdapter();
|
||||
setObservabilityAdapter(adapter);
|
||||
|
||||
console.log("=== Observability Enabled ===");
|
||||
console.log("All agent operations will be traced to console.\n");
|
||||
|
||||
// Create agent
|
||||
const agent = new Agent({
|
||||
instructions: "You are a helpful assistant.",
|
||||
verbose: true
|
||||
});
|
||||
|
||||
// Run some operations - they will be traced
|
||||
console.log("=== Running Agent ===");
|
||||
await agent.chat("Hello! Tell me a joke.");
|
||||
|
||||
console.log("\n=== Trace Complete ===");
|
||||
console.log("Check console output above for trace information.");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Planning Example - Using planning mode for complex tasks
|
||||
*
|
||||
* Run: npx ts-node examples/features/planning.ts
|
||||
*/
|
||||
|
||||
import { Agent, Plan, PlanStep, TodoList, TodoItem } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create a plan for a complex task
|
||||
console.log("=== Creating a Plan ===");
|
||||
const plan = new Plan({
|
||||
name: "Build a Web App",
|
||||
description: "Plan to build a simple web application"
|
||||
});
|
||||
|
||||
// Add steps to the plan
|
||||
plan.addStep(new PlanStep({ description: "Initialize project structure" }));
|
||||
plan.addStep(new PlanStep({ description: "Build API endpoints" }));
|
||||
plan.addStep(new PlanStep({ description: "Create UI components" }));
|
||||
plan.addStep(new PlanStep({ description: "Write and run tests" }));
|
||||
plan.addStep(new PlanStep({ description: "Deploy to production" }));
|
||||
|
||||
console.log("Plan created with", plan.steps.length, "steps");
|
||||
|
||||
// Create a todo list
|
||||
console.log("\n=== Creating a Todo List ===");
|
||||
const todos = new TodoList();
|
||||
|
||||
todos.add(new TodoItem({ content: "Research frameworks", priority: "high" }));
|
||||
todos.add(new TodoItem({ content: "Design database schema", priority: "medium" }));
|
||||
todos.add(new TodoItem({ content: "Write documentation", priority: "low" }));
|
||||
|
||||
console.log("Todo list has", todos.items.length, "items");
|
||||
|
||||
// Create agent with planning context
|
||||
const agent = new Agent({
|
||||
instructions: `You are a project planning assistant.
|
||||
|
||||
Current plan: ${plan.name}
|
||||
Steps: ${plan.steps.map((s: PlanStep) => s.description).join(', ')}
|
||||
|
||||
Help the user execute this plan.`,
|
||||
verbose: true
|
||||
});
|
||||
|
||||
await agent.chat("What should I do first?");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* LLM Providers Example - Using different LLM providers
|
||||
*
|
||||
* Run: npx ts-node examples/features/providers.ts
|
||||
*/
|
||||
|
||||
import { Agent } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// OpenAI (default)
|
||||
console.log("=== OpenAI Agent ===");
|
||||
const openaiAgent = new Agent({
|
||||
instructions: "You are a helpful assistant.",
|
||||
llm: "gpt-4o-mini" // or just omit for default
|
||||
});
|
||||
await openaiAgent.chat("Hello from OpenAI!");
|
||||
|
||||
// Anthropic Claude
|
||||
console.log("\n=== Anthropic Agent ===");
|
||||
const anthropicAgent = new Agent({
|
||||
instructions: "You are a helpful assistant.",
|
||||
llm: "anthropic/claude-3-haiku-20240307"
|
||||
});
|
||||
// Uncomment if you have ANTHROPIC_API_KEY set
|
||||
// await anthropicAgent.chat("Hello from Anthropic!");
|
||||
|
||||
// Google Gemini
|
||||
console.log("\n=== Google Agent ===");
|
||||
const googleAgent = new Agent({
|
||||
instructions: "You are a helpful assistant.",
|
||||
llm: "google/gemini-1.5-flash"
|
||||
});
|
||||
// Uncomment if you have GOOGLE_API_KEY set
|
||||
// await googleAgent.chat("Hello from Google!");
|
||||
|
||||
// Model string formats
|
||||
console.log("\n=== Model String Formats ===");
|
||||
console.log("Supported formats:");
|
||||
console.log(" - 'gpt-4o-mini' (defaults to OpenAI)");
|
||||
console.log(" - 'openai/gpt-4o'");
|
||||
console.log(" - 'anthropic/claude-3-sonnet-20240229'");
|
||||
console.log(" - 'google/gemini-1.5-pro'");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Sessions Example - Conversation persistence with session IDs
|
||||
*
|
||||
* Run: npx ts-node examples/features/sessions.ts
|
||||
*/
|
||||
|
||||
import { Agent, db } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create an agent with session persistence
|
||||
const agent = new Agent({
|
||||
instructions: "You are a helpful assistant that remembers our conversation.",
|
||||
db: db("sqlite:./sessions.db"),
|
||||
sessionId: "user-alice-123"
|
||||
});
|
||||
|
||||
// First conversation
|
||||
console.log("=== First Conversation ===");
|
||||
await agent.chat("My name is Alice and I love programming.");
|
||||
|
||||
// Continue the conversation - agent remembers context
|
||||
console.log("\n=== Continuing Conversation ===");
|
||||
await agent.chat("What's my name and what do I love?");
|
||||
|
||||
// Get session info
|
||||
console.log("\n=== Session Info ===");
|
||||
console.log("Session ID:", agent.getSessionId());
|
||||
console.log("Run ID:", agent.getRunId());
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Skills Example - Using agent skills
|
||||
*
|
||||
* Run: npx ts-node examples/features/skills.ts
|
||||
*/
|
||||
|
||||
import { Agent, SkillManager, type Skill } from '../../src';
|
||||
|
||||
async function main() {
|
||||
// Create a skill manager
|
||||
const skillManager = new SkillManager({
|
||||
paths: ['./.praison/skills'],
|
||||
includeProject: true,
|
||||
includeUser: true
|
||||
});
|
||||
|
||||
// Discover available skills
|
||||
console.log("=== Discovering Skills ===");
|
||||
const skills = await skillManager.discover();
|
||||
|
||||
if (skills.length === 0) {
|
||||
console.log("No skills found. Create a skill in .praison/skills/");
|
||||
console.log("\nExample SKILL.md:");
|
||||
console.log(`---
|
||||
name: code-review
|
||||
description: Reviews code for best practices
|
||||
---
|
||||
|
||||
# Code Review Skill
|
||||
|
||||
When reviewing code:
|
||||
1. Check for security issues
|
||||
2. Verify error handling
|
||||
3. Suggest improvements
|
||||
`);
|
||||
} else {
|
||||
skills.forEach((skill: Skill) => {
|
||||
console.log(` - ${skill.metadata.name}: ${skill.metadata.description}`);
|
||||
});
|
||||
}
|
||||
|
||||
// Create agent (skills would be loaded automatically if configured)
|
||||
const agent = new Agent({
|
||||
instructions: "You are a helpful coding assistant.",
|
||||
verbose: true
|
||||
});
|
||||
|
||||
await agent.chat("Help me write clean code");
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Multi-Agent Pattern - Orchestrate multiple agents
|
||||
*
|
||||
* Run: npx ts-node examples/patterns/multi-agent.ts
|
||||
*/
|
||||
|
||||
import { Agent, Agents } from '../../src';
|
||||
|
||||
// Create specialized agents
|
||||
const researcher = new Agent({
|
||||
instructions: "You are a research specialist. Thoroughly research the given topic and provide detailed findings.",
|
||||
name: "Researcher"
|
||||
});
|
||||
|
||||
const writer = new Agent({
|
||||
instructions: "You are a professional writer. Take the research provided and write a clear, engaging summary.",
|
||||
name: "Writer"
|
||||
});
|
||||
|
||||
// Orchestrate agents - they run sequentially, passing output to the next
|
||||
const agents = new Agents([researcher, writer]);
|
||||
|
||||
// Alternative: use config object for more control
|
||||
// const agents = new Agents({
|
||||
// agents: [researcher, writer],
|
||||
// process: 'sequential', // or 'parallel'
|
||||
// verbose: true
|
||||
// });
|
||||
|
||||
agents.start().then(results => {
|
||||
console.log("\n=== Research Agent Output ===");
|
||||
console.log(results[0]);
|
||||
console.log("\n=== Writer Agent Output ===");
|
||||
console.log(results[1]);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Streaming Pattern - Real-time response streaming
|
||||
*
|
||||
* Run: npx ts-node examples/patterns/streaming.ts
|
||||
*/
|
||||
|
||||
import { Agent } from '../../src';
|
||||
|
||||
// Create an agent with streaming enabled
|
||||
const agent = new Agent({
|
||||
instructions: "You are a creative storyteller. Write engaging, detailed stories.",
|
||||
name: "Storyteller",
|
||||
stream: true, // Enable streaming (default is true)
|
||||
verbose: true
|
||||
});
|
||||
|
||||
// The response will stream to stdout as it's generated
|
||||
console.log("=== Streaming Story ===\n");
|
||||
agent.chat("Write a short story about a robot learning to paint.").then(() => {
|
||||
console.log("\n\n=== Story Complete ===");
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Workflow Pattern - Step-based pipeline with agent integration
|
||||
*
|
||||
* Run: npx ts-node examples/patterns/workflow.ts
|
||||
*/
|
||||
|
||||
import { Agent, Workflow } from '../../src';
|
||||
|
||||
// Create agents for the workflow
|
||||
const researcher = new Agent({
|
||||
instructions: "You are a research specialist. Research the given topic thoroughly.",
|
||||
name: "Researcher"
|
||||
});
|
||||
|
||||
const analyzer = new Agent({
|
||||
instructions: "You are a data analyst. Analyze the research and extract key insights.",
|
||||
name: "Analyzer"
|
||||
});
|
||||
|
||||
const writer = new Agent({
|
||||
instructions: "You are a professional writer. Write a clear report based on the analysis.",
|
||||
name: "Writer"
|
||||
});
|
||||
|
||||
// Create a workflow with agent steps
|
||||
const workflow = new Workflow("Research Pipeline")
|
||||
.agent(researcher, "Research the topic")
|
||||
.agent(analyzer, "Analyze the research findings")
|
||||
.agent(writer, "Write a summary report");
|
||||
|
||||
// Run the workflow
|
||||
workflow.run("Artificial Intelligence trends in 2025").then(result => {
|
||||
console.log("\n=== Workflow Results ===");
|
||||
console.log(`Steps completed: ${result.results.length}`);
|
||||
console.log(`Final output:\n${result.output}`);
|
||||
|
||||
// Access individual step results
|
||||
result.results.forEach((step, i) => {
|
||||
console.log(`\nStep ${i + 1} (${step.stepName}): ${step.status} in ${step.duration}ms`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Hello Agent - The simplest possible PraisonAI example (3 lines)
|
||||
*
|
||||
* Run: npx ts-node examples/quickstart/hello-agent.ts
|
||||
*/
|
||||
|
||||
import { Agent } from '../../src';
|
||||
|
||||
const agent = new Agent({ instructions: "You are a helpful assistant" });
|
||||
agent.chat("Hello! Tell me a fun fact about AI.").then(console.log);
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Agent with Persistence - Auto-save messages to database (4 lines)
|
||||
*
|
||||
* Run: npx ts-node examples/quickstart/with-persistence.ts
|
||||
*/
|
||||
|
||||
import { Agent, db } from '../../src';
|
||||
|
||||
const agent = new Agent({
|
||||
instructions: "You are a helpful assistant with memory",
|
||||
db: db("memory:"), // Use in-memory db for demo (use "sqlite:./data.db" for file)
|
||||
sessionId: "my-session"
|
||||
});
|
||||
|
||||
agent.chat("Remember that my favorite color is blue.").then(console.log);
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Agent with Tools - Pass plain functions as tools (5 lines)
|
||||
*
|
||||
* Run: npx ts-node examples/quickstart/with-tools.ts
|
||||
*/
|
||||
|
||||
import { Agent } from '../../src';
|
||||
|
||||
// Define a simple tool as a plain function
|
||||
const getWeather = (city: string) => `Weather in ${city}: 22°C, Sunny`;
|
||||
|
||||
const agent = new Agent({
|
||||
instructions: "You are a weather assistant. Use the getWeather tool to answer questions.",
|
||||
tools: [getWeather]
|
||||
});
|
||||
|
||||
agent.chat("What's the weather like in Paris?").then(console.log);
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "praisonai",
|
||||
"version": "1.2.4",
|
||||
"version": "1.3.0",
|
||||
"description": "PraisonAI TypeScript AI Agents Framework - Node.js, npm, and Javascript AI Agents Framework",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -1,12 +1,31 @@
|
||||
// Export implementation based on mode
|
||||
/**
|
||||
* Agent Module - Core agent classes for PraisonAI
|
||||
*
|
||||
* The primary exports are:
|
||||
* - Agent: Single agent with instructions, tools, and optional persistence
|
||||
* - Agents: Multi-agent orchestration (alias for PraisonAIAgents)
|
||||
* - Workflow: Step-based workflow execution (from workflows module)
|
||||
*/
|
||||
|
||||
// Core exports - the main API surface
|
||||
export { Agent, PraisonAIAgents, Agents } from './simple';
|
||||
export type { SimpleAgentConfig, PraisonAIAgentsConfig } from './simple';
|
||||
|
||||
// Task support (for advanced use cases)
|
||||
export { Task } from './types';
|
||||
export type { TaskConfig, AgentConfig as TaskAgentConfig } from './types';
|
||||
|
||||
// Legacy compatibility - setTaskMode is deprecated but kept for backward compat
|
||||
let useTaskMode = false;
|
||||
|
||||
/**
|
||||
* @deprecated Task mode is no longer needed. Use Agent with role/goal/backstory instead.
|
||||
*/
|
||||
export function setTaskMode(enabled: boolean) {
|
||||
if (enabled) {
|
||||
console.warn(
|
||||
'setTaskMode() is deprecated. Use Agent({ role, goal, backstory }) instead of task mode.'
|
||||
);
|
||||
}
|
||||
useTaskMode = enabled;
|
||||
}
|
||||
|
||||
export { Agent, PraisonAIAgents, Task } from './proxy';
|
||||
export type { ProxyAgentConfig } from './proxy';
|
||||
export type { AgentConfig } from './types';
|
||||
export type { TaskConfig } from './types';
|
||||
export type { PraisonAIAgentsConfig, SimpleAgentConfig } from './simple';
|
||||
@@ -1,17 +1,84 @@
|
||||
import { OpenAIService } from '../llm/openai';
|
||||
import { Logger } from '../utils/logger';
|
||||
import type { ChatCompletionTool } from 'openai/resources/chat/completions';
|
||||
import type { DbAdapter, DbMessage, DbRun } from '../db/types';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
/**
|
||||
* Agent Configuration
|
||||
*
|
||||
* The Agent class is the primary entry point for PraisonAI.
|
||||
* It supports both simple instruction-based agents and advanced configurations.
|
||||
*
|
||||
* @example Simple usage (3 lines)
|
||||
* ```typescript
|
||||
* import { Agent } from 'praisonai';
|
||||
* const agent = new Agent({ instructions: "You are helpful" });
|
||||
* await agent.chat("Hello!");
|
||||
* ```
|
||||
*
|
||||
* @example With tools (5 lines)
|
||||
* ```typescript
|
||||
* const getWeather = (city: string) => `Weather in ${city}: 20°C`;
|
||||
* const agent = new Agent({
|
||||
* instructions: "You provide weather info",
|
||||
* tools: [getWeather]
|
||||
* });
|
||||
* await agent.chat("Weather in Paris?");
|
||||
* ```
|
||||
*
|
||||
* @example With persistence (4 lines)
|
||||
* ```typescript
|
||||
* import { Agent, db } from 'praisonai';
|
||||
* const agent = new Agent({
|
||||
* instructions: "You are helpful",
|
||||
* db: db("sqlite:./data.db"),
|
||||
* sessionId: "my-session"
|
||||
* });
|
||||
* await agent.chat("Hello!");
|
||||
* ```
|
||||
*/
|
||||
export interface SimpleAgentConfig {
|
||||
/** Agent instructions/system prompt (required) */
|
||||
instructions: string;
|
||||
/** Agent name (auto-generated if not provided) */
|
||||
name?: string;
|
||||
/** Enable verbose logging (default: true) */
|
||||
verbose?: boolean;
|
||||
/** Enable pretty output formatting */
|
||||
pretty?: boolean;
|
||||
/**
|
||||
* LLM model to use. Accepts:
|
||||
* - Model name: "gpt-4o-mini", "claude-3-sonnet"
|
||||
* - Provider/model: "openai/gpt-4o", "anthropic/claude-3"
|
||||
* Default: "gpt-4o-mini"
|
||||
*/
|
||||
llm?: string;
|
||||
/** Enable markdown formatting in responses */
|
||||
markdown?: boolean;
|
||||
/** Enable streaming responses (default: true) */
|
||||
stream?: boolean;
|
||||
/**
|
||||
* Tools available to the agent.
|
||||
* Can be plain functions (auto-schema) or OpenAI tool definitions.
|
||||
*/
|
||||
tools?: any[] | Function[];
|
||||
/** Map of tool function implementations */
|
||||
toolFunctions?: Record<string, Function>;
|
||||
/** Database adapter for persistence */
|
||||
db?: DbAdapter;
|
||||
/** Session ID for conversation persistence */
|
||||
sessionId?: string;
|
||||
/** Run ID for tracing (auto-generated if not provided) */
|
||||
runId?: string;
|
||||
|
||||
// Advanced mode (role/goal/backstory) - for compatibility
|
||||
/** Agent role (advanced mode) */
|
||||
role?: string;
|
||||
/** Agent goal (advanced mode) */
|
||||
goal?: string;
|
||||
/** Agent backstory (advanced mode) */
|
||||
backstory?: string;
|
||||
}
|
||||
|
||||
export class Agent {
|
||||
@@ -25,16 +92,36 @@ export class Agent {
|
||||
private llmService: OpenAIService;
|
||||
private tools?: any[];
|
||||
private toolFunctions: Record<string, Function> = {};
|
||||
private dbAdapter?: DbAdapter;
|
||||
private sessionId: string;
|
||||
private runId: string;
|
||||
private messages: Array<{ role: string; content: string | null; tool_calls?: any[]; tool_call_id?: string }> = [];
|
||||
|
||||
constructor(config: SimpleAgentConfig) {
|
||||
this.instructions = config.instructions;
|
||||
// Build instructions from either simple or advanced mode
|
||||
if (config.instructions) {
|
||||
this.instructions = config.instructions;
|
||||
} else if (config.role || config.goal || config.backstory) {
|
||||
// Advanced mode: construct instructions from role/goal/backstory
|
||||
const parts: string[] = [];
|
||||
if (config.role) parts.push(`You are a ${config.role}.`);
|
||||
if (config.goal) parts.push(`Your goal is: ${config.goal}`);
|
||||
if (config.backstory) parts.push(`Background: ${config.backstory}`);
|
||||
this.instructions = parts.join('\n');
|
||||
} else {
|
||||
this.instructions = 'You are a helpful AI assistant.';
|
||||
}
|
||||
|
||||
this.name = config.name || `Agent_${Math.random().toString(36).substr(2, 9)}`;
|
||||
this.verbose = config.verbose ?? process.env.PRAISON_VERBOSE !== 'false';
|
||||
this.pretty = config.pretty ?? process.env.PRAISON_PRETTY === 'true';
|
||||
this.llm = config.llm || 'gpt-4o-mini';
|
||||
this.llm = config.llm || process.env.OPENAI_MODEL_NAME || process.env.PRAISONAI_MODEL || 'gpt-4o-mini';
|
||||
this.markdown = config.markdown ?? true;
|
||||
this.stream = config.stream ?? true;
|
||||
this.tools = config.tools;
|
||||
this.dbAdapter = config.db;
|
||||
this.sessionId = config.sessionId || randomUUID();
|
||||
this.runId = config.runId || randomUUID();
|
||||
this.llmService = new OpenAIService(this.llm);
|
||||
|
||||
// Configure logging
|
||||
@@ -315,7 +402,19 @@ export class Agent {
|
||||
}
|
||||
|
||||
async chat(prompt: string, previousResult?: string): Promise<string> {
|
||||
return this.start(prompt, previousResult);
|
||||
// Persist user message if db is configured
|
||||
if (this.dbAdapter) {
|
||||
await this.persistMessage('user', prompt);
|
||||
}
|
||||
|
||||
const response = await this.start(prompt, previousResult);
|
||||
|
||||
// Persist assistant response if db is configured
|
||||
if (this.dbAdapter) {
|
||||
await this.persistMessage('assistant', response);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
async execute(previousResult?: string): Promise<string> {
|
||||
@@ -323,6 +422,41 @@ export class Agent {
|
||||
return this.start(this.instructions, previousResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a message to the database
|
||||
*/
|
||||
private async persistMessage(role: 'user' | 'assistant' | 'system' | 'tool', content: string): Promise<void> {
|
||||
if (!this.dbAdapter) return;
|
||||
|
||||
try {
|
||||
const message: DbMessage = {
|
||||
id: randomUUID(),
|
||||
sessionId: this.sessionId,
|
||||
runId: this.runId,
|
||||
role,
|
||||
content,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
await this.dbAdapter.saveMessage(message);
|
||||
} catch (error) {
|
||||
await Logger.warn('Failed to persist message:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session ID for this agent
|
||||
*/
|
||||
getSessionId(): string {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the run ID for this agent
|
||||
*/
|
||||
getRunId(): string {
|
||||
return this.runId;
|
||||
}
|
||||
|
||||
getResult(): string | null {
|
||||
return null;
|
||||
}
|
||||
@@ -332,6 +466,9 @@ export class Agent {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for multi-agent orchestration
|
||||
*/
|
||||
export interface PraisonAIAgentsConfig {
|
||||
agents: Agent[];
|
||||
tasks?: string[];
|
||||
@@ -340,6 +477,28 @@ export interface PraisonAIAgentsConfig {
|
||||
process?: 'sequential' | 'parallel';
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi-agent orchestration class
|
||||
*
|
||||
* @example Simple array syntax
|
||||
* ```typescript
|
||||
* import { Agent, Agents } from 'praisonai';
|
||||
*
|
||||
* const researcher = new Agent({ instructions: "Research the topic" });
|
||||
* const writer = new Agent({ instructions: "Write based on research" });
|
||||
*
|
||||
* const agents = new Agents([researcher, writer]);
|
||||
* await agents.start();
|
||||
* ```
|
||||
*
|
||||
* @example Config object syntax
|
||||
* ```typescript
|
||||
* const agents = new Agents({
|
||||
* agents: [researcher, writer],
|
||||
* process: 'parallel'
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export class PraisonAIAgents {
|
||||
private agents: Agent[];
|
||||
private tasks: string[];
|
||||
@@ -347,7 +506,16 @@ export class PraisonAIAgents {
|
||||
private pretty: boolean;
|
||||
private process: 'sequential' | 'parallel';
|
||||
|
||||
constructor(config: PraisonAIAgentsConfig) {
|
||||
/**
|
||||
* Create a multi-agent orchestration
|
||||
* @param configOrAgents - Either an array of agents or a config object
|
||||
*/
|
||||
constructor(configOrAgents: PraisonAIAgentsConfig | Agent[]) {
|
||||
// Support array syntax: new Agents([a1, a2])
|
||||
const config: PraisonAIAgentsConfig = Array.isArray(configOrAgents)
|
||||
? { agents: configOrAgents }
|
||||
: configOrAgents;
|
||||
|
||||
this.agents = config.agents;
|
||||
this.verbose = config.verbose ?? process.env.PRAISON_VERBOSE !== 'false';
|
||||
this.pretty = config.pretty ?? process.env.PRAISON_PRETTY === 'true';
|
||||
@@ -425,3 +593,22 @@ export class PraisonAIAgents {
|
||||
return this.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Agents - Alias for PraisonAIAgents
|
||||
*
|
||||
* This is the recommended class name for multi-agent orchestration.
|
||||
* PraisonAIAgents is kept for backward compatibility.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Agent, Agents } from 'praisonai';
|
||||
*
|
||||
* const agents = new Agents([
|
||||
* new Agent({ instructions: "Research the topic" }),
|
||||
* new Agent({ instructions: "Write based on research" })
|
||||
* ]);
|
||||
* await agents.start();
|
||||
* ```
|
||||
*/
|
||||
export const Agents = PraisonAIAgents;
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Agent command - Run a single agent with instructions
|
||||
*
|
||||
* Usage:
|
||||
* praisonai-ts agent chat "Hello, how are you?"
|
||||
* praisonai-ts agent run --instructions "You are helpful" "Tell me a joke"
|
||||
*/
|
||||
|
||||
import { Agent } from '../../agent';
|
||||
import { resolveConfig } from '../config/resolve';
|
||||
import { printSuccess, printError, outputJson, formatSuccess, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
import { EXIT_CODES } from '../spec/cli-spec';
|
||||
import { ERROR_CODES, normalizeError } from '../output/errors';
|
||||
|
||||
export interface AgentOptions {
|
||||
instructions?: string;
|
||||
model?: string;
|
||||
stream?: boolean;
|
||||
verbose?: boolean;
|
||||
profile?: string;
|
||||
config?: string;
|
||||
output?: 'json' | 'text' | 'pretty';
|
||||
json?: boolean;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute agent chat subcommand
|
||||
*/
|
||||
export async function executeChat(args: string[], options: AgentOptions): Promise<void> {
|
||||
const prompt = args[0];
|
||||
|
||||
if (!prompt) {
|
||||
if (options.json || options.output === 'json') {
|
||||
printError(ERROR_CODES.MISSING_ARG, 'Please provide a prompt');
|
||||
} else {
|
||||
await pretty.error('Please provide a prompt');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
|
||||
const config = resolveConfig({
|
||||
configPath: options.config,
|
||||
profile: options.profile,
|
||||
model: options.model,
|
||||
verbose: options.verbose,
|
||||
stream: options.stream
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
|
||||
|
||||
try {
|
||||
const agent = new Agent({
|
||||
instructions: options.instructions || 'You are a helpful AI assistant.',
|
||||
llm: config.model,
|
||||
stream: config.stream && outputFormat !== 'json',
|
||||
verbose: config.verbose,
|
||||
sessionId: options.sessionId
|
||||
});
|
||||
|
||||
const response = await agent.chat(prompt);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess(
|
||||
{
|
||||
response,
|
||||
agent: agent.name,
|
||||
sessionId: agent.getSessionId()
|
||||
},
|
||||
{
|
||||
duration_ms: duration,
|
||||
model: config.model
|
||||
}
|
||||
));
|
||||
} else if (outputFormat === 'text') {
|
||||
console.log(response);
|
||||
} else {
|
||||
// Pretty output - response already printed if streaming
|
||||
if (!config.stream) {
|
||||
console.log(response);
|
||||
}
|
||||
console.log(); // Newline
|
||||
await pretty.dim(`Completed in ${duration}ms`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const cliError = normalizeError(error);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(cliError.code, cliError.message, cliError.details));
|
||||
} else {
|
||||
await pretty.error(cliError.message);
|
||||
if (config.verbose && error instanceof Error && error.stack) {
|
||||
await pretty.dim(error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(cliError.exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute agent run subcommand (same as chat but requires instructions)
|
||||
*/
|
||||
export async function executeRun(args: string[], options: AgentOptions): Promise<void> {
|
||||
if (!options.instructions) {
|
||||
if (options.json || options.output === 'json') {
|
||||
printError(ERROR_CODES.MISSING_ARG, 'Please provide --instructions');
|
||||
} else {
|
||||
await pretty.error('Please provide --instructions');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
|
||||
return executeChat(args, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execute function - routes to subcommands
|
||||
*/
|
||||
export async function execute(args: string[], options: AgentOptions): Promise<void> {
|
||||
const subcommand = args[0];
|
||||
const subArgs = args.slice(1);
|
||||
|
||||
switch (subcommand) {
|
||||
case 'chat':
|
||||
return executeChat(subArgs, options);
|
||||
case 'run':
|
||||
return executeRun(subArgs, options);
|
||||
default:
|
||||
// If no subcommand, treat the first arg as a prompt for chat
|
||||
if (subcommand) {
|
||||
return executeChat(args, options);
|
||||
}
|
||||
|
||||
if (options.json || options.output === 'json') {
|
||||
printError(ERROR_CODES.MISSING_ARG, 'Please provide a subcommand (chat, run) or a prompt');
|
||||
} else {
|
||||
await pretty.error('Usage: praisonai-ts agent [chat|run] <prompt>');
|
||||
await pretty.info(' agent chat "Hello" - Chat with default agent');
|
||||
await pretty.info(' agent run --instructions "..." "Hello" - Run with custom instructions');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* Agents command - Run multi-agent orchestration
|
||||
*
|
||||
* Usage:
|
||||
* praisonai-ts agents run --agents "researcher,writer" "Research AI trends"
|
||||
*/
|
||||
|
||||
import { Agent, Agents } from '../../agent';
|
||||
import { resolveConfig } from '../config/resolve';
|
||||
import { printSuccess, printError, outputJson, formatSuccess, formatError } from '../output/json';
|
||||
import * as pretty from '../output/pretty';
|
||||
import { EXIT_CODES } from '../spec/cli-spec';
|
||||
import { ERROR_CODES, normalizeError } from '../output/errors';
|
||||
|
||||
export interface AgentsOptions {
|
||||
agents?: string; // Comma-separated agent instructions
|
||||
process?: 'sequential' | 'parallel';
|
||||
model?: string;
|
||||
verbose?: boolean;
|
||||
profile?: string;
|
||||
config?: string;
|
||||
output?: 'json' | 'text' | 'pretty';
|
||||
json?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse agent definitions from comma-separated string
|
||||
*/
|
||||
function parseAgentDefinitions(agentsStr: string): string[] {
|
||||
return agentsStr.split(',').map(s => s.trim()).filter(s => s.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute agents run subcommand
|
||||
*/
|
||||
export async function executeRun(args: string[], options: AgentsOptions): Promise<void> {
|
||||
const prompt = args[0];
|
||||
|
||||
if (!prompt) {
|
||||
if (options.json || options.output === 'json') {
|
||||
printError(ERROR_CODES.MISSING_ARG, 'Please provide a prompt/task');
|
||||
} else {
|
||||
await pretty.error('Please provide a prompt/task');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
|
||||
const config = resolveConfig({
|
||||
configPath: options.config,
|
||||
profile: options.profile,
|
||||
model: options.model,
|
||||
verbose: options.verbose
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
const outputFormat = options.json ? 'json' : (options.output || 'pretty');
|
||||
|
||||
try {
|
||||
// Create agents from definitions or use defaults
|
||||
let agentList: Agent[];
|
||||
|
||||
if (options.agents) {
|
||||
const definitions = parseAgentDefinitions(options.agents);
|
||||
agentList = definitions.map((instructions, i) =>
|
||||
new Agent({
|
||||
instructions,
|
||||
name: `Agent_${i + 1}`,
|
||||
llm: config.model,
|
||||
verbose: config.verbose
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Default: create a researcher and writer
|
||||
agentList = [
|
||||
new Agent({
|
||||
instructions: `Research the following topic thoroughly: ${prompt}`,
|
||||
name: 'Researcher',
|
||||
llm: config.model,
|
||||
verbose: config.verbose
|
||||
}),
|
||||
new Agent({
|
||||
instructions: 'Summarize and write a clear report based on the research provided.',
|
||||
name: 'Writer',
|
||||
llm: config.model,
|
||||
verbose: config.verbose
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
if (outputFormat !== 'json') {
|
||||
await pretty.heading(`Running ${agentList.length} agents (${options.process || 'sequential'})`);
|
||||
}
|
||||
|
||||
const agents = new Agents({
|
||||
agents: agentList,
|
||||
process: options.process || 'sequential',
|
||||
verbose: config.verbose
|
||||
});
|
||||
|
||||
const results = await agents.start();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatSuccess(
|
||||
{
|
||||
results,
|
||||
agentCount: agentList.length,
|
||||
process: options.process || 'sequential'
|
||||
},
|
||||
{
|
||||
duration_ms: duration,
|
||||
model: config.model
|
||||
}
|
||||
));
|
||||
} else if (outputFormat === 'text') {
|
||||
results.forEach((result, i) => {
|
||||
console.log(`\n--- Agent ${i + 1} ---\n${result}`);
|
||||
});
|
||||
} else {
|
||||
// Pretty output
|
||||
console.log();
|
||||
await pretty.success(`Completed ${agentList.length} agents in ${duration}ms`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const cliError = normalizeError(error);
|
||||
|
||||
if (outputFormat === 'json') {
|
||||
outputJson(formatError(cliError.code, cliError.message, cliError.details));
|
||||
} else {
|
||||
await pretty.error(cliError.message);
|
||||
if (config.verbose && error instanceof Error && error.stack) {
|
||||
await pretty.dim(error.stack);
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(cliError.exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main execute function - routes to subcommands
|
||||
*/
|
||||
export async function execute(args: string[], options: AgentsOptions): Promise<void> {
|
||||
const subcommand = args[0];
|
||||
const subArgs = args.slice(1);
|
||||
|
||||
switch (subcommand) {
|
||||
case 'run':
|
||||
return executeRun(subArgs, options);
|
||||
default:
|
||||
// If no subcommand, treat as run
|
||||
if (subcommand) {
|
||||
return executeRun(args, options);
|
||||
}
|
||||
|
||||
if (options.json || options.output === 'json') {
|
||||
printError(ERROR_CODES.MISSING_ARG, 'Please provide a task/prompt');
|
||||
} else {
|
||||
await pretty.error('Usage: praisonai-ts agents [run] <task>');
|
||||
await pretty.info(' agents run "Research AI" - Run with default agents');
|
||||
await pretty.info(' agents run --agents "..." "Research AI" - Run with custom agents');
|
||||
await pretty.info(' agents run --process parallel "Research AI" - Run in parallel');
|
||||
}
|
||||
process.exit(EXIT_CODES.INVALID_ARGUMENTS);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,61 @@
|
||||
// Export all public modules
|
||||
export * from './agent';
|
||||
export * from './knowledge';
|
||||
export * from './llm';
|
||||
export * from './memory';
|
||||
export * from './process';
|
||||
/**
|
||||
* PraisonAI TypeScript SDK
|
||||
*
|
||||
* The primary API surface consists of three core classes:
|
||||
* - Agent: Single AI agent with instructions, tools, and optional persistence
|
||||
* - Agents: Multi-agent orchestration (sequential or parallel)
|
||||
* - Workflow: Step-based workflow execution
|
||||
*
|
||||
* @example Quickstart (3 lines)
|
||||
* ```typescript
|
||||
* import { Agent } from 'praisonai';
|
||||
* const agent = new Agent({ instructions: "You are helpful" });
|
||||
* await agent.chat("Hello!");
|
||||
* ```
|
||||
*
|
||||
* @example With tools (5 lines)
|
||||
* ```typescript
|
||||
* const getWeather = (city: string) => `Weather in ${city}: 20°C`;
|
||||
* const agent = new Agent({ instructions: "You provide weather", tools: [getWeather] });
|
||||
* await agent.chat("Weather in Paris?");
|
||||
* ```
|
||||
*
|
||||
* @example With persistence (4 lines)
|
||||
* ```typescript
|
||||
* import { Agent, db } from 'praisonai';
|
||||
* const agent = new Agent({ instructions: "You are helpful", db: db("sqlite:./data.db") });
|
||||
* await agent.chat("Hello!");
|
||||
* ```
|
||||
*
|
||||
* @example Multi-agent (6 lines)
|
||||
* ```typescript
|
||||
* import { Agent, Agents } from 'praisonai';
|
||||
* const researcher = new Agent({ instructions: "Research the topic" });
|
||||
* const writer = new Agent({ instructions: "Write based on research" });
|
||||
* const agents = new Agents([researcher, writer]);
|
||||
* await agents.start();
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Export tools (excluding conflicting types)
|
||||
// ============================================================================
|
||||
// CORE API - The main classes users should use
|
||||
// ============================================================================
|
||||
|
||||
// Agent - Single agent with instructions, tools, and optional persistence
|
||||
export { Agent, Agents, PraisonAIAgents } from './agent';
|
||||
export type { SimpleAgentConfig, PraisonAIAgentsConfig } from './agent';
|
||||
|
||||
// Workflow - Step-based workflow execution
|
||||
export { Workflow, parallel, route, loop, repeat } from './workflows';
|
||||
export type { WorkflowStep, WorkflowContext, StepResult } from './workflows';
|
||||
|
||||
// Database factory - Python-like db() shortcut
|
||||
export { db, createDbAdapter, getDefaultDbAdapter, setDefaultDbAdapter } from './db';
|
||||
export type { DbAdapter, DbConfig, DbMessage, DbRun, DbTrace } from './db';
|
||||
|
||||
// ============================================================================
|
||||
// TOOLS - Function tools and tool registry
|
||||
// ============================================================================
|
||||
export {
|
||||
BaseTool, ToolResult, ToolValidationError, validateTool, createTool,
|
||||
FunctionTool, tool, ToolRegistry, getRegistry, registerTool, getTool,
|
||||
@@ -14,14 +64,25 @@ export {
|
||||
export * from './tools/arxivTools';
|
||||
export * from './tools/mcpSse';
|
||||
|
||||
// Export session management
|
||||
// ============================================================================
|
||||
// SESSION & MEMORY
|
||||
// ============================================================================
|
||||
export * from './session';
|
||||
|
||||
// Export database adapters
|
||||
export * from './db';
|
||||
// ============================================================================
|
||||
// KNOWLEDGE & RAG
|
||||
// ============================================================================
|
||||
export * from './knowledge';
|
||||
|
||||
// Export workflows
|
||||
export * from './workflows';
|
||||
// ============================================================================
|
||||
// LLM PROVIDERS
|
||||
// ============================================================================
|
||||
export * from './llm';
|
||||
export * from './process';
|
||||
|
||||
// ============================================================================
|
||||
// WORKFLOWS (additional exports)
|
||||
// ============================================================================
|
||||
|
||||
// Export guardrails
|
||||
export * from './guardrails';
|
||||
@@ -35,8 +96,6 @@ export { RouterAgent, createRouter, routeConditions, type RouterConfig, type Rou
|
||||
// Export context agent
|
||||
export { ContextAgent, createContextAgent, type ContextAgentConfig, type ContextMessage } from './agent/context';
|
||||
|
||||
// Export knowledge base (RAG)
|
||||
export { KnowledgeBase, createKnowledgeBase, type Document, type SearchResult, type EmbeddingProvider, type KnowledgeBaseConfig } from './knowledge/rag';
|
||||
|
||||
// Export evaluation framework
|
||||
export { accuracyEval, performanceEval, reliabilityEval, EvalSuite, type EvalResult, type PerformanceResult, type AccuracyEvalConfig, type PerformanceEvalConfig, type ReliabilityEvalConfig } from './eval';
|
||||
@@ -50,8 +109,9 @@ export { SkillManager, createSkillManager, parseSkillFile, type Skill, type Skil
|
||||
// Export CLI
|
||||
export { parseArgs, executeCommand, CLI_SPEC_VERSION } from './cli';
|
||||
|
||||
// Export Memory
|
||||
export { Memory, createMemory, type MemoryEntry, type MemoryConfig, type SearchResult as MemorySearchResult } from './memory/memory';
|
||||
// Export Memory (explicit for convenience)
|
||||
export { Memory, createMemory } from './memory/memory';
|
||||
export type { MemoryEntry, MemoryConfig } from './memory/memory';
|
||||
|
||||
// Export FileMemory
|
||||
export { FileMemory, createFileMemory, type FileMemoryConfig, type FileMemoryEntry } from './memory/file-memory';
|
||||
@@ -82,8 +142,6 @@ export { QueryRewriterAgent, createQueryRewriterAgent, type QueryRewriterConfig,
|
||||
// Export PromptExpanderAgent
|
||||
export { PromptExpanderAgent, createPromptExpanderAgent, type PromptExpanderConfig, type ExpandResult, type ExpandStrategy } from './agent/prompt-expander';
|
||||
|
||||
// Export Chunking
|
||||
export { Chunking, createChunking, type ChunkingConfig, type Chunk, type ChunkStrategy } from './knowledge/chunking';
|
||||
|
||||
// Export LLMGuardrail
|
||||
export { LLMGuardrail, createLLMGuardrail, type LLMGuardrailConfig, type LLMGuardrailResult } from './guardrails/llm-guardrail';
|
||||
@@ -101,7 +159,7 @@ export { PubSub, EventEmitterPubSub, AgentEventBus, AgentEvents, createEventBus,
|
||||
export { parseYAMLWorkflow, createWorkflowFromYAML, loadWorkflowFromFile, validateWorkflowDefinition, type YAMLWorkflowDefinition, type YAMLStepDefinition, type ParsedWorkflow } from './workflows/yaml-parser';
|
||||
|
||||
// Export SQLite Adapter
|
||||
export { SQLiteAdapter, createSQLiteAdapter, type SQLiteConfig, type DbMessage, type DbRun, type DbTrace } from './db/sqlite';
|
||||
export { SQLiteAdapter, createSQLiteAdapter, type SQLiteConfig } from './db/sqlite';
|
||||
|
||||
// Export Redis Adapter
|
||||
export {
|
||||
|
||||
@@ -194,6 +194,40 @@ export class Workflow<TInput = any, TOutput = any> {
|
||||
return this.addStep({ name, execute });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an agent step to the workflow
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { Agent, Workflow } from 'praisonai';
|
||||
*
|
||||
* const researcher = new Agent({ instructions: "Research the topic" });
|
||||
* const writer = new Agent({ instructions: "Write based on research" });
|
||||
*
|
||||
* const workflow = new Workflow("Research Pipeline")
|
||||
* .agent(researcher, "Research AI trends")
|
||||
* .agent(writer, "Write article based on research");
|
||||
*
|
||||
* await workflow.run("AI in 2025");
|
||||
* ```
|
||||
*/
|
||||
agent(agentInstance: { chat: (prompt: string) => Promise<string>; name?: string }, task?: string): this {
|
||||
const agentName = (agentInstance as any).name || 'Agent';
|
||||
const stepName = task ? `${agentName}: ${task.slice(0, 30)}...` : agentName;
|
||||
|
||||
return this.addStep({
|
||||
name: stepName,
|
||||
execute: async (input: any, context: WorkflowContext) => {
|
||||
// Build prompt from task and previous step output
|
||||
const prompt = task
|
||||
? `${task}\n\nInput: ${typeof input === 'string' ? input : JSON.stringify(input)}`
|
||||
: typeof input === 'string' ? input : JSON.stringify(input);
|
||||
|
||||
return agentInstance.chat(prompt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the workflow
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Real API Smoke Test - Run outside Jest to avoid mocks
|
||||
*
|
||||
* Usage: npx ts-node tests/integration/real-api-smoke.ts
|
||||
*/
|
||||
|
||||
import { Agent } from '../../src';
|
||||
|
||||
async function runSmokeTests() {
|
||||
console.log('🔥 Running Real API Smoke Tests\n');
|
||||
|
||||
const OPENAI_KEY = process.env.OPENAI_API_KEY;
|
||||
|
||||
if (!OPENAI_KEY) {
|
||||
console.log('⚠️ OPENAI_API_KEY not set - skipping real API tests');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('✅ OPENAI_API_KEY detected (masked)');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
// Test 1: Simple chat
|
||||
console.log('\n📝 Test 1: Simple Agent.chat()');
|
||||
try {
|
||||
const agent = new Agent({
|
||||
instructions: 'You are a helpful assistant. Be very brief.',
|
||||
llm: 'gpt-4o-mini',
|
||||
stream: false,
|
||||
verbose: false
|
||||
});
|
||||
|
||||
const response = await agent.chat('Say "hello" and nothing else.');
|
||||
|
||||
if (response && response.toLowerCase().includes('hello')) {
|
||||
console.log(' ✅ PASSED - Response contains "hello"');
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` ❌ FAILED - Response: "${response}"`);
|
||||
failed++;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(` ❌ FAILED - Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
// Test 2: Tool calling
|
||||
console.log('\n📝 Test 2: Agent with tool calling');
|
||||
try {
|
||||
const getWeather = (city: string) => `Weather in ${city}: 22°C, Sunny`;
|
||||
|
||||
const agent = new Agent({
|
||||
instructions: 'You are a weather assistant. Use the getWeather tool to answer weather questions.',
|
||||
llm: 'gpt-4o-mini',
|
||||
stream: false,
|
||||
verbose: false,
|
||||
tools: [getWeather]
|
||||
});
|
||||
|
||||
const response = await agent.chat('What is the weather in Paris?');
|
||||
|
||||
if (response && /paris|22|sunny|weather/i.test(response)) {
|
||||
console.log(' ✅ PASSED - Response mentions weather data');
|
||||
passed++;
|
||||
} else {
|
||||
console.log(` ❌ FAILED - Response: "${response}"`);
|
||||
failed++;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(` ❌ FAILED - Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
// Test 3: Session ID
|
||||
console.log('\n📝 Test 3: Session ID generation');
|
||||
try {
|
||||
const agent1 = new Agent({ instructions: 'Test' });
|
||||
const agent2 = new Agent({ instructions: 'Test' });
|
||||
|
||||
if (agent1.getSessionId() !== agent2.getSessionId()) {
|
||||
console.log(' ✅ PASSED - Unique session IDs generated');
|
||||
passed++;
|
||||
} else {
|
||||
console.log(' ❌ FAILED - Session IDs are not unique');
|
||||
failed++;
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.log(` ❌ FAILED - Error: ${error.message}`);
|
||||
failed++;
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log(`📊 Results: ${passed} passed, ${failed} failed`);
|
||||
console.log('='.repeat(50));
|
||||
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
runSmokeTests().catch(console.error);
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Smoke Tests - Real API key validation (gated)
|
||||
*
|
||||
* These tests only run when API keys are available in environment
|
||||
* AND when not running in mock mode.
|
||||
* They validate real provider integration without exposing secrets.
|
||||
*
|
||||
* To run with real API:
|
||||
* OPENAI_API_KEY=sk-... npm test -- --testPathPattern=smoke --no-coverage
|
||||
*/
|
||||
|
||||
import { Agent } from '../../src';
|
||||
|
||||
const OPENAI_KEY = process.env.OPENAI_API_KEY;
|
||||
const IS_MOCK_MODE = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
|
||||
const SKIP_REASON = IS_MOCK_MODE
|
||||
? 'Skipping: Running in mock mode (use real-smoke.test.ts for real API tests)'
|
||||
: 'Skipping: OPENAI_API_KEY not set';
|
||||
const SHOULD_RUN_REAL_API = OPENAI_KEY && !IS_MOCK_MODE;
|
||||
|
||||
describe('Real Provider Smoke Tests', () => {
|
||||
describe('Agent.chat with OpenAI', () => {
|
||||
(SHOULD_RUN_REAL_API ? it : it.skip)('should complete a simple chat', async () => {
|
||||
const agent = new Agent({
|
||||
instructions: 'You are a helpful assistant. Be very brief.',
|
||||
llm: 'gpt-4o-mini',
|
||||
stream: false,
|
||||
verbose: false
|
||||
});
|
||||
|
||||
const response = await agent.chat('Say "hello" and nothing else.');
|
||||
|
||||
expect(response).toBeDefined();
|
||||
expect(typeof response).toBe('string');
|
||||
expect(response.length).toBeGreaterThan(0);
|
||||
// Response should contain "hello" in some form
|
||||
expect(response.toLowerCase()).toContain('hello');
|
||||
}, 30000);
|
||||
|
||||
(SHOULD_RUN_REAL_API ? it : it.skip)('should handle tool calling', async () => {
|
||||
const getWeather = (city: string) => `Weather in ${city}: 22°C, Sunny`;
|
||||
|
||||
const agent = new Agent({
|
||||
instructions: 'You are a weather assistant. Use the getWeather tool to answer.',
|
||||
llm: 'gpt-4o-mini',
|
||||
stream: false,
|
||||
verbose: false,
|
||||
tools: [getWeather]
|
||||
});
|
||||
|
||||
const response = await agent.chat('What is the weather in Paris?');
|
||||
|
||||
expect(response).toBeDefined();
|
||||
expect(typeof response).toBe('string');
|
||||
// Response should mention the weather data
|
||||
expect(response.toLowerCase()).toMatch(/paris|22|sunny|weather/i);
|
||||
}, 60000);
|
||||
});
|
||||
|
||||
describe('Session persistence', () => {
|
||||
it('should generate unique session IDs', () => {
|
||||
const agent1 = new Agent({ instructions: 'Test' });
|
||||
const agent2 = new Agent({ instructions: 'Test' });
|
||||
|
||||
expect(agent1.getSessionId()).not.toBe(agent2.getSessionId());
|
||||
});
|
||||
|
||||
it('should use provided session ID', () => {
|
||||
const agent = new Agent({
|
||||
instructions: 'Test',
|
||||
sessionId: 'test-session-123'
|
||||
});
|
||||
|
||||
expect(agent.getSessionId()).toBe('test-session-123');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Log skip reason if no API key
|
||||
if (!OPENAI_KEY) {
|
||||
console.log(`\n⚠️ ${SKIP_REASON}\n`);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Agent API Tests - Unified Agent, Agents, db(), tools
|
||||
*/
|
||||
|
||||
import { Agent, Agents, PraisonAIAgents, db, Workflow } from '../../src';
|
||||
|
||||
describe('Agent API', () => {
|
||||
describe('Agent constructor', () => {
|
||||
it('should create agent with instructions', () => {
|
||||
const agent = new Agent({ instructions: 'You are helpful' });
|
||||
expect(agent).toBeDefined();
|
||||
expect(agent.getInstructions()).toBe('You are helpful');
|
||||
});
|
||||
|
||||
it('should auto-generate name if not provided', () => {
|
||||
const agent = new Agent({ instructions: 'Test' });
|
||||
expect(agent.name).toMatch(/^Agent_/);
|
||||
});
|
||||
|
||||
it('should use provided name', () => {
|
||||
const agent = new Agent({ instructions: 'Test', name: 'MyAgent' });
|
||||
expect(agent.name).toBe('MyAgent');
|
||||
});
|
||||
|
||||
it('should support role/goal/backstory mode', () => {
|
||||
const agent = new Agent({
|
||||
instructions: '',
|
||||
role: 'researcher',
|
||||
goal: 'find information',
|
||||
backstory: 'expert in research'
|
||||
});
|
||||
const instructions = agent.getInstructions();
|
||||
expect(instructions).toContain('researcher');
|
||||
expect(instructions).toContain('find information');
|
||||
expect(instructions).toContain('expert in research');
|
||||
});
|
||||
|
||||
it('should have sessionId', () => {
|
||||
const agent = new Agent({ instructions: 'Test' });
|
||||
expect(agent.getSessionId()).toBeDefined();
|
||||
expect(typeof agent.getSessionId()).toBe('string');
|
||||
});
|
||||
|
||||
it('should use provided sessionId', () => {
|
||||
const agent = new Agent({ instructions: 'Test', sessionId: 'my-session' });
|
||||
expect(agent.getSessionId()).toBe('my-session');
|
||||
});
|
||||
|
||||
it('should have runId', () => {
|
||||
const agent = new Agent({ instructions: 'Test' });
|
||||
expect(agent.getRunId()).toBeDefined();
|
||||
expect(typeof agent.getRunId()).toBe('string');
|
||||
});
|
||||
|
||||
it('should accept db adapter', () => {
|
||||
const dbAdapter = db('memory:');
|
||||
const agent = new Agent({
|
||||
instructions: 'Test',
|
||||
db: dbAdapter
|
||||
});
|
||||
expect(agent).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Agents alias', () => {
|
||||
it('should export Agents as alias for PraisonAIAgents', () => {
|
||||
expect(Agents).toBe(PraisonAIAgents);
|
||||
});
|
||||
|
||||
it('should support array syntax', () => {
|
||||
const a1 = new Agent({ instructions: 'Agent 1' });
|
||||
const a2 = new Agent({ instructions: 'Agent 2' });
|
||||
const agents = new Agents([a1, a2]);
|
||||
expect(agents).toBeDefined();
|
||||
});
|
||||
|
||||
it('should support config object syntax', () => {
|
||||
const a1 = new Agent({ instructions: 'Agent 1' });
|
||||
const a2 = new Agent({ instructions: 'Agent 2' });
|
||||
const agents = new Agents({
|
||||
agents: [a1, a2],
|
||||
process: 'sequential'
|
||||
});
|
||||
expect(agents).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('db() factory', () => {
|
||||
it('should create memory adapter by default', () => {
|
||||
const adapter = db();
|
||||
expect(adapter).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse memory: URL', () => {
|
||||
const adapter = db('memory:');
|
||||
expect(adapter).toBeDefined();
|
||||
});
|
||||
|
||||
it('should parse :memory: URL', () => {
|
||||
const adapter = db(':memory:');
|
||||
expect(adapter).toBeDefined();
|
||||
});
|
||||
|
||||
it('should accept config object', () => {
|
||||
const adapter = db({ type: 'memory' });
|
||||
expect(adapter).toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw on invalid URL', () => {
|
||||
expect(() => db('invalid://url')).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tool auto-schema', () => {
|
||||
it('should accept plain functions as tools', () => {
|
||||
const getWeather = (city: string) => `Weather in ${city}: 20°C`;
|
||||
|
||||
const agent = new Agent({
|
||||
instructions: 'Weather assistant',
|
||||
tools: [getWeather]
|
||||
});
|
||||
|
||||
expect(agent).toBeDefined();
|
||||
});
|
||||
|
||||
it('should accept multiple function tools', () => {
|
||||
const getWeather = (city: string) => `Weather: 20°C`;
|
||||
const getTime = () => new Date().toISOString();
|
||||
|
||||
const agent = new Agent({
|
||||
instructions: 'Assistant',
|
||||
tools: [getWeather, getTime]
|
||||
});
|
||||
|
||||
expect(agent).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Workflow.agent()', () => {
|
||||
it('should add agent step to workflow', () => {
|
||||
const agent = new Agent({ instructions: 'Test' });
|
||||
const workflow = new Workflow('Test')
|
||||
.agent(agent, 'Do something');
|
||||
|
||||
expect(workflow.stepCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should chain multiple agent steps', () => {
|
||||
const a1 = new Agent({ instructions: 'Agent 1' });
|
||||
const a2 = new Agent({ instructions: 'Agent 2' });
|
||||
|
||||
const workflow = new Workflow('Test')
|
||||
.agent(a1, 'Step 1')
|
||||
.agent(a2, 'Step 2');
|
||||
|
||||
expect(workflow.stepCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should mix agent and function steps', () => {
|
||||
const agent = new Agent({ instructions: 'Test' });
|
||||
|
||||
const workflow = new Workflow('Test')
|
||||
.step('preprocess', async (input) => input.toUpperCase())
|
||||
.agent(agent, 'Process')
|
||||
.step('postprocess', async (input) => input.trim());
|
||||
|
||||
expect(workflow.stepCount).toBe(3);
|
||||
});
|
||||
});
|
||||
+12
-8
@@ -1994,19 +1994,23 @@ manager.delete_checkpoint("deploy-v1")
|
||||
|
||||
### Tools CLI:
|
||||
```bash
|
||||
# List all available tools
|
||||
praisonai tools list
|
||||
|
||||
# Get info about a specific tool
|
||||
praisonai tools info internet_search
|
||||
|
||||
# Search for tools
|
||||
praisonai tools search "web"
|
||||
|
||||
# Show all commands
|
||||
praisonai tools help
|
||||
praisonai tools doctor
|
||||
praisonai tools resolve shell_tool
|
||||
praisonai tools discover
|
||||
praisonai tools show-sources
|
||||
praisonai tools show-sources --template ai-video-editor
|
||||
```
|
||||
|
||||
| Command | Example | Docs |
|
||||
|---------|---------|------|
|
||||
| `tools list` | [example](examples/tools/) | [docs](https://docs.praison.ai/docs/cli/tools) |
|
||||
| `tools resolve` | [example](examples/tools/example_tools_resolve.py) | [docs](https://docs.praison.ai/docs/cli/tools-resolve) |
|
||||
| `tools discover` | [example](examples/tools/example_tools_discover.py) | [docs](https://docs.praison.ai/docs/cli/tools-discover) |
|
||||
| `tools show-sources` | [example](examples/tools/example_tools_sources.py) | [docs](https://docs.praison.ai/docs/cli/tools-show-sources) |
|
||||
|
||||
### Handoff CLI:
|
||||
```bash
|
||||
# Enable agent-to-agent task delegation
|
||||
|
||||
Whitespace-only changes.
@@ -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.3.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.6.3.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -35,6 +35,7 @@ Features:
|
||||
- thinking: Thinking budget management
|
||||
- compaction: Context compaction settings
|
||||
- output_style: Output style configuration
|
||||
- ollama: Ollama provider and Weak-Model-Proof execution
|
||||
"""
|
||||
|
||||
# Type hints available for IDE support
|
||||
@@ -87,6 +88,8 @@ __all__ = [
|
||||
'ThinkingHandler',
|
||||
'CompactionHandler',
|
||||
'OutputStyleHandler',
|
||||
# Ollama + WMP
|
||||
'OllamaHandler',
|
||||
]
|
||||
|
||||
def __getattr__(name):
|
||||
@@ -204,4 +207,7 @@ def __getattr__(name):
|
||||
elif name == 'OutputStyleHandler':
|
||||
from .output_style import OutputStyleHandler
|
||||
return OutputStyleHandler
|
||||
elif name == 'OllamaHandler':
|
||||
from .ollama import OllamaHandler
|
||||
return OllamaHandler
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
@@ -0,0 +1,361 @@
|
||||
"""
|
||||
Ollama CLI Handler
|
||||
|
||||
CLI support for Ollama provider and Weak-Model-Proof execution.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OllamaHandler:
|
||||
"""
|
||||
Handler for Ollama CLI commands.
|
||||
|
||||
Supports:
|
||||
- --provider ollama
|
||||
- --weak-model-proof / --wmp
|
||||
- --ollama-host
|
||||
- --ollama-model
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str = "llama3.2:3b",
|
||||
host: Optional[str] = None,
|
||||
weak_model_proof: bool = True,
|
||||
verbose: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Initialize Ollama handler.
|
||||
|
||||
Args:
|
||||
model: Ollama model name
|
||||
host: Ollama server host
|
||||
weak_model_proof: Enable WMP
|
||||
verbose: Verbose output
|
||||
**kwargs: Additional options
|
||||
"""
|
||||
self.model = model
|
||||
self.host = host or os.environ.get("OLLAMA_HOST", "http://localhost:11434")
|
||||
self.weak_model_proof = weak_model_proof
|
||||
self.verbose = verbose
|
||||
self.extra_kwargs = kwargs
|
||||
|
||||
self._provider = None
|
||||
self._wmp_hooks = None
|
||||
|
||||
@property
|
||||
def provider(self):
|
||||
"""Lazy-load Ollama provider."""
|
||||
if self._provider is None:
|
||||
try:
|
||||
from praisonai.providers import OllamaProvider
|
||||
self._provider = OllamaProvider(
|
||||
model=self.model,
|
||||
host=self.host,
|
||||
weak_model_proof=self.weak_model_proof,
|
||||
**self.extra_kwargs
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import OllamaProvider: {e}")
|
||||
raise
|
||||
return self._provider
|
||||
|
||||
@property
|
||||
def wmp_hooks(self):
|
||||
"""Lazy-load WMP hooks."""
|
||||
if self._wmp_hooks is None and self.weak_model_proof:
|
||||
try:
|
||||
from praisonai.wmp import WMPHooks, WeakModelProofConfig
|
||||
|
||||
# Auto-configure based on model
|
||||
config = WeakModelProofConfig.for_ollama(
|
||||
self._detect_model_size()
|
||||
)
|
||||
|
||||
self._wmp_hooks = WMPHooks(
|
||||
config=config,
|
||||
llm_fn=self._llm_fn,
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.warning(f"WMP not available: {e}")
|
||||
self._wmp_hooks = None
|
||||
return self._wmp_hooks
|
||||
|
||||
def _detect_model_size(self) -> str:
|
||||
"""Detect model size from name."""
|
||||
model_lower = self.model.lower()
|
||||
|
||||
if any(x in model_lower for x in ["70b", "72b", "65b"]):
|
||||
return "large"
|
||||
elif any(x in model_lower for x in ["13b", "14b", "7b", "8b"]):
|
||||
return "medium"
|
||||
else:
|
||||
return "small"
|
||||
|
||||
def _llm_fn(self, prompt: str) -> str:
|
||||
"""LLM function for WMP."""
|
||||
response = self.provider.chat(prompt)
|
||||
return response.content
|
||||
|
||||
def chat(
|
||||
self,
|
||||
message: str,
|
||||
system: Optional[str] = None,
|
||||
stream: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
Send a chat message.
|
||||
|
||||
Args:
|
||||
message: User message
|
||||
system: System prompt
|
||||
stream: Enable streaming
|
||||
|
||||
Returns:
|
||||
Response text
|
||||
"""
|
||||
if self.weak_model_proof and self.wmp_hooks:
|
||||
# Use WMP execution
|
||||
self.wmp_hooks.on_task_start(message)
|
||||
|
||||
messages = [{"role": "user", "content": message}]
|
||||
if system:
|
||||
messages.insert(0, {"role": "system", "content": system})
|
||||
|
||||
enhanced_messages = self.wmp_hooks.on_before_llm_call(messages)
|
||||
|
||||
# Call Ollama
|
||||
response = self.provider.chat(
|
||||
messages=enhanced_messages,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
if stream:
|
||||
# Collect streamed response
|
||||
full_response = ""
|
||||
for chunk in response:
|
||||
full_response += chunk
|
||||
if self.verbose:
|
||||
print(chunk, end="", flush=True)
|
||||
if self.verbose:
|
||||
print()
|
||||
response_text = full_response
|
||||
else:
|
||||
response_text = response.content
|
||||
|
||||
# Post-process with WMP
|
||||
response_text = self.wmp_hooks.on_after_llm_call(response_text)
|
||||
|
||||
return response_text
|
||||
else:
|
||||
# Direct Ollama call
|
||||
response = self.provider.chat(
|
||||
messages=message,
|
||||
system=system,
|
||||
stream=stream,
|
||||
)
|
||||
|
||||
if stream:
|
||||
full_response = ""
|
||||
for chunk in response:
|
||||
full_response += chunk
|
||||
if self.verbose:
|
||||
print(chunk, end="", flush=True)
|
||||
if self.verbose:
|
||||
print()
|
||||
return full_response
|
||||
else:
|
||||
return response.content
|
||||
|
||||
def execute(
|
||||
self,
|
||||
task: str,
|
||||
tools: Optional[List] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute a task with WMP.
|
||||
|
||||
Args:
|
||||
task: Task description
|
||||
tools: Available tools
|
||||
|
||||
Returns:
|
||||
Execution result
|
||||
"""
|
||||
if self.weak_model_proof:
|
||||
try:
|
||||
from praisonai.wmp import WMPExecutor, WeakModelProofConfig
|
||||
|
||||
config = WeakModelProofConfig.for_ollama(
|
||||
self._detect_model_size()
|
||||
)
|
||||
|
||||
executor = WMPExecutor(config=config)
|
||||
result = executor.execute(
|
||||
task=task,
|
||||
llm_fn=self._llm_fn,
|
||||
tools=tools,
|
||||
)
|
||||
|
||||
return result.to_dict()
|
||||
|
||||
except ImportError:
|
||||
logger.warning("WMP not available, falling back to direct execution")
|
||||
|
||||
# Direct execution
|
||||
response = self.chat(task)
|
||||
return {
|
||||
"success": True,
|
||||
"result": response,
|
||||
"wmp_enabled": False,
|
||||
}
|
||||
|
||||
def list_models(self) -> List[Dict[str, Any]]:
|
||||
"""List available Ollama models."""
|
||||
return self.provider.list_models()
|
||||
|
||||
def pull_model(self, model: str) -> None:
|
||||
"""Pull a model from Ollama registry."""
|
||||
if self.verbose:
|
||||
print(f"Pulling model: {model}")
|
||||
|
||||
for progress in self.provider.pull_model(model, stream=True):
|
||||
if self.verbose and hasattr(progress, 'status'):
|
||||
print(f" {progress.status}", end="\r")
|
||||
|
||||
if self.verbose:
|
||||
print(f"\nModel {model} pulled successfully")
|
||||
|
||||
def is_available(self) -> bool:
|
||||
"""Check if Ollama is available."""
|
||||
return self.provider.is_available()
|
||||
|
||||
|
||||
def handle_ollama_command(args, unknown_args: List[str] = None) -> int:
|
||||
"""
|
||||
Handle Ollama CLI commands.
|
||||
|
||||
Args:
|
||||
args: Parsed arguments
|
||||
unknown_args: Unknown arguments
|
||||
|
||||
Returns:
|
||||
Exit code
|
||||
"""
|
||||
from rich.console import Console
|
||||
console = Console()
|
||||
|
||||
# Get options from args
|
||||
model = getattr(args, 'ollama_model', None) or getattr(args, 'model', 'llama3.2:3b')
|
||||
host = getattr(args, 'ollama_host', None)
|
||||
wmp = getattr(args, 'weak_model_proof', True)
|
||||
verbose = getattr(args, 'verbose', False)
|
||||
|
||||
try:
|
||||
handler = OllamaHandler(
|
||||
model=model,
|
||||
host=host,
|
||||
weak_model_proof=wmp,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
# Check availability
|
||||
if not handler.is_available():
|
||||
console.print("[red]Error: Ollama server not available[/red]")
|
||||
console.print(f"Make sure Ollama is running at {handler.host}")
|
||||
return 1
|
||||
|
||||
# Get command/prompt
|
||||
prompt = getattr(args, 'command', None) or getattr(args, 'direct_prompt', None)
|
||||
|
||||
if not prompt:
|
||||
console.print("[yellow]No prompt provided[/yellow]")
|
||||
return 1
|
||||
|
||||
# Execute
|
||||
if wmp:
|
||||
console.print(f"[dim]Using Ollama with Weak-Model-Proof ({model})[/dim]")
|
||||
else:
|
||||
console.print(f"[dim]Using Ollama ({model})[/dim]")
|
||||
|
||||
result = handler.execute(prompt)
|
||||
|
||||
if result.get("success"):
|
||||
console.print(result.get("result", ""))
|
||||
|
||||
if verbose and result.get("wmp_enabled"):
|
||||
console.print(f"\n[dim]Steps: {result.get('steps_executed', 0)}, "
|
||||
f"Retries: {result.get('retries', 0)}[/dim]")
|
||||
else:
|
||||
console.print(f"[red]Error: {result.get('error', 'Unknown error')}[/red]")
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error: {e}[/red]")
|
||||
if verbose:
|
||||
import traceback
|
||||
console.print(traceback.format_exc())
|
||||
return 1
|
||||
|
||||
|
||||
def add_ollama_arguments(parser) -> None:
|
||||
"""
|
||||
Add Ollama-related arguments to parser.
|
||||
|
||||
Args:
|
||||
parser: ArgumentParser instance
|
||||
"""
|
||||
parser.add_argument(
|
||||
"--provider",
|
||||
type=str,
|
||||
choices=["ollama", "litellm", "openai"],
|
||||
help="LLM provider to use"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ollama-model",
|
||||
type=str,
|
||||
default="llama3.2:3b",
|
||||
help="Ollama model name (e.g., llama3.2:3b, mistral, qwen2.5:7b)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ollama-host",
|
||||
type=str,
|
||||
help="Ollama server host (default: http://localhost:11434)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--weak-model-proof", "--wmp",
|
||||
action="store_true",
|
||||
default=None,
|
||||
dest="weak_model_proof",
|
||||
help="Enable Weak-Model-Proof execution (default: auto for Ollama)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-wmp",
|
||||
action="store_false",
|
||||
dest="weak_model_proof",
|
||||
help="Disable Weak-Model-Proof execution"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--wmp-strict",
|
||||
action="store_true",
|
||||
help="Use strict WMP mode (more retries, stricter validation)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--wmp-fast",
|
||||
action="store_true",
|
||||
help="Use fast WMP mode (fewer retries, faster execution)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--step-budget",
|
||||
type=int,
|
||||
default=10,
|
||||
help="Maximum steps for WMP execution (default: 10)"
|
||||
)
|
||||
@@ -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,104 @@ 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
|
||||
no_template_tools_py = "--no-template-tools-py" in args
|
||||
|
||||
# Parse --tools, --tools-dir, --tools-source overrides
|
||||
tools_files = []
|
||||
tools_dirs = []
|
||||
tools_sources_override = []
|
||||
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
|
||||
elif args[i] == "--tools-source" and i + 1 < len(args):
|
||||
tools_sources_override.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
|
||||
from praisonai.templates.tool_override import create_tool_registry_with_overrides, resolve_tools
|
||||
|
||||
# Get template directory for local tools.py autoload (unless disabled)
|
||||
template_dir = None
|
||||
if not no_template_tools_py:
|
||||
template_dir = str(template.path) if template.path else None
|
||||
|
||||
# Get tools_sources from template requires + CLI overrides
|
||||
tools_sources = []
|
||||
if template.requires and isinstance(template.requires, dict):
|
||||
ts = template.requires.get("tools_sources", [])
|
||||
if ts:
|
||||
tools_sources.extend(ts)
|
||||
# Add CLI --tools-source overrides
|
||||
if tools_sources_override:
|
||||
tools_sources.extend(tools_sources_override)
|
||||
|
||||
# Always build registry (includes defaults + template sources)
|
||||
tool_registry = create_tool_registry_with_overrides(
|
||||
override_files=tools_files if tools_files else None,
|
||||
override_dirs=tools_dirs if tools_dirs else None,
|
||||
include_defaults=True,
|
||||
tools_sources=tools_sources if tools_sources else None,
|
||||
template_dir=template_dir,
|
||||
)
|
||||
|
||||
if tools_files or tools_dirs:
|
||||
print(f"✓ Loaded {len(tool_registry)} tools from overrides")
|
||||
|
||||
# Load and run workflow
|
||||
workflow_config = loader.load_workflow_config(template)
|
||||
@@ -377,10 +574,81 @@ 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:
|
||||
# Resolve tool names to callable tools
|
||||
agent_tool_names = agent_cfg.get("tools", [])
|
||||
resolved_agent_tools = resolve_tools(
|
||||
agent_tool_names,
|
||||
registry=tool_registry,
|
||||
template_dir=template_dir
|
||||
)
|
||||
|
||||
agent = Agent(
|
||||
name=agent_cfg.get("name", "Agent"),
|
||||
role=agent_cfg.get("role", ""),
|
||||
goal=agent_cfg.get("goal", ""),
|
||||
backstory=agent_cfg.get("backstory", ""),
|
||||
tools=resolved_agent_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) - resolve tools in each step's agent
|
||||
from praisonaiagents import Workflow
|
||||
|
||||
# Resolve tools for each step's agent if specified
|
||||
steps = workflow_config.get("steps", [])
|
||||
for step in steps:
|
||||
if "agent" in step and isinstance(step["agent"], dict):
|
||||
agent_cfg = step["agent"]
|
||||
if "tools" in agent_cfg:
|
||||
agent_cfg["tools"] = resolve_tools(
|
||||
agent_cfg["tools"],
|
||||
registry=tool_registry,
|
||||
template_dir=template_dir
|
||||
)
|
||||
|
||||
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", "resolve", "discover", "show-sources", "help"]
|
||||
|
||||
def get_help_text(self) -> str:
|
||||
return """
|
||||
@@ -39,6 +39,11 @@ 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
|
||||
praisonai tools resolve <name> - Resolve a tool name to its source
|
||||
praisonai tools discover - Discover tools from installed packages
|
||||
praisonai tools show-sources - Show all tool sources for a template
|
||||
|
||||
Built-in tools include: internet_search, calculator, file operations, etc.
|
||||
"""
|
||||
@@ -213,6 +218,232 @@ 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 action_resolve(self, args: List[str], **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Resolve a tool name to its source.
|
||||
|
||||
Args:
|
||||
args: [tool_name, --template <template>, --tools <file>, --tools-dir <dir>]
|
||||
"""
|
||||
if not args:
|
||||
self.print_status("Usage: praisonai tools resolve <tool_name> [--template <template>]", "error")
|
||||
return {}
|
||||
|
||||
tool_name = args[0]
|
||||
template_name = None
|
||||
tools_files = []
|
||||
tools_dirs = []
|
||||
|
||||
# Parse flags
|
||||
i = 1
|
||||
while i < len(args):
|
||||
if args[i] == "--template" and i + 1 < len(args):
|
||||
template_name = args[i + 1]
|
||||
i += 2
|
||||
elif 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.tool_override import create_tool_registry_with_overrides, resolve_tools
|
||||
|
||||
registry = create_tool_registry_with_overrides(
|
||||
override_files=tools_files if tools_files else None,
|
||||
override_dirs=tools_dirs if tools_dirs else None,
|
||||
include_defaults=True,
|
||||
)
|
||||
|
||||
resolved = resolve_tools([tool_name], registry=registry)
|
||||
|
||||
if resolved:
|
||||
tool = resolved[0]
|
||||
info = {
|
||||
"name": tool_name,
|
||||
"resolved": True,
|
||||
"type": type(tool).__name__,
|
||||
"module": getattr(tool, "__module__", "unknown"),
|
||||
}
|
||||
self.print_status(f"\n✓ Tool '{tool_name}' resolved:", "success")
|
||||
for k, v in info.items():
|
||||
self.print_status(f" {k}: {v}", "info")
|
||||
return info
|
||||
else:
|
||||
self.print_status(f"✗ Tool '{tool_name}' not found", "error")
|
||||
return {"name": tool_name, "resolved": False}
|
||||
|
||||
except Exception as e:
|
||||
self.print_status(f"Error resolving tool: {e}", "error")
|
||||
return {}
|
||||
|
||||
def action_discover(self, args: List[str], **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Discover tools from installed packages.
|
||||
|
||||
Args:
|
||||
args: [--include <package>, --entrypoints]
|
||||
"""
|
||||
include_packages = []
|
||||
use_entrypoints = "--entrypoints" in args
|
||||
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--include" and i + 1 < len(args):
|
||||
include_packages.append(args[i + 1])
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
discovered = {}
|
||||
|
||||
# Try praisonai_tools package
|
||||
try:
|
||||
import praisonai_tools
|
||||
pkg_tools = []
|
||||
|
||||
# Check for video module
|
||||
try:
|
||||
from praisonai_tools import video
|
||||
pkg_tools.append("praisonai_tools.video")
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Check for tools module
|
||||
try:
|
||||
import praisonai_tools.tools as ext_tools
|
||||
for name in dir(ext_tools):
|
||||
if not name.startswith('_'):
|
||||
obj = getattr(ext_tools, name, None)
|
||||
if callable(obj):
|
||||
pkg_tools.append(name)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
if pkg_tools:
|
||||
discovered["praisonai_tools"] = pkg_tools
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Try praisonaiagents built-in tools
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
discovered["praisonaiagents.tools"] = list(TOOL_MAPPINGS.keys())[:20] # Limit
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Additional packages from --include
|
||||
for pkg in include_packages:
|
||||
try:
|
||||
import importlib
|
||||
mod = importlib.import_module(pkg)
|
||||
tools = [n for n in dir(mod) if not n.startswith('_') and callable(getattr(mod, n, None))]
|
||||
if tools:
|
||||
discovered[pkg] = tools[:20]
|
||||
except ImportError:
|
||||
self.print_status(f"Package '{pkg}' not found", "warning")
|
||||
|
||||
self.print_status("\n🔍 Discovered Tools:", "info")
|
||||
for pkg, tools in discovered.items():
|
||||
self.print_status(f"\n {pkg}:", "info")
|
||||
for t in tools[:10]:
|
||||
self.print_status(f" • {t}", "info")
|
||||
if len(tools) > 10:
|
||||
self.print_status(f" ... and {len(tools) - 10} more", "info")
|
||||
|
||||
return discovered
|
||||
|
||||
def action_show_sources(self, args: List[str], **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
Show all tool sources for a template.
|
||||
|
||||
Args:
|
||||
args: [--template <template>]
|
||||
"""
|
||||
template_name = None
|
||||
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--template" and i + 1 < len(args):
|
||||
template_name = args[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
i += 1
|
||||
|
||||
sources = {
|
||||
"built_in": "praisonaiagents.tools.TOOL_MAPPINGS",
|
||||
"package_discovery": ["praisonai_tools"],
|
||||
"default_dirs": [
|
||||
"~/.praison/tools",
|
||||
"~/.config/praison/tools",
|
||||
],
|
||||
}
|
||||
|
||||
if template_name:
|
||||
try:
|
||||
from praisonai.templates.loader import TemplateLoader
|
||||
loader = TemplateLoader()
|
||||
template = loader.load_template(template_name)
|
||||
|
||||
if template.requires and isinstance(template.requires, dict):
|
||||
ts = template.requires.get("tools_sources", [])
|
||||
if ts:
|
||||
sources["template_tools_sources"] = ts
|
||||
|
||||
if template.path:
|
||||
from pathlib import Path
|
||||
tools_py = Path(template.path) / "tools.py"
|
||||
if tools_py.exists():
|
||||
sources["template_local_tools_py"] = str(tools_py)
|
||||
|
||||
except Exception as e:
|
||||
self.print_status(f"Could not load template: {e}", "warning")
|
||||
|
||||
self.print_status("\n📦 Tool Sources:", "info")
|
||||
for source_type, value in sources.items():
|
||||
if isinstance(value, list):
|
||||
self.print_status(f"\n {source_type}:", "info")
|
||||
for v in value:
|
||||
self.print_status(f" • {v}", "info")
|
||||
else:
|
||||
self.print_status(f"\n {source_type}: {value}", "info")
|
||||
|
||||
return sources
|
||||
|
||||
def execute(self, action: str, action_args: List[str], **kwargs) -> Any:
|
||||
"""Execute tools command action."""
|
||||
# Handle hyphenated action names
|
||||
if action == "show-sources":
|
||||
return self.action_show_sources(action_args, **kwargs)
|
||||
return super().execute(action, action_args, **kwargs)
|
||||
@@ -312,7 +312,8 @@ class PraisonAI:
|
||||
if combined_inputs:
|
||||
combined_prompt = f"{args.command} {' '.join(combined_inputs)}"
|
||||
result = self.handle_direct_prompt(combined_prompt)
|
||||
print(result)
|
||||
if result is not None:
|
||||
print(result)
|
||||
return result
|
||||
else:
|
||||
self.agent_file = args.command
|
||||
@@ -339,7 +340,8 @@ class PraisonAI:
|
||||
prompt_parts.append(file_input)
|
||||
prompt = ' '.join(prompt_parts)
|
||||
result = self.handle_direct_prompt(prompt)
|
||||
print(result)
|
||||
if result is not None:
|
||||
print(result)
|
||||
return result
|
||||
else:
|
||||
# Agent file was explicitly set, ignore direct prompt and use the file
|
||||
@@ -355,7 +357,8 @@ class PraisonAI:
|
||||
inputs.append(file_input)
|
||||
combined_input = ' '.join(inputs)
|
||||
result = self.handle_direct_prompt(combined_input)
|
||||
print(result)
|
||||
if result is not None:
|
||||
print(result)
|
||||
return result
|
||||
# If no command or direct_prompt, preserve agent_file from constructor (don't overwrite)
|
||||
|
||||
@@ -678,7 +681,7 @@ class PraisonAI:
|
||||
return default_args
|
||||
|
||||
# Define special commands
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run', 'thinking', 'compaction', 'output', 'deploy']
|
||||
special_commands = ['chat', 'code', 'call', 'realtime', 'train', 'ui', 'context', 'research', 'memory', 'rules', 'workflow', 'hooks', 'knowledge', 'session', 'tools', 'todo', 'docs', 'mcp', 'commit', 'serve', 'schedule', 'skills', 'profile', 'eval', 'agents', 'run', 'thinking', 'compaction', 'output', 'deploy', 'templates']
|
||||
|
||||
parser = argparse.ArgumentParser(prog="praisonai", description="praisonAI command-line interface")
|
||||
parser.add_argument("--framework", choices=["crewai", "autogen", "praisonai"], help="Specify the framework")
|
||||
@@ -711,6 +714,7 @@ class PraisonAI:
|
||||
parser.add_argument("--expand-prompt", action="store_true", help="Expand short prompt into detailed prompt (works with any command)")
|
||||
parser.add_argument("--expand-tools", type=str, help="Tools for prompt expander (e.g., 'internet_search' or path to tools.py)")
|
||||
parser.add_argument("--tools", "-t", type=str, help="Path to tools.py file for research agent")
|
||||
parser.add_argument("--no-tools", action="store_true", help="Disable default built-in tools (for models that don't support tool calling)")
|
||||
parser.add_argument("--save", "-s", action="store_true", help="Save research output to file (output/research/)")
|
||||
parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output for research")
|
||||
parser.add_argument("--web-search", action="store_true", help="Enable native web search (OpenAI, Gemini, Anthropic, xAI, Perplexity)")
|
||||
@@ -832,6 +836,16 @@ class PraisonAI:
|
||||
parser.add_argument("--temperature", type=float, help="Override temperature for LLM calls")
|
||||
parser.add_argument("--llm-provider", type=str, help="Override LLM provider (openai, anthropic, google, etc.)")
|
||||
|
||||
# Ollama Provider - native Ollama integration
|
||||
parser.add_argument("--ollama-model", type=str, help="Ollama model name (e.g., llama3.2:3b, mistral, qwen2.5:7b)")
|
||||
parser.add_argument("--ollama-host", type=str, help="Ollama server host (default: http://localhost:11434)")
|
||||
|
||||
# Tool calling reliability - for weak/local models like Ollama
|
||||
parser.add_argument("--max-tool-repairs", type=int, default=None,
|
||||
help="Max tool call repair attempts (default: 2 for Ollama, 0 for others)")
|
||||
parser.add_argument("--force-tool-usage", type=str, choices=["auto", "always", "never"], default=None,
|
||||
help="Force tool usage mode: auto (default for Ollama), always, never")
|
||||
|
||||
# If we're in a test environment, parse with empty args to avoid pytest interference
|
||||
if in_test_env:
|
||||
args, unknown_args = parser.parse_known_args([])
|
||||
@@ -3320,21 +3334,32 @@ Provide ONLY the commit message, no explanations."""
|
||||
else:
|
||||
agent_config["verbose"] = False
|
||||
|
||||
# Load default tools (same as interactive mode)
|
||||
default_tools = self._load_interactive_tools()
|
||||
if default_tools:
|
||||
agent_config["tools"] = default_tools
|
||||
# Load default tools (same as interactive mode) unless --no-tools is set
|
||||
if not getattr(self.args, 'no_tools', False):
|
||||
default_tools = self._load_interactive_tools()
|
||||
if default_tools:
|
||||
agent_config["tools"] = default_tools
|
||||
else:
|
||||
agent_config["tools"] = [] # Explicitly set empty tools
|
||||
|
||||
# Add llm if specified
|
||||
if hasattr(self, 'args') and self.args.llm:
|
||||
# Check if max_tokens is specified - pass as dict config
|
||||
# Build LLM config dict
|
||||
llm_config = {"model": self.args.llm}
|
||||
|
||||
# Add max_tokens if specified
|
||||
max_tokens = getattr(self.args, 'max_tokens', 16000)
|
||||
if max_tokens:
|
||||
# Pass llm as dict with model and max_tokens
|
||||
agent_config["llm"] = {"model": self.args.llm, "max_tokens": max_tokens}
|
||||
llm_config["max_tokens"] = max_tokens
|
||||
logging.debug(f"Max tokens set to: {max_tokens}")
|
||||
else:
|
||||
agent_config["llm"] = self.args.llm
|
||||
|
||||
# Add tool reliability settings (for weak models like Ollama)
|
||||
if getattr(self.args, 'max_tool_repairs', None) is not None:
|
||||
llm_config["max_tool_repairs"] = self.args.max_tool_repairs
|
||||
if getattr(self.args, 'force_tool_usage', None) is not None:
|
||||
llm_config["force_tool_usage"] = self.args.force_tool_usage
|
||||
|
||||
agent_config["llm"] = llm_config
|
||||
|
||||
# Add feature flags if enabled
|
||||
if hasattr(self, 'args'):
|
||||
@@ -3487,8 +3512,7 @@ Provide ONLY the commit message, no explanations."""
|
||||
from .features.external_agents import ExternalAgentsHandler
|
||||
handler = ExternalAgentsHandler(verbose=getattr(self.args, 'verbose', False))
|
||||
|
||||
# Get workspace from current directory
|
||||
import os
|
||||
# Get workspace from current directory (os is imported at module level)
|
||||
workspace = os.getcwd()
|
||||
|
||||
integration = handler.get_integration(external_agent_name, workspace=workspace)
|
||||
@@ -3604,7 +3628,11 @@ Provide ONLY the commit message, no explanations."""
|
||||
if not is_verbose:
|
||||
result = self._run_with_status_display(agent, prompt)
|
||||
else:
|
||||
result = agent.start(prompt)
|
||||
# Try start method first, fallback to chat
|
||||
if hasattr(agent, 'start'):
|
||||
result = agent.start(prompt)
|
||||
else:
|
||||
result = agent.chat(prompt)
|
||||
|
||||
# ===== POST-PROCESSING WITH NEW FEATURES =====
|
||||
|
||||
@@ -3752,11 +3780,24 @@ Now, {final_instruction.lower()}:"""
|
||||
from rich.live import Live
|
||||
from rich.text import Text
|
||||
|
||||
# Import callback registration from praisonaiagents
|
||||
from praisonaiagents import register_display_callback, sync_display_callbacks
|
||||
|
||||
console = Console()
|
||||
|
||||
# Import callback registration from praisonaiagents
|
||||
try:
|
||||
from praisonaiagents import register_display_callback, sync_display_callbacks
|
||||
except ImportError:
|
||||
# Fallback if callbacks not available - just run agent directly
|
||||
try:
|
||||
result = agent.start(prompt)
|
||||
except AttributeError:
|
||||
# Try chat method if start not available
|
||||
result = agent.chat(prompt)
|
||||
if result:
|
||||
console.print(result)
|
||||
elif result == "" or result is None:
|
||||
console.print("[dim]No response generated[/dim]")
|
||||
return result
|
||||
|
||||
# Get tool names for display
|
||||
tool_names = []
|
||||
if hasattr(agent, 'tools') and agent.tools:
|
||||
@@ -3936,7 +3977,14 @@ Now, {final_instruction.lower()}:"""
|
||||
if status_info['tool_calls']:
|
||||
console.print(f"[dim]Tools used: {', '.join(status_info['tool_calls'])}[/dim]")
|
||||
|
||||
return status_info['result']
|
||||
# Get the result and print it directly here to ensure it's displayed
|
||||
result = status_info['result']
|
||||
if result:
|
||||
console.print(result)
|
||||
elif result == "" or result is None:
|
||||
console.print("[dim]No response generated[/dim]")
|
||||
|
||||
return result
|
||||
|
||||
def _handle_serve_command(self, args, unknown_args):
|
||||
"""
|
||||
|
||||
@@ -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.3 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,488 @@
|
||||
"""
|
||||
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,
|
||||
tools_sources: Optional[List[str]] = None,
|
||||
template_dir: Optional[str] = None,
|
||||
) -> Dict[str, Callable]:
|
||||
"""
|
||||
Create a tool registry with custom overrides.
|
||||
|
||||
Resolution order (highest priority first):
|
||||
1. Override files (explicit CLI --tools)
|
||||
2. Override directories (explicit CLI --tools-dir)
|
||||
3. Template tools_sources (from TEMPLATE.yaml)
|
||||
4. Template-local tools.py
|
||||
5. Default custom dirs (~/.praison/tools, etc.)
|
||||
6. Package discovery (praisonai-tools if installed)
|
||||
7. 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
|
||||
tools_sources: Template-declared tool sources (modules or paths)
|
||||
template_dir: Template directory for local tools.py
|
||||
|
||||
Returns:
|
||||
Dict mapping tool names to callable functions
|
||||
"""
|
||||
registry = {}
|
||||
loader = ToolOverrideLoader()
|
||||
|
||||
# 7. Start with built-in tools (lowest priority)
|
||||
try:
|
||||
from praisonaiagents.tools import TOOL_MAPPINGS
|
||||
registry.update(TOOL_MAPPINGS)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# 6. Package discovery - try praisonai-tools if installed
|
||||
try:
|
||||
import praisonai_tools.tools as external_tools
|
||||
# Get all exported tools from praisonai_tools
|
||||
for name in dir(external_tools):
|
||||
if not name.startswith('_'):
|
||||
obj = getattr(external_tools, name, None)
|
||||
if callable(obj) or (hasattr(obj, 'run') and callable(getattr(obj, 'run', None))):
|
||||
registry[name] = obj
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# 5. 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
|
||||
|
||||
# 4. Template-local tools.py
|
||||
if template_dir:
|
||||
tools_py = Path(template_dir) / "tools.py"
|
||||
if tools_py.exists():
|
||||
try:
|
||||
tools = loader.load_from_file(str(tools_py))
|
||||
registry.update(tools)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 3. Template tools_sources (from TEMPLATE.yaml)
|
||||
if tools_sources:
|
||||
for source in tools_sources:
|
||||
try:
|
||||
# Security: only allow local paths and python modules
|
||||
if source.startswith(("http://", "https://", "ftp://")):
|
||||
continue # Skip remote URLs
|
||||
|
||||
source_path = Path(source).expanduser()
|
||||
|
||||
if source_path.exists():
|
||||
# It's a local path
|
||||
if source_path.is_file() and source_path.suffix == ".py":
|
||||
tools = loader.load_from_file(str(source_path))
|
||||
registry.update(tools)
|
||||
elif source_path.is_dir():
|
||||
tools = loader.load_from_directory(str(source_path))
|
||||
registry.update(tools)
|
||||
else:
|
||||
# Try as a Python module path
|
||||
tools = loader.load_from_module(source)
|
||||
registry.update(tools)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. Add override directories (CLI --tools-dir)
|
||||
if override_dirs:
|
||||
for dir_path in override_dirs:
|
||||
try:
|
||||
tools = loader.load_from_directory(dir_path)
|
||||
registry.update(tools)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 1. Add override files (highest priority, CLI --tools)
|
||||
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
|
||||
|
||||
|
||||
def resolve_tools(
|
||||
tool_names: List[Any],
|
||||
registry: Optional[Dict[str, Callable]] = None,
|
||||
template_dir: Optional[str] = None,
|
||||
) -> List[Callable]:
|
||||
"""
|
||||
Resolve tool names to callable tools from registry.
|
||||
|
||||
Handles:
|
||||
- String tool names (looked up in registry)
|
||||
- Already-callable tools (passed through)
|
||||
- Built-in tool names (shell_tool, file_tool, etc.)
|
||||
|
||||
Args:
|
||||
tool_names: List of tool names (strings) or callables
|
||||
registry: Tool registry to look up names in
|
||||
template_dir: Optional template directory for local tools.py autoload
|
||||
|
||||
Returns:
|
||||
List of resolved callable tools
|
||||
"""
|
||||
if not tool_names:
|
||||
return []
|
||||
|
||||
resolved = []
|
||||
|
||||
# Build registry if not provided
|
||||
if registry is None:
|
||||
registry = create_tool_registry_with_overrides(include_defaults=True)
|
||||
|
||||
# Load template-local tools.py if exists
|
||||
if template_dir:
|
||||
loader = ToolOverrideLoader()
|
||||
tools_py = Path(template_dir) / "tools.py"
|
||||
if tools_py.exists():
|
||||
try:
|
||||
local_tools = loader.load_from_file(str(tools_py))
|
||||
registry.update(local_tools)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for tool in tool_names:
|
||||
if callable(tool):
|
||||
# Already a callable, use directly
|
||||
resolved.append(tool)
|
||||
elif isinstance(tool, str):
|
||||
# Look up by name in registry
|
||||
tool_name = tool.strip()
|
||||
|
||||
# Try exact match first
|
||||
if tool_name in registry:
|
||||
tool_obj = registry[tool_name]
|
||||
# Handle lazy-loaded tools (tuples of module, class)
|
||||
if isinstance(tool_obj, tuple):
|
||||
try:
|
||||
module_name, class_name = tool_obj
|
||||
import importlib
|
||||
module = importlib.import_module(module_name)
|
||||
tool_class = getattr(module, class_name)
|
||||
resolved.append(tool_class())
|
||||
except Exception:
|
||||
pass
|
||||
elif callable(tool_obj):
|
||||
resolved.append(tool_obj)
|
||||
else:
|
||||
# Try to instantiate if it's a class
|
||||
try:
|
||||
resolved.append(tool_obj())
|
||||
except Exception:
|
||||
resolved.append(tool_obj)
|
||||
else:
|
||||
# Try common variations
|
||||
variations = [
|
||||
tool_name,
|
||||
tool_name.lower(),
|
||||
tool_name.replace("-", "_"),
|
||||
tool_name.replace("_", "-"),
|
||||
f"{tool_name}_tool",
|
||||
f"{tool_name}Tool",
|
||||
]
|
||||
found = False
|
||||
for var in variations:
|
||||
if var in registry:
|
||||
tool_obj = registry[var]
|
||||
if callable(tool_obj):
|
||||
resolved.append(tool_obj)
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
# Try to import from praisonaiagents.tools
|
||||
try:
|
||||
from praisonaiagents import tools as agent_tools
|
||||
if hasattr(agent_tools, tool_name):
|
||||
resolved.append(getattr(agent_tools, tool_name))
|
||||
elif hasattr(agent_tools, f"{tool_name}_tool"):
|
||||
resolved.append(getattr(agent_tools, f"{tool_name}_tool"))
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
return resolved
|
||||
@@ -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.0"
|
||||
__version__ = "2.6.3"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.4.0",
|
||||
"praisonaiagents>=0.4.1",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ import argparse
|
||||
import subprocess
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def get_project_root() -> Path:
|
||||
@@ -36,7 +37,7 @@ def get_praisonai_dir() -> Path:
|
||||
return get_project_root() / "src/praisonai"
|
||||
|
||||
|
||||
def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> subprocess.CompletedProcess:
|
||||
def run(cmd: list[str], cwd: Optional[Path] = None, check: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a command and print it."""
|
||||
print(f"$ {' '.join(cmd)}")
|
||||
result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
|
||||
@@ -71,7 +72,7 @@ def update_file(filepath: Path, patterns: list[tuple[str, str]], root: Path) ->
|
||||
return False
|
||||
|
||||
|
||||
def bump_version(new_version: str, agents_version: str | None = None):
|
||||
def bump_version(new_version: str, agents_version: Optional[str] = None):
|
||||
"""Bump version in all required files."""
|
||||
root = get_project_root()
|
||||
praisonai_dir = get_praisonai_dir()
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
"""
|
||||
Live integration tests for Ollama tool calling.
|
||||
|
||||
These tests require:
|
||||
1. Ollama running locally (default: http://localhost:11434)
|
||||
2. olmo-3 model pulled: ollama pull olmo-3
|
||||
3. Environment variable: PRAISONAI_TEST_OLLAMA=1
|
||||
|
||||
Run with:
|
||||
PRAISONAI_TEST_OLLAMA=1 LOGLEVEL=debug python -m pytest tests/integration/test_ollama_tool_calling_live.py -v
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import logging
|
||||
|
||||
|
||||
# Skip all tests in this module if PRAISONAI_TEST_OLLAMA is not set
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not os.getenv("PRAISONAI_TEST_OLLAMA"),
|
||||
reason="Ollama live tests disabled. Set PRAISONAI_TEST_OLLAMA=1 to enable."
|
||||
)
|
||||
|
||||
|
||||
def calculator(a: int, b: int) -> int:
|
||||
"""Add two integers together.
|
||||
|
||||
Args:
|
||||
a: First number to add
|
||||
b: Second number to add
|
||||
|
||||
Returns:
|
||||
The sum of a and b
|
||||
"""
|
||||
return a + b
|
||||
|
||||
|
||||
def check_ollama_available():
|
||||
"""Check if Ollama is running and accessible."""
|
||||
try:
|
||||
import httpx
|
||||
response = httpx.get("http://localhost:11434/api/tags", timeout=5.0)
|
||||
return response.status_code == 200
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def ollama_available():
|
||||
"""Fixture to check Ollama availability."""
|
||||
if not check_ollama_available():
|
||||
pytest.skip("Ollama is not running at localhost:11434")
|
||||
return True
|
||||
|
||||
|
||||
class TestOllamaToolCallingLive:
|
||||
"""Live tests for Ollama tool calling with olmo-3."""
|
||||
|
||||
def test_basic_tool_call(self, ollama_available):
|
||||
"""Test basic tool calling with calculator."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Calculator Agent",
|
||||
llm="ollama/olmo-3",
|
||||
tools=[calculator],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.chat("Compute 17 + 25. You MUST use the calculator tool.")
|
||||
|
||||
# Verify result contains the correct answer
|
||||
assert result is not None
|
||||
assert "42" in str(result)
|
||||
|
||||
def test_forced_tool_usage(self, ollama_available):
|
||||
"""Test that force_tool_usage=always works."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Calculator Agent",
|
||||
llm="ollama/olmo-3",
|
||||
tools=[calculator],
|
||||
force_tool_usage="always",
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.chat("What is 17 plus 25?")
|
||||
|
||||
# Should still get correct answer via tool
|
||||
assert result is not None
|
||||
assert "42" in str(result)
|
||||
|
||||
def test_tool_call_with_distraction(self, ollama_available):
|
||||
"""Test tool calling when model might want to answer directly."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Calculator Agent",
|
||||
llm="ollama/olmo-3",
|
||||
tools=[calculator],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.chat(
|
||||
"I know 17+25 is easy, but please use the calculator tool to compute it anyway."
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert "42" in str(result)
|
||||
|
||||
def test_multi_step_arithmetic(self, ollama_available):
|
||||
"""Test multiple tool calls for multi-step computation."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Calculator Agent",
|
||||
llm="ollama/olmo-3",
|
||||
tools=[calculator],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.chat(
|
||||
"Compute (17+25) + (8+9). Use the calculator tool for each addition."
|
||||
)
|
||||
|
||||
# Final answer should be 59 (42 + 17)
|
||||
assert result is not None
|
||||
# Check for either intermediate or final results
|
||||
assert any(x in str(result) for x in ["42", "17", "59"])
|
||||
|
||||
def test_max_tool_repairs_setting(self, ollama_available):
|
||||
"""Test that max_tool_repairs setting is respected."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Calculator Agent",
|
||||
llm="ollama/olmo-3",
|
||||
tools=[calculator],
|
||||
max_tool_repairs=3,
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.chat("Calculate 100 + 200 using the calculator tool.")
|
||||
|
||||
assert result is not None
|
||||
assert "300" in str(result)
|
||||
|
||||
def test_no_tools_direct_answer(self, ollama_available):
|
||||
"""Test that model answers directly when no tools provided."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Direct Agent",
|
||||
llm="ollama/olmo-3",
|
||||
tools=[], # No tools
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.chat("What is 17 + 25?")
|
||||
|
||||
# Should get an answer (may or may not be correct without tools)
|
||||
assert result is not None
|
||||
|
||||
|
||||
class TestOllamaToolCallingDebugLogging:
|
||||
"""Tests that verify debug logging works correctly."""
|
||||
|
||||
def test_debug_logging_enabled(self, ollama_available, caplog):
|
||||
"""Test that debug logging captures tool call details."""
|
||||
from praisonaiagents import Agent
|
||||
|
||||
# Enable debug logging
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
|
||||
with caplog.at_level(logging.DEBUG):
|
||||
agent = Agent(
|
||||
name="Calculator Agent",
|
||||
llm="ollama/olmo-3",
|
||||
tools=[calculator],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
result = agent.chat("Compute 5 + 3 using the calculator tool.")
|
||||
|
||||
# Check that debug logs were captured
|
||||
# Should see tool-related debug messages
|
||||
# Note: exact messages depend on implementation
|
||||
assert result is not None
|
||||
# Verify some logging occurred (caplog.text contains all captured logs)
|
||||
assert len(caplog.records) >= 0 # At minimum, logging infrastructure works
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Enable debug logging for manual runs
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
print("=" * 60)
|
||||
print("Ollama Tool Calling Live Tests")
|
||||
print("=" * 60)
|
||||
|
||||
if not check_ollama_available():
|
||||
print("ERROR: Ollama is not running at localhost:11434")
|
||||
print("Start Ollama with: ollama serve")
|
||||
print("Pull model with: ollama pull olmo-3")
|
||||
exit(1)
|
||||
|
||||
print("Ollama is available. Running tests...")
|
||||
print()
|
||||
|
||||
# Run a simple test
|
||||
from praisonaiagents import Agent
|
||||
|
||||
agent = Agent(
|
||||
name="Calculator Agent",
|
||||
llm="ollama/olmo-3",
|
||||
tools=[calculator],
|
||||
verbose=True
|
||||
)
|
||||
|
||||
print("Test 1: Basic tool call")
|
||||
print("-" * 40)
|
||||
result = agent.chat("Compute 17 + 25. You MUST use the calculator tool.")
|
||||
print(f"Result: {result}")
|
||||
print()
|
||||
|
||||
if "42" in str(result):
|
||||
print("✅ Test PASSED: Got correct answer 42")
|
||||
else:
|
||||
print("❌ Test FAILED: Did not get expected answer 42")
|
||||
@@ -0,0 +1 @@
|
||||
# Unit tests for LLM module
|
||||
@@ -0,0 +1,541 @@
|
||||
"""
|
||||
Unit tests for Ollama tool calling reliability features.
|
||||
|
||||
Tests cover:
|
||||
1. Tool call parsing from JSON response
|
||||
2. Force tool usage prompting
|
||||
3. Tool call repair loop
|
||||
4. max_tool_repairs configuration
|
||||
5. force_tool_usage modes (auto/always/never)
|
||||
6. Loop prevention
|
||||
|
||||
These tests mock the LLM class to avoid requiring litellm.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import os
|
||||
import json
|
||||
|
||||
# Set dummy API key for tests
|
||||
os.environ.setdefault('OPENAI_API_KEY', 'test-key-for-unit-tests')
|
||||
|
||||
|
||||
class MockLLM:
|
||||
"""Mock LLM class for testing tool reliability features without litellm."""
|
||||
|
||||
# Prompt templates (copied from actual LLM class)
|
||||
FORCE_TOOL_USAGE_PROMPT = """You MUST use one of the available tools to answer this question.
|
||||
Do NOT provide a direct answer. Respond ONLY with a tool call.
|
||||
|
||||
Available tools: {tool_names}
|
||||
|
||||
Respond with a JSON object in this format:
|
||||
{{"name": "tool_name", "arguments": {{"arg1": value1, "arg2": value2}}}}"""
|
||||
|
||||
TOOL_CALL_REPAIR_PROMPT = """Your previous tool call was malformed: {error}
|
||||
|
||||
Please try again. Available tools:
|
||||
{tool_schemas}
|
||||
|
||||
Respond with ONLY a valid JSON tool call in this format:
|
||||
{{"name": "tool_name", "arguments": {{"arg1": value1, "arg2": value2}}}}"""
|
||||
|
||||
def __init__(self, model, max_tool_repairs=None, force_tool_usage=None, base_url=None, **kwargs):
|
||||
self.model = model
|
||||
self.base_url = base_url
|
||||
|
||||
# Track if values were explicitly set
|
||||
self._max_tool_repairs_explicit = max_tool_repairs is not None
|
||||
self._force_tool_usage_explicit = force_tool_usage is not None
|
||||
|
||||
# Set defaults
|
||||
self.max_tool_repairs = max_tool_repairs if max_tool_repairs is not None else 0
|
||||
self.force_tool_usage = force_tool_usage if force_tool_usage is not None else 'never'
|
||||
|
||||
# Apply Ollama defaults
|
||||
self._apply_ollama_defaults()
|
||||
|
||||
def _is_ollama_provider(self) -> bool:
|
||||
"""Detect if this is an Ollama provider."""
|
||||
if not self.model:
|
||||
return False
|
||||
if self.model.startswith("ollama/"):
|
||||
return True
|
||||
if self.base_url and ("ollama" in self.base_url.lower() or ":11434" in self.base_url):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _apply_ollama_defaults(self):
|
||||
"""Apply Ollama-specific defaults for tool calling reliability."""
|
||||
if self._is_ollama_provider():
|
||||
if not self._max_tool_repairs_explicit:
|
||||
self.max_tool_repairs = 2
|
||||
if not self._force_tool_usage_explicit:
|
||||
self.force_tool_usage = 'auto'
|
||||
|
||||
def _get_tool_names_for_prompt(self, formatted_tools):
|
||||
"""Extract tool names from formatted tools for prompts."""
|
||||
if not formatted_tools:
|
||||
return "None"
|
||||
names = []
|
||||
for tool in formatted_tools:
|
||||
if isinstance(tool, dict) and 'function' in tool:
|
||||
names.append(tool['function'].get('name', 'unknown'))
|
||||
return ', '.join(names) if names else "None"
|
||||
|
||||
def _get_tool_schemas_for_prompt(self, formatted_tools):
|
||||
"""Generate compact tool schemas for repair prompts."""
|
||||
if not formatted_tools:
|
||||
return "None"
|
||||
schemas = []
|
||||
for tool in formatted_tools:
|
||||
if isinstance(tool, dict) and 'function' in tool:
|
||||
func = tool['function']
|
||||
name = func.get('name', 'unknown')
|
||||
params = func.get('parameters', {})
|
||||
required = params.get('required', [])
|
||||
props = params.get('properties', {})
|
||||
param_strs = []
|
||||
for pname, pinfo in props.items():
|
||||
ptype = pinfo.get('type', 'any')
|
||||
req = '*' if pname in required else ''
|
||||
param_strs.append(f"{pname}{req}: {ptype}")
|
||||
schemas.append(f"- {name}({', '.join(param_strs)})")
|
||||
return '\n'.join(schemas) if schemas else "None"
|
||||
|
||||
def _should_force_tool_usage(self, response_text, tool_calls, formatted_tools, iteration_count):
|
||||
"""Determine if we should force tool usage."""
|
||||
if not formatted_tools:
|
||||
return False
|
||||
if tool_calls:
|
||||
return False
|
||||
|
||||
if self.force_tool_usage == 'never':
|
||||
return False
|
||||
elif self.force_tool_usage == 'always':
|
||||
return True
|
||||
elif self.force_tool_usage == 'auto':
|
||||
return self._is_ollama_provider() and iteration_count == 0
|
||||
return False
|
||||
|
||||
def _try_parse_tool_call_json(self, response_text, iteration_count):
|
||||
"""Try to parse tool call from JSON response text."""
|
||||
if not response_text or not response_text.strip():
|
||||
return None, None
|
||||
|
||||
try:
|
||||
response_json = json.loads(response_text.strip())
|
||||
if isinstance(response_json, dict) and "name" in response_json:
|
||||
tool_calls = [{
|
||||
"id": f"tool_{iteration_count}",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": response_json["name"],
|
||||
"arguments": json.dumps(response_json.get("arguments", {}))
|
||||
}
|
||||
}]
|
||||
return tool_calls, None
|
||||
elif isinstance(response_json, list):
|
||||
tool_calls = []
|
||||
for idx, tool_json in enumerate(response_json):
|
||||
if isinstance(tool_json, dict) and "name" in tool_json:
|
||||
tool_calls.append({
|
||||
"id": f"tool_{iteration_count}_{idx}",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool_json["name"],
|
||||
"arguments": json.dumps(tool_json.get("arguments", {}))
|
||||
}
|
||||
})
|
||||
return tool_calls if tool_calls else None, None
|
||||
else:
|
||||
return None, "Response is not a valid tool call format (missing 'name' field)"
|
||||
except json.JSONDecodeError as e:
|
||||
return None, f"JSON parse error: {str(e)}"
|
||||
|
||||
def _validate_tool_call(self, tool_call, formatted_tools):
|
||||
"""Validate a tool call against available tools."""
|
||||
if not formatted_tools:
|
||||
return None
|
||||
|
||||
try:
|
||||
func_info = tool_call.get('function', {})
|
||||
tool_name = func_info.get('name', '')
|
||||
arguments_str = func_info.get('arguments', '{}')
|
||||
|
||||
available_names = [t['function']['name'] for t in formatted_tools if isinstance(t, dict) and 'function' in t]
|
||||
if tool_name not in available_names:
|
||||
return f"Unknown tool '{tool_name}'. Available: {', '.join(available_names)}"
|
||||
|
||||
try:
|
||||
arguments = json.loads(arguments_str) if isinstance(arguments_str, str) else arguments_str
|
||||
except json.JSONDecodeError as e:
|
||||
return f"Invalid arguments JSON: {str(e)}"
|
||||
|
||||
for tool in formatted_tools:
|
||||
if isinstance(tool, dict) and tool.get('function', {}).get('name') == tool_name:
|
||||
params = tool['function'].get('parameters', {})
|
||||
required = params.get('required', [])
|
||||
missing = [r for r in required if r not in arguments]
|
||||
if missing:
|
||||
return f"Missing required arguments for '{tool_name}': {', '.join(missing)}"
|
||||
break
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
return f"Validation error: {str(e)}"
|
||||
|
||||
def _format_ollama_tool_result_message(self, function_name, tool_result):
|
||||
"""Format tool result message for Ollama provider."""
|
||||
tool_result_str = str(tool_result)
|
||||
return {
|
||||
"role": "user",
|
||||
"content": f"""Tool execution complete.
|
||||
Function: {function_name}
|
||||
Result: {tool_result_str}
|
||||
|
||||
Now provide your final answer using this result."""
|
||||
}
|
||||
|
||||
|
||||
class TestOllamaToolReliability:
|
||||
"""Tests for Ollama tool calling reliability enhancements."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_llm(self):
|
||||
"""Create a mock LLM instance for testing."""
|
||||
return MockLLM(model="ollama/olmo-3")
|
||||
|
||||
@pytest.fixture
|
||||
def mock_llm_openai(self):
|
||||
"""Create a mock LLM instance for OpenAI (non-Ollama)."""
|
||||
return MockLLM(model="gpt-4o-mini")
|
||||
|
||||
@pytest.fixture
|
||||
def sample_formatted_tools(self):
|
||||
"""Sample formatted tools for testing."""
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"description": "Add two integers",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"a": {"type": "integer"},
|
||||
"b": {"type": "integer"}
|
||||
},
|
||||
"required": ["a", "b"]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
# ===== Test Ollama Defaults =====
|
||||
|
||||
def test_ollama_defaults_applied(self, mock_llm):
|
||||
"""Test that Ollama-specific defaults are applied."""
|
||||
assert mock_llm.max_tool_repairs == 2
|
||||
assert mock_llm.force_tool_usage == 'auto'
|
||||
|
||||
def test_non_ollama_defaults(self, mock_llm_openai):
|
||||
"""Test that non-Ollama models get default values."""
|
||||
assert mock_llm_openai.max_tool_repairs == 0
|
||||
assert mock_llm_openai.force_tool_usage == 'never'
|
||||
|
||||
def test_explicit_override_respected(self):
|
||||
"""Test that explicit user overrides are respected."""
|
||||
llm = MockLLM(model="ollama/olmo-3", max_tool_repairs=5, force_tool_usage='always')
|
||||
assert llm.max_tool_repairs == 5
|
||||
assert llm.force_tool_usage == 'always'
|
||||
|
||||
# ===== Test Tool Call Parsing =====
|
||||
|
||||
def test_try_parse_tool_call_json_valid(self, mock_llm):
|
||||
"""Test parsing valid tool call JSON."""
|
||||
response_text = '{"name": "calculator", "arguments": {"a": 17, "b": 25}}'
|
||||
tool_calls, error = mock_llm._try_parse_tool_call_json(response_text, 0)
|
||||
|
||||
assert tool_calls is not None
|
||||
assert len(tool_calls) == 1
|
||||
assert tool_calls[0]['function']['name'] == 'calculator'
|
||||
assert error is None
|
||||
|
||||
def test_try_parse_tool_call_json_invalid(self, mock_llm):
|
||||
"""Test parsing invalid JSON returns error."""
|
||||
response_text = 'This is not JSON'
|
||||
tool_calls, error = mock_llm._try_parse_tool_call_json(response_text, 0)
|
||||
|
||||
assert tool_calls is None
|
||||
assert error is not None
|
||||
assert "JSON parse error" in error
|
||||
|
||||
def test_try_parse_tool_call_json_empty(self, mock_llm):
|
||||
"""Test parsing empty response."""
|
||||
tool_calls, error = mock_llm._try_parse_tool_call_json("", 0)
|
||||
assert tool_calls is None
|
||||
assert error is None
|
||||
|
||||
def test_try_parse_tool_call_json_list(self, mock_llm):
|
||||
"""Test parsing list of tool calls."""
|
||||
response_text = '[{"name": "calculator", "arguments": {"a": 1, "b": 2}}, {"name": "calculator", "arguments": {"a": 3, "b": 4}}]'
|
||||
tool_calls, error = mock_llm._try_parse_tool_call_json(response_text, 0)
|
||||
|
||||
assert tool_calls is not None
|
||||
assert len(tool_calls) == 2
|
||||
|
||||
# ===== Test Tool Call Validation =====
|
||||
|
||||
def test_validate_tool_call_valid(self, mock_llm, sample_formatted_tools):
|
||||
"""Test validation of valid tool call."""
|
||||
tool_call = {
|
||||
"id": "tool_0",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"arguments": '{"a": 17, "b": 25}'
|
||||
}
|
||||
}
|
||||
error = mock_llm._validate_tool_call(tool_call, sample_formatted_tools)
|
||||
assert error is None
|
||||
|
||||
def test_validate_tool_call_unknown_tool(self, mock_llm, sample_formatted_tools):
|
||||
"""Test validation fails for unknown tool."""
|
||||
tool_call = {
|
||||
"id": "tool_0",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "unknown_tool",
|
||||
"arguments": '{}'
|
||||
}
|
||||
}
|
||||
error = mock_llm._validate_tool_call(tool_call, sample_formatted_tools)
|
||||
assert error is not None
|
||||
assert "Unknown tool" in error
|
||||
|
||||
def test_validate_tool_call_missing_required_args(self, mock_llm, sample_formatted_tools):
|
||||
"""Test validation fails for missing required arguments."""
|
||||
tool_call = {
|
||||
"id": "tool_0",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"arguments": '{"a": 17}' # Missing 'b'
|
||||
}
|
||||
}
|
||||
error = mock_llm._validate_tool_call(tool_call, sample_formatted_tools)
|
||||
assert error is not None
|
||||
assert "Missing required arguments" in error
|
||||
|
||||
def test_validate_tool_call_invalid_json_args(self, mock_llm, sample_formatted_tools):
|
||||
"""Test validation fails for invalid JSON arguments."""
|
||||
tool_call = {
|
||||
"id": "tool_0",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"arguments": 'not valid json'
|
||||
}
|
||||
}
|
||||
error = mock_llm._validate_tool_call(tool_call, sample_formatted_tools)
|
||||
assert error is not None
|
||||
assert "Invalid arguments JSON" in error
|
||||
|
||||
# ===== Test Force Tool Usage =====
|
||||
|
||||
def test_should_force_tool_usage_auto_ollama(self, mock_llm, sample_formatted_tools):
|
||||
"""Test force tool usage in auto mode for Ollama."""
|
||||
# First iteration, no tool calls, has tools
|
||||
result = mock_llm._should_force_tool_usage(
|
||||
response_text="I'll just answer directly",
|
||||
tool_calls=None,
|
||||
formatted_tools=sample_formatted_tools,
|
||||
iteration_count=0
|
||||
)
|
||||
assert result is True
|
||||
|
||||
def test_should_force_tool_usage_auto_not_first_iteration(self, mock_llm, sample_formatted_tools):
|
||||
"""Test force tool usage doesn't trigger after first iteration."""
|
||||
result = mock_llm._should_force_tool_usage(
|
||||
response_text="I'll just answer directly",
|
||||
tool_calls=None,
|
||||
formatted_tools=sample_formatted_tools,
|
||||
iteration_count=1
|
||||
)
|
||||
assert result is False
|
||||
|
||||
def test_should_force_tool_usage_never(self, mock_llm, sample_formatted_tools):
|
||||
"""Test force tool usage never mode."""
|
||||
mock_llm.force_tool_usage = 'never'
|
||||
result = mock_llm._should_force_tool_usage(
|
||||
response_text="I'll just answer directly",
|
||||
tool_calls=None,
|
||||
formatted_tools=sample_formatted_tools,
|
||||
iteration_count=0
|
||||
)
|
||||
assert result is False
|
||||
|
||||
def test_should_force_tool_usage_always(self, mock_llm, sample_formatted_tools):
|
||||
"""Test force tool usage always mode."""
|
||||
mock_llm.force_tool_usage = 'always'
|
||||
result = mock_llm._should_force_tool_usage(
|
||||
response_text="I'll just answer directly",
|
||||
tool_calls=None,
|
||||
formatted_tools=sample_formatted_tools,
|
||||
iteration_count=5 # Even on later iterations
|
||||
)
|
||||
assert result is True
|
||||
|
||||
def test_should_force_tool_usage_with_existing_tool_calls(self, mock_llm, sample_formatted_tools):
|
||||
"""Test force tool usage doesn't trigger when tool calls exist."""
|
||||
mock_llm.force_tool_usage = 'always'
|
||||
tool_calls = [{"id": "tool_0", "function": {"name": "calculator"}}]
|
||||
result = mock_llm._should_force_tool_usage(
|
||||
response_text="",
|
||||
tool_calls=tool_calls,
|
||||
formatted_tools=sample_formatted_tools,
|
||||
iteration_count=0
|
||||
)
|
||||
assert result is False
|
||||
|
||||
def test_should_force_tool_usage_no_tools(self, mock_llm):
|
||||
"""Test force tool usage doesn't trigger without tools."""
|
||||
mock_llm.force_tool_usage = 'always'
|
||||
result = mock_llm._should_force_tool_usage(
|
||||
response_text="I'll just answer directly",
|
||||
tool_calls=None,
|
||||
formatted_tools=None,
|
||||
iteration_count=0
|
||||
)
|
||||
assert result is False
|
||||
|
||||
# ===== Test Helper Methods =====
|
||||
|
||||
def test_get_tool_names_for_prompt(self, mock_llm, sample_formatted_tools):
|
||||
"""Test extracting tool names for prompts."""
|
||||
names = mock_llm._get_tool_names_for_prompt(sample_formatted_tools)
|
||||
assert names == "calculator"
|
||||
|
||||
def test_get_tool_names_for_prompt_empty(self, mock_llm):
|
||||
"""Test extracting tool names from empty list."""
|
||||
names = mock_llm._get_tool_names_for_prompt(None)
|
||||
assert names == "None"
|
||||
|
||||
def test_get_tool_schemas_for_prompt(self, mock_llm, sample_formatted_tools):
|
||||
"""Test generating tool schemas for prompts."""
|
||||
schemas = mock_llm._get_tool_schemas_for_prompt(sample_formatted_tools)
|
||||
assert "calculator" in schemas
|
||||
assert "a*" in schemas # Required arg marked with *
|
||||
assert "b*" in schemas
|
||||
|
||||
def test_format_ollama_tool_result_message(self, mock_llm):
|
||||
"""Test formatting tool result message for Ollama."""
|
||||
result = mock_llm._format_ollama_tool_result_message("calculator", 42)
|
||||
assert result["role"] == "user"
|
||||
assert "calculator" in result["content"]
|
||||
assert "42" in result["content"]
|
||||
assert "final answer" in result["content"].lower()
|
||||
|
||||
# ===== Test Prompt Templates =====
|
||||
|
||||
def test_force_tool_usage_prompt_template(self, mock_llm):
|
||||
"""Test force tool usage prompt template."""
|
||||
prompt = mock_llm.FORCE_TOOL_USAGE_PROMPT.format(tool_names="calculator")
|
||||
assert "MUST use" in prompt
|
||||
assert "calculator" in prompt
|
||||
assert "tool call" in prompt.lower()
|
||||
|
||||
def test_tool_call_repair_prompt_template(self, mock_llm):
|
||||
"""Test tool call repair prompt template."""
|
||||
prompt = mock_llm.TOOL_CALL_REPAIR_PROMPT.format(
|
||||
error="Missing required argument 'b'",
|
||||
tool_schemas="- calculator(a*: integer, b*: integer)"
|
||||
)
|
||||
assert "malformed" in prompt.lower()
|
||||
assert "Missing required argument" in prompt
|
||||
assert "calculator" in prompt
|
||||
|
||||
|
||||
class TestOllamaToolReliabilityIntegration:
|
||||
"""Integration-style tests for tool reliability using MockLLM."""
|
||||
|
||||
def test_force_tool_usage_flow(self):
|
||||
"""Test the complete force tool usage flow with MockLLM."""
|
||||
llm = MockLLM(model="ollama/olmo-3")
|
||||
|
||||
sample_tools = [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"parameters": {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"]}
|
||||
}
|
||||
}]
|
||||
|
||||
# Simulate: model returns text instead of tool call
|
||||
response_text = "The answer is 42"
|
||||
tool_calls = None
|
||||
|
||||
# Should trigger force tool usage on first iteration
|
||||
should_force = llm._should_force_tool_usage(response_text, tool_calls, sample_tools, 0)
|
||||
assert should_force is True
|
||||
|
||||
# Should NOT trigger on second iteration
|
||||
should_force = llm._should_force_tool_usage(response_text, tool_calls, sample_tools, 1)
|
||||
assert should_force is False
|
||||
|
||||
def test_repair_loop_flow(self):
|
||||
"""Test the complete repair loop flow with MockLLM."""
|
||||
llm = MockLLM(model="ollama/olmo-3")
|
||||
|
||||
sample_tools = [{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"parameters": {"type": "object", "properties": {"a": {"type": "integer"}}, "required": ["a"]}
|
||||
}
|
||||
}]
|
||||
|
||||
# Simulate: malformed tool call (unknown tool)
|
||||
bad_tool_call = {
|
||||
"id": "tool_0",
|
||||
"type": "function",
|
||||
"function": {"name": "unknown_tool", "arguments": "{}"}
|
||||
}
|
||||
|
||||
error = llm._validate_tool_call(bad_tool_call, sample_tools)
|
||||
assert error is not None
|
||||
assert "Unknown tool" in error
|
||||
|
||||
# Simulate: valid tool call after repair
|
||||
good_tool_call = {
|
||||
"id": "tool_1",
|
||||
"type": "function",
|
||||
"function": {"name": "calculator", "arguments": '{"a": 17}'}
|
||||
}
|
||||
|
||||
error = llm._validate_tool_call(good_tool_call, sample_tools)
|
||||
assert error is None
|
||||
|
||||
|
||||
class TestOllamaProviderDetection:
|
||||
"""Tests for Ollama provider detection."""
|
||||
|
||||
def test_detect_ollama_prefix(self):
|
||||
"""Test detection via ollama/ prefix."""
|
||||
llm = MockLLM(model="ollama/llama3.2")
|
||||
assert llm._is_ollama_provider() is True
|
||||
|
||||
def test_detect_ollama_base_url(self):
|
||||
"""Test detection via base_url containing ollama."""
|
||||
llm = MockLLM(model="llama3.2", base_url="http://localhost:11434")
|
||||
assert llm._is_ollama_provider() is True
|
||||
|
||||
def test_detect_non_ollama(self):
|
||||
"""Test non-Ollama models are not detected as Ollama."""
|
||||
llm = MockLLM(model="gpt-4o-mini")
|
||||
assert llm._is_ollama_provider() is False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
File diff suppressed because it is too large.
Load diff
Generated
+11
-257
@@ -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,10 +4114,10 @@ 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 = "praisonaiagents", specifier = ">=0.4.0" },
|
||||
{ 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.1" },
|
||||
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = "==0.2.29" },
|
||||
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
|
||||
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
|
||||
@@ -4227,32 +4147,19 @@ 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]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4261,9 +4168,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/21/f3d8224b0dc6fe8fa0f9a2209f8def8941f18b43696befb41e1a5574b1a6/praisonaiagents-0.4.0.tar.gz", hash = "sha256:cd96b0711502cafc5c7d31cd667a0754cb37ce354cf3e819bca77491741e7f07", size = 763895, upload-time = "2025-12-26T19:30:31.887Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/e8/5cf1306785b5e47b278363f58dee68ea381d26acc87ac9fd86b9d7abab0a/praisonaiagents-0.4.1.tar.gz", hash = "sha256:61cf37b3392c8857855176cdae6a758a88b110d103c61e226a4da7ab2ad8d30c", size = 766683, upload-time = "2025-12-28T07:27:21.999Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/7e/53291318129fb5e61e4efc3af74183c9dc558c2b9b1477d3c68c879f08df/praisonaiagents-0.4.0-py3-none-any.whl", hash = "sha256:a08e7d350fd3e47e15eb3d919279c79af5867967834b84010489ec419b008729", size = 711196, upload-time = "2025-12-26T19:30:30.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/26/b1593bf8d360793cc7e82020f7bef080734290370ddbb54658c6d9a0d8e1/praisonaiagents-0.4.1-py3-none-any.whl", hash = "sha256:0cdb9c8514cf042340c093103d9051a4d25dfb6d8126d05757b552f21b8b7085", size = 713637, upload-time = "2025-12-28T07:27:19.944Z" },
|
||||
]
|
||||
|
||||
[[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"
|
||||
|
||||
Reference in new issue
Block a user