mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 11:51:37 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8fa61b082 | ||
|
|
955d236924 |
No files matched your search
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=2.3.83" \
|
||||
"praisonai>=2.3.84" \
|
||||
"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.3.83" \
|
||||
"praisonai>=2.3.84" \
|
||||
"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.3.83" \
|
||||
"praisonai>=2.3.84" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -651,8 +651,45 @@ Your Goal: {self.goal}
|
||||
self._skill_manager.discover(self._skills_dirs, include_defaults=False)
|
||||
|
||||
self._skills_initialized = True
|
||||
|
||||
# Auto-add skill execution tools if not already present
|
||||
self._add_skill_tools()
|
||||
return self._skill_manager
|
||||
|
||||
def _add_skill_tools(self):
|
||||
"""Add tools required for skill execution (read_file, run_skill_script).
|
||||
|
||||
Uses lazy imports from praisonaiagents.tools to avoid performance impact
|
||||
when skills are not used.
|
||||
"""
|
||||
# Check if tools already include required capabilities
|
||||
tool_names = set()
|
||||
for tool in self.tools:
|
||||
if callable(tool) and hasattr(tool, '__name__'):
|
||||
tool_names.add(tool.__name__)
|
||||
elif hasattr(tool, 'name'):
|
||||
tool_names.add(tool.name)
|
||||
|
||||
# Add read_file if not present
|
||||
if 'read_file' not in tool_names:
|
||||
try:
|
||||
from ..tools import read_file
|
||||
self.tools.append(read_file)
|
||||
logging.debug("Added read_file tool for skill support")
|
||||
except ImportError:
|
||||
logging.warning("Could not import read_file tool for skills")
|
||||
|
||||
# Add run_skill_script from skill_tools module
|
||||
if 'run_skill_script' not in tool_names:
|
||||
try:
|
||||
from ..tools.skill_tools import create_skill_tools
|
||||
# Create skill tools with current working directory
|
||||
skill_tools = create_skill_tools()
|
||||
self.tools.append(skill_tools.run_skill_script)
|
||||
logging.debug("Added run_skill_script tool for skill support")
|
||||
except ImportError as e:
|
||||
logging.warning(f"Could not import skill_tools: {e}")
|
||||
|
||||
def get_skills_prompt(self) -> str:
|
||||
"""Get the XML prompt for available skills.
|
||||
|
||||
@@ -1321,6 +1358,13 @@ Your Goal: {self.goal}"""
|
||||
if self.verbose:
|
||||
self._display_memory_info()
|
||||
|
||||
# Add skills prompt if skills are configured
|
||||
if self._skills or self._skills_dirs:
|
||||
skills_prompt = self.get_skills_prompt()
|
||||
if skills_prompt:
|
||||
system_prompt += f"\n\n## Available Skills\n{skills_prompt}"
|
||||
system_prompt += "\n\nWhen a skill is relevant to the task, read its SKILL.md file to get detailed instructions. If the skill has scripts in its scripts/ directory, you can execute them using the execute_code or run_script tool."
|
||||
|
||||
# Add tool usage instructions if tools are available
|
||||
# Use provided tools or fall back to self.tools
|
||||
tools_to_use = tools if tools is not None else self.tools
|
||||
|
||||
@@ -222,6 +222,14 @@ TOOL_MAPPINGS = {
|
||||
# Unified Web Search (auto-fallback across providers)
|
||||
'search_web': ('.web_search', None),
|
||||
'get_available_providers': ('.web_search', None),
|
||||
|
||||
# Skill Tools (for Agent Skills script execution)
|
||||
'run_skill_script': ('.skill_tools', None),
|
||||
'read_skill_file': ('.skill_tools', None),
|
||||
'list_skill_scripts': ('.skill_tools', None),
|
||||
'create_skill_tools': ('.skill_tools', None),
|
||||
'SkillTools': ('.skill_tools', 'SkillTools'),
|
||||
'skill_tools': ('.skill_tools', None),
|
||||
}
|
||||
|
||||
_instances = {} # Cache for class instances
|
||||
@@ -259,11 +267,12 @@ def __getattr__(name: str) -> Any:
|
||||
'exa_search_async', 'exa_search_contents_async', 'exa_answer_async',
|
||||
'crawl4ai', 'crawl4ai_many', 'crawl4ai_extract', 'crawl4ai_llm_extract',
|
||||
'crawl4ai_sync', 'crawl4ai_extract_sync',
|
||||
'search_web', 'get_available_providers'
|
||||
'search_web', 'get_available_providers',
|
||||
'run_skill_script', 'read_skill_file', 'list_skill_scripts', 'create_skill_tools'
|
||||
]:
|
||||
return getattr(module, name)
|
||||
if name in ['file_tools', 'pandas_tools', 'wikipedia_tools',
|
||||
'newspaper_tools', 'arxiv_tools', 'spider_tools', 'duckdb_tools', 'mongodb_tools', 'csv_tools', 'json_tools', 'excel_tools', 'xml_tools', 'yaml_tools', 'calculator_tools', 'python_tools', 'shell_tools', 'cot_tools', 'tavily_tools', 'youdotcom_tools', 'exa_tools', 'crawl4ai_tools']:
|
||||
'newspaper_tools', 'arxiv_tools', 'spider_tools', 'duckdb_tools', 'mongodb_tools', 'csv_tools', 'json_tools', 'excel_tools', 'xml_tools', 'yaml_tools', 'calculator_tools', 'python_tools', 'shell_tools', 'cot_tools', 'tavily_tools', 'youdotcom_tools', 'exa_tools', 'crawl4ai_tools', 'skill_tools']:
|
||||
return module # Returns the callable module
|
||||
return getattr(module, name)
|
||||
else:
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
"""Tools for executing Agent Skills scripts.
|
||||
|
||||
This module provides tools for running scripts bundled with Agent Skills.
|
||||
Skills are modular packages that extend agent capabilities through SKILL.md
|
||||
files and optional scripts.
|
||||
|
||||
Usage:
|
||||
from praisonaiagents.tools import run_skill_script
|
||||
result = run_skill_script("./my-skill/scripts/script.py", "./data.csv")
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import os
|
||||
import logging
|
||||
|
||||
|
||||
class SkillTools:
|
||||
"""Tools for executing Agent Skills scripts."""
|
||||
|
||||
def __init__(self, working_directory: str = None):
|
||||
"""Initialize SkillTools.
|
||||
|
||||
Args:
|
||||
working_directory: Base directory for resolving relative paths.
|
||||
Defaults to current working directory.
|
||||
"""
|
||||
self._working_directory = working_directory or os.getcwd()
|
||||
|
||||
@property
|
||||
def working_directory(self) -> str:
|
||||
"""Get the working directory for path resolution."""
|
||||
return self._working_directory
|
||||
|
||||
@working_directory.setter
|
||||
def working_directory(self, value: str):
|
||||
"""Set the working directory for path resolution."""
|
||||
self._working_directory = value
|
||||
|
||||
def run_skill_script(
|
||||
self,
|
||||
script_path: str,
|
||||
args: str = "",
|
||||
timeout: int = 60
|
||||
) -> str:
|
||||
"""
|
||||
Execute a skill script from a skill's scripts/ directory.
|
||||
|
||||
Args:
|
||||
script_path: Path to the script file (e.g., ./csv-analyzer/scripts/script.py)
|
||||
args: Arguments to pass to the script (e.g., file path to analyze)
|
||||
timeout: Maximum execution time in seconds (default: 60)
|
||||
|
||||
Returns:
|
||||
The output from running the script, or error message if failed
|
||||
"""
|
||||
try:
|
||||
# Resolve the script path
|
||||
script_path = os.path.expanduser(script_path)
|
||||
if not os.path.isabs(script_path):
|
||||
script_path = os.path.join(self._working_directory, script_path)
|
||||
script_path = os.path.abspath(script_path)
|
||||
|
||||
if not os.path.exists(script_path):
|
||||
return f"Error: Script not found at {script_path}"
|
||||
|
||||
# Resolve args paths relative to working directory
|
||||
resolved_args = []
|
||||
if args:
|
||||
for arg in args.split():
|
||||
# If arg looks like a file path, resolve it
|
||||
if arg.startswith('./') or arg.startswith('../') or (
|
||||
not arg.startswith('-') and '/' in arg or '.' in arg
|
||||
):
|
||||
potential_path = os.path.join(self._working_directory, arg)
|
||||
if os.path.exists(potential_path):
|
||||
resolved_args.append(os.path.abspath(potential_path))
|
||||
else:
|
||||
# Keep original arg if path doesn't exist
|
||||
resolved_args.append(arg)
|
||||
else:
|
||||
resolved_args.append(arg)
|
||||
|
||||
# Determine how to run the script based on extension
|
||||
ext = os.path.splitext(script_path)[1].lower()
|
||||
|
||||
if ext == '.py':
|
||||
cmd = ['python', script_path] + resolved_args
|
||||
elif ext == '.sh':
|
||||
cmd = ['bash', script_path] + resolved_args
|
||||
elif ext == '.js':
|
||||
cmd = ['node', script_path] + resolved_args
|
||||
else:
|
||||
# Try to run directly
|
||||
cmd = [script_path] + resolved_args
|
||||
|
||||
logging.debug(f"Running skill script: {' '.join(cmd)}")
|
||||
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
cwd=self._working_directory
|
||||
)
|
||||
|
||||
output = result.stdout
|
||||
if result.stderr:
|
||||
output += f"\nStderr: {result.stderr}"
|
||||
if result.returncode != 0:
|
||||
output += f"\nExit code: {result.returncode}"
|
||||
|
||||
return output if output else "Script executed successfully (no output)"
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
return f"Error: Script execution timed out after {timeout} seconds"
|
||||
except FileNotFoundError as e:
|
||||
return f"Error: Could not find interpreter or script: {str(e)}"
|
||||
except PermissionError as e:
|
||||
return f"Error: Permission denied: {str(e)}"
|
||||
except Exception as e:
|
||||
return f"Error executing script: {str(e)}"
|
||||
|
||||
def read_skill_file(
|
||||
self,
|
||||
skill_path: str,
|
||||
file_path: str,
|
||||
encoding: str = 'utf-8'
|
||||
) -> str:
|
||||
"""
|
||||
Read a file from a skill directory.
|
||||
|
||||
Args:
|
||||
skill_path: Path to the skill directory (e.g., ./csv-analyzer)
|
||||
file_path: Relative path within the skill (e.g., SKILL.md, scripts/script.py)
|
||||
encoding: File encoding (default: utf-8)
|
||||
|
||||
Returns:
|
||||
File contents or error message
|
||||
"""
|
||||
try:
|
||||
# Resolve skill path
|
||||
skill_path = os.path.expanduser(skill_path)
|
||||
if not os.path.isabs(skill_path):
|
||||
skill_path = os.path.join(self._working_directory, skill_path)
|
||||
skill_path = os.path.abspath(skill_path)
|
||||
|
||||
if not os.path.exists(skill_path):
|
||||
return f"Error: Skill directory not found at {skill_path}"
|
||||
|
||||
if not os.path.isdir(skill_path):
|
||||
return f"Error: {skill_path} is not a directory"
|
||||
|
||||
# Resolve file path within skill
|
||||
full_path = os.path.join(skill_path, file_path)
|
||||
full_path = os.path.abspath(full_path)
|
||||
|
||||
# Security check: ensure file is within skill directory
|
||||
if not full_path.startswith(skill_path):
|
||||
return f"Error: Path traversal detected - {file_path} is outside skill directory"
|
||||
|
||||
if not os.path.exists(full_path):
|
||||
return f"Error: File not found at {full_path}"
|
||||
|
||||
with open(full_path, 'r', encoding=encoding) as f:
|
||||
return f.read()
|
||||
|
||||
except UnicodeDecodeError as e:
|
||||
return f"Error: Could not decode file with {encoding} encoding: {str(e)}"
|
||||
except PermissionError as e:
|
||||
return f"Error: Permission denied reading file: {str(e)}"
|
||||
except Exception as e:
|
||||
return f"Error reading skill file: {str(e)}"
|
||||
|
||||
def list_skill_scripts(self, skill_path: str) -> str:
|
||||
"""
|
||||
List available scripts in a skill's scripts/ directory.
|
||||
|
||||
Args:
|
||||
skill_path: Path to the skill directory
|
||||
|
||||
Returns:
|
||||
JSON string with list of scripts or error message
|
||||
"""
|
||||
import json
|
||||
|
||||
try:
|
||||
# Resolve skill path
|
||||
skill_path = os.path.expanduser(skill_path)
|
||||
if not os.path.isabs(skill_path):
|
||||
skill_path = os.path.join(self._working_directory, skill_path)
|
||||
skill_path = os.path.abspath(skill_path)
|
||||
|
||||
if not os.path.exists(skill_path):
|
||||
return f"Error: Skill directory not found at {skill_path}"
|
||||
|
||||
scripts_dir = os.path.join(skill_path, "scripts")
|
||||
if not os.path.exists(scripts_dir):
|
||||
return json.dumps({"scripts": [], "message": "No scripts/ directory found"})
|
||||
|
||||
scripts = []
|
||||
for item in os.listdir(scripts_dir):
|
||||
item_path = os.path.join(scripts_dir, item)
|
||||
if os.path.isfile(item_path):
|
||||
ext = os.path.splitext(item)[1].lower()
|
||||
if ext in ['.py', '.sh', '.js', '.rb', '.pl']:
|
||||
scripts.append({
|
||||
"name": item,
|
||||
"path": os.path.join("scripts", item),
|
||||
"type": ext[1:] # Remove the dot
|
||||
})
|
||||
|
||||
return json.dumps({"scripts": scripts}, indent=2)
|
||||
|
||||
except Exception as e:
|
||||
return f"Error listing skill scripts: {str(e)}"
|
||||
|
||||
|
||||
# Create default instance for direct function access
|
||||
_skill_tools = SkillTools()
|
||||
|
||||
def run_skill_script(script_path: str, args: str = "", timeout: int = 60) -> str:
|
||||
"""
|
||||
Execute a skill script from a skill's scripts/ directory.
|
||||
|
||||
Args:
|
||||
script_path: Path to the script file (e.g., ./csv-analyzer/scripts/script.py)
|
||||
args: Arguments to pass to the script (e.g., file path to analyze)
|
||||
timeout: Maximum execution time in seconds (default: 60)
|
||||
|
||||
Returns:
|
||||
The output from running the script, or error message if failed
|
||||
"""
|
||||
return _skill_tools.run_skill_script(script_path, args, timeout)
|
||||
|
||||
|
||||
def read_skill_file(skill_path: str, file_path: str, encoding: str = 'utf-8') -> str:
|
||||
"""
|
||||
Read a file from a skill directory.
|
||||
|
||||
Args:
|
||||
skill_path: Path to the skill directory (e.g., ./csv-analyzer)
|
||||
file_path: Relative path within the skill (e.g., SKILL.md, scripts/script.py)
|
||||
encoding: File encoding (default: utf-8)
|
||||
|
||||
Returns:
|
||||
File contents or error message
|
||||
"""
|
||||
return _skill_tools.read_skill_file(skill_path, file_path, encoding)
|
||||
|
||||
|
||||
def list_skill_scripts(skill_path: str) -> str:
|
||||
"""
|
||||
List available scripts in a skill's scripts/ directory.
|
||||
|
||||
Args:
|
||||
skill_path: Path to the skill directory
|
||||
|
||||
Returns:
|
||||
JSON string with list of scripts or error message
|
||||
"""
|
||||
return _skill_tools.list_skill_scripts(skill_path)
|
||||
|
||||
|
||||
def create_skill_tools(working_directory: str = None) -> SkillTools:
|
||||
"""
|
||||
Create a SkillTools instance with a specific working directory.
|
||||
|
||||
This is useful when you need to resolve paths relative to a specific
|
||||
directory rather than the current working directory.
|
||||
|
||||
Args:
|
||||
working_directory: Base directory for resolving relative paths
|
||||
|
||||
Returns:
|
||||
SkillTools instance
|
||||
"""
|
||||
return SkillTools(working_directory)
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
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.1.24"
|
||||
version = "0.1.25"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
|
||||
@@ -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.3.83.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.83.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.84.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.84.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
@@ -168,6 +168,8 @@ class SkillsHandler:
|
||||
# Generate scripts/script.py if requested or AI provided it
|
||||
if generate_script or (ai_content and ai_content.get("skill_py")):
|
||||
script_content = (ai_content or {}).get("skill_py") or self._generate_template_script(name, description)
|
||||
# Strip any remaining code blocks from script content
|
||||
script_content = self._strip_code_blocks(script_content)
|
||||
(skill_dir / "scripts" / "script.py").write_text(script_content)
|
||||
if self.verbose:
|
||||
print(" - scripts/script.py")
|
||||
@@ -475,7 +477,32 @@ metadata:
|
||||
Generate detailed, practical content based on the description. Make it comprehensive but concise.
|
||||
The skill should be immediately useful for an AI agent.
|
||||
|
||||
Also, if this skill requires any Python code to function, provide a skill.py script that implements the core functionality.
|
||||
Also, if this skill requires any Python code to function, provide a script.py that implements the core functionality.
|
||||
The script MUST follow this pattern:
|
||||
1. Have a main function that accepts file path as command line argument (sys.argv[1])
|
||||
2. Print JSON output using json.dumps()
|
||||
3. Include usage message if no arguments provided
|
||||
|
||||
Example script pattern:
|
||||
```python
|
||||
import sys
|
||||
import json
|
||||
|
||||
def process_file(file_path: str) -> dict:
|
||||
# Core logic here
|
||||
return {{"result": "data"}}
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
result = process_file(sys.argv[1])
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
print("Usage: python script.py <file_path>")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
Format your response as:
|
||||
---SKILL.MD---
|
||||
[content]
|
||||
|
||||
@@ -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.3.83 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==2.3.84 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "2.3.83"
|
||||
__version__ = "2.3.84"
|
||||
@@ -12,7 +12,7 @@ dependencies = [
|
||||
"rich>=13.7",
|
||||
"markdown>=3.5",
|
||||
"pyparsing>=3.0.0",
|
||||
"praisonaiagents>=0.1.24",
|
||||
"praisonaiagents>=0.1.25",
|
||||
"python-dotenv>=0.19.0",
|
||||
"instructor>=1.3.3",
|
||||
"PyYAML>=6.0",
|
||||
|
||||
Generated
+4
-4
@@ -4197,7 +4197,7 @@ requires-dist = [
|
||||
{ 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.1.24" },
|
||||
{ name = "praisonaiagents", specifier = ">=0.1.25" },
|
||||
{ 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" },
|
||||
@@ -4252,7 +4252,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -4261,9 +4261,9 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "rich" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/31/3e/9dec08dd8107b963cdb3ea3a77b5530e7f68b239c38c80f713be27920f1b/praisonaiagents-0.1.24.tar.gz", hash = "sha256:8a6d17c2df22f2171df11b44b44b817e72a6f34f1f5b4e389691bedbbff06621", size = 678110, upload-time = "2025-12-23T05:39:18.104Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/55/77/94bcdab12d08f43ca9cc0de9a37bf7583dfb3fff72e9e0b0d220aaa208c7/praisonaiagents-0.1.25.tar.gz", hash = "sha256:4219654e73d3352ef1b7c042c51e00a6902390970b825692f23460d877e89530", size = 681211, upload-time = "2025-12-23T14:56:09.308Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/37/33/3b45e341433a01408f59ced6eb39da3ecb17067f0ff3c954fa38e07aaffb/praisonaiagents-0.1.24-py3-none-any.whl", hash = "sha256:03a1c476bb5287ad9e519b85a17a93c42ce11b01db87009fcde17a1901e4828a", size = 616792, upload-time = "2025-12-23T05:39:16.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/28/0c50fbb51cb13a5243d12a01ca0624ada486e5daee5d417b9b58054024c5/praisonaiagents-0.1.25-py3-none-any.whl", hash = "sha256:fae71e1052aa471e1e7f42e0bd21b08f6da37fd596b4809abd5ea0f1f2eb78f2", size = 620078, upload-time = "2025-12-23T14:56:07.211Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user