Compare commits

...
1 Commits
Author SHA1 Message Date
Mervin Praison d8e944c1d3 Release v2.5.4 2025-12-26 06:49:33 +00:00
18 changed files with 396 additions and 119 deletions

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.5.3" \
"praisonai>=2.5.4" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.5.3" \
"praisonai>=2.5.4" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.5.3" \
"praisonai>=2.5.4" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -89,11 +89,9 @@ class AccuracyEvaluator(BaseEvaluator):
EvaluationScore with score and reasoning
"""
try:
from openai import OpenAI
import litellm
except ImportError:
raise ImportError("OpenAI package required for accuracy evaluation. Install with: pip install openai")
client = OpenAI()
raise ImportError("litellm package required for accuracy evaluation. Install with: pip install litellm")
prompt = f"""You are an expert evaluator. Compare the actual output against the expected output and provide a score from 1-10.
@@ -115,7 +113,8 @@ Respond in this exact format:
SCORE: [number 1-10]
REASONING: [brief explanation]"""
response = client.chat.completions.create(
# Use litellm for unified multi-provider support
response = litellm.completion(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
@@ -99,11 +99,9 @@ class CriteriaEvaluator(BaseEvaluator):
CriteriaScore with score, passed status, and reasoning
"""
try:
from openai import OpenAI
import litellm
except ImportError:
raise ImportError("OpenAI package required for criteria evaluation. Install with: pip install openai")
client = OpenAI()
raise ImportError("litellm package required for criteria evaluation. Install with: pip install litellm")
if self.scoring_type == "binary":
prompt = f"""You are an expert evaluator. Evaluate the following output against the given criteria.
@@ -138,7 +136,8 @@ Respond in this exact format:
SCORE: [number 1-10]
REASONING: [brief explanation]"""
response = client.chat.completions.create(
# Use litellm for unified multi-provider support
response = litellm.completion(
model=self.model,
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
@@ -91,19 +91,20 @@ class MongoDBMemory:
raise Exception(f"Failed to initialize MongoDB: {e}")
def _init_embedding_model(self):
"""Initialize embedding model from config."""
"""Initialize embedding model from config using litellm for unified provider support."""
try:
# Set up embedding model based on config
embedder_config = self.config.get("embedder", {})
if embedder_config.get("provider") == "openai":
import openai
self.embedding_model = openai.OpenAI()
self.embedding_model_name = embedder_config.get("config", {}).get("model", "text-embedding-3-small")
else:
# Default to OpenAI
import openai
self.embedding_model = openai.OpenAI()
self.embedding_model_name = "text-embedding-3-small"
provider = embedder_config.get("provider", "openai")
model_name = embedder_config.get("config", {}).get("model", "text-embedding-3-small")
# Store model name for later use
self.embedding_model_name = model_name
# Use litellm for embeddings - it handles all providers uniformly
# We'll use litellm.embedding() in _get_embeddings() instead of storing a client
self.embedding_provider = provider
self.embedding_model = None # Will use litellm.embedding() directly
except Exception as e:
raise Exception(f"Failed to initialize embedding model: {e}")
@@ -172,13 +173,16 @@ class MongoDBMemory:
logging.warning(f"Could not create vector search index: {e}")
def _get_embedding(self, text):
"""Get embedding for text."""
"""Get embedding for text using litellm for unified provider support."""
try:
response = self.embedding_model.embeddings.create(
input=text,
model=self.embedding_model_name
import litellm
# Use litellm.embedding() for unified multi-provider support
response = litellm.embedding(
model=self.embedding_model_name,
input=text
)
return response.data[0].embedding
return response.data[0]["embedding"]
except Exception as e:
logging.error(f"Error getting embedding: {e}")
return None
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.3.3"
version = "0.3.4"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
@@ -91,14 +91,11 @@ class TestAccuracyEvaluator:
def test_judge_output(self):
"""Test LLM judging of output."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 8\nREASONING: Good match"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "4"
@@ -115,14 +112,11 @@ class TestAccuracyEvaluator:
def test_run_returns_accuracy_result(self):
"""Test that run returns AccuracyResult."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 9\nREASONING: Excellent"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "4"
@@ -140,14 +134,11 @@ class TestAccuracyEvaluator:
def test_evaluate_output(self):
"""Test evaluate_output method for pre-generated outputs."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 10\nREASONING: Perfect"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "unused"
@@ -325,14 +316,11 @@ class TestCriteriaEvaluator:
def test_judge_output_numeric(self):
"""Test numeric scoring."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 8\nREASONING: Good"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "test"
@@ -350,14 +338,11 @@ class TestCriteriaEvaluator:
def test_judge_output_binary(self):
"""Test binary scoring."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "RESULT: PASS\nREASONING: Meets criteria"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "test"
@@ -374,14 +359,11 @@ class TestCriteriaEvaluator:
def test_on_fail_callback(self):
"""Test on_fail callback is called."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 3\nREASONING: Poor"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
on_fail_called = []
def on_fail(score):
@@ -403,14 +385,11 @@ class TestCriteriaEvaluator:
def test_evaluate_output_criteria(self):
"""Test evaluate_output method."""
with patch('openai.OpenAI') as mock_openai_class:
mock_client = MagicMock()
mock_openai_class.return_value = mock_client
with patch('litellm.completion') as mock_completion:
mock_response = MagicMock()
mock_response.choices = [MagicMock()]
mock_response.choices[0].message.content = "SCORE: 9\nREASONING: Great"
mock_client.chat.completions.create.return_value = mock_response
mock_completion.return_value = mock_response
def test_func(x):
return "unused"
+1 -1
View File
@@ -2422,7 +2422,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.3.3"
version = "0.3.4"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.3.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.3.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.4.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.4.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+16 -32
View File
@@ -57,13 +57,10 @@ class LLMReranker:
self._client = None
def _get_client(self):
"""Lazy load OpenAI client."""
if self._client is None:
if self.llm:
return self.llm
import openai
self._client = openai.OpenAI()
return self._client
"""Get LLM client - returns self.llm if provided, otherwise None (use litellm)."""
if self.llm:
return self.llm
return None # Will use litellm directly
def rerank(
self,
@@ -118,7 +115,7 @@ class LLMReranker:
return results
def _score_document(self, query: str, document: str) -> float:
"""Score a single document's relevance to query."""
"""Score a single document's relevance to query using litellm."""
try:
client = self._get_client()
@@ -132,33 +129,20 @@ Document: {document[:1000]}
Relevance score (0-10):"""
# Check if client has a callable 'chat' method (PraisonAI Agent)
# Note: OpenAI client has 'chat' attribute but it's a namespace, not callable
if hasattr(client, 'chat') and callable(getattr(client, 'chat', None)):
if client is not None and hasattr(client, 'chat') and callable(getattr(client, 'chat', None)):
# PraisonAI Agent or similar with callable chat method
response = client.chat(prompt)
else:
# OpenAI client - use chat.completions.create
# Try max_completion_tokens first (newer models), then max_tokens (older)
# Note: Some models (e.g., gpt-5-nano) don't support temperature=0
api_error = None
for token_param in [{"max_completion_tokens": 10}, {"max_tokens": 10}]:
try:
response = client.chat.completions.create(
model=self.model,
messages=[{"role": "user", "content": prompt}],
**token_param
)
response = response.choices[0].message.content
api_error = None
break
except Exception as e:
api_error = e
err_str = str(e).lower()
if "max_completion_tokens" not in err_str and "max_tokens" not in err_str:
raise # Re-raise if not a token param error
continue
if api_error:
raise api_error
# Use litellm for unified multi-provider support
import litellm
# litellm handles model-specific quirks automatically
completion = litellm.completion(
model=self.model,
messages=[{"role": "user", "content": prompt}],
max_tokens=10
)
response = completion.choices[0].message.content
# Parse score
if response:
+4 -12
View File
@@ -79,24 +79,16 @@ def start_chat_server(
_register_agent(a)
# Start the chainlit server
import sys
from pathlib import Path
# Use the built-in app file
app_file = Path(__file__).parent / "app.py"
sys.argv = [
"chainlit",
"run",
str(app_file),
"--host", config.host,
"--port", str(config.port),
]
# Set environment variables for chainlit
os.environ["CHAINLIT_HOST"] = config.host
os.environ["CHAINLIT_PORT"] = str(config.port)
if config.debug:
sys.argv.append("-w")
run_chainlit()
run_chainlit(str(app_file))
# Global registry for agents
@@ -68,7 +68,7 @@ class KnowledgeHandler(CommandHandler):
return "knowledge"
def get_actions(self) -> List[str]:
return ["add", "query", "search", "list", "clear", "stats", "info", "help"]
return ["add", "query", "search", "list", "clear", "stats", "info", "export", "import", "help"]
def get_help_text(self) -> str:
return """
@@ -78,6 +78,8 @@ Knowledge Commands:
praisonai knowledge list - List indexed documents
praisonai knowledge clear - Clear knowledge base
praisonai knowledge stats - Show knowledge base statistics
praisonai knowledge export <file.json> - Export knowledge base to JSON file
praisonai knowledge import <file.json> - Import knowledge base from JSON file
praisonai knowledge help - Show this help message
Options:
@@ -380,6 +382,150 @@ Examples:
"""Alias for action_stats."""
return self.action_stats(args, **kwargs)
def action_export(self, args: List[str], **kwargs) -> bool:
"""
Export knowledge base to JSON file.
Exports all documents with their metadata, embeddings, and content
for backup or migration purposes.
Args:
args: List containing output file path
Returns:
True if successful
"""
import json
from datetime import datetime
if not args:
# Default filename with timestamp
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
output_file = os.path.join(self.workspace, f"knowledge_export_{timestamp}.json")
else:
output_file = args[0]
if not os.path.isabs(output_file):
output_file = os.path.join(self.workspace, output_file)
knowledge = self._get_knowledge()
if not knowledge:
return False
try:
export_data = {
"version": "1.0",
"exported_at": datetime.now().isoformat(),
"workspace": self.workspace,
"vector_store": self.vector_store,
"documents": []
}
# Try to get all documents from knowledge/memory
if hasattr(knowledge, 'memory') and hasattr(knowledge.memory, 'get_all'):
all_docs = knowledge.memory.get_all()
if all_docs:
for doc in all_docs:
if isinstance(doc, dict):
export_data["documents"].append(doc)
else:
export_data["documents"].append({
"content": str(doc),
"metadata": {}
})
elif hasattr(knowledge, 'get_all'):
all_docs = knowledge.get_all()
if all_docs:
for doc in all_docs:
if isinstance(doc, dict):
export_data["documents"].append(doc)
else:
export_data["documents"].append({
"content": str(doc),
"metadata": {}
})
# Ensure output directory exists
os.makedirs(os.path.dirname(output_file) or ".", exist_ok=True)
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(export_data, f, indent=2, default=str)
doc_count = len(export_data["documents"])
self.print_status(f"✅ Exported {doc_count} documents to {output_file}", "success")
return True
except Exception as e:
self.print_status(f"Export failed: {e}", "error")
return False
def action_import(self, args: List[str], **kwargs) -> bool:
"""
Import knowledge base from JSON file.
Imports documents from a previously exported JSON file.
Args:
args: List containing input file path
Returns:
True if successful
"""
import json
if not args:
self.print_status("Usage: praisonai knowledge import <file.json>", "error")
return False
input_file = args[0]
if not os.path.isabs(input_file):
input_file = os.path.join(self.workspace, input_file)
if not os.path.exists(input_file):
self.print_status(f"File not found: {input_file}", "error")
return False
knowledge = self._get_knowledge()
if not knowledge:
return False
try:
with open(input_file, 'r', encoding='utf-8') as f:
import_data = json.load(f)
# version field reserved for future schema migrations
_ = import_data.get("version", "unknown")
documents = import_data.get("documents", [])
if not documents:
self.print_status("No documents found in import file", "warning")
return True
imported_count = 0
for doc in documents:
try:
content = doc.get("content") or doc.get("memory") or doc.get("text", "")
metadata = doc.get("metadata", {})
if content:
if hasattr(knowledge, 'store'):
knowledge.store(content, metadata=metadata)
elif hasattr(knowledge, 'add'):
knowledge.add(content, metadata=metadata)
imported_count += 1
except Exception as e:
if self.verbose:
self.print_status(f"Skipped document: {e}", "warning")
self.print_status(f"✅ Imported {imported_count} documents from {input_file}", "success")
return True
except json.JSONDecodeError as e:
self.print_status(f"Invalid JSON file: {e}", "error")
return False
except Exception as e:
self.print_status(f"Import failed: {e}", "error")
return False
def execute(self, action: str, action_args: List[str], **kwargs) -> Any:
"""Execute knowledge command action."""
return super().execute(action, action_args, **kwargs)
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.5.3 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.5.4 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.5.3"
__version__ = "2.5.4"
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.3.3",
"praisonaiagents>=0.3.4",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
@@ -266,3 +266,177 @@ class TestKnowledgeHandlerEdgeCases:
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
results = handler.action_query(["test"])
assert results == []
class TestKnowledgeHandlerExportImport:
"""Tests for export/import functionality."""
def test_get_actions_includes_export_import(self):
"""Test that export and import are in available actions."""
handler = KnowledgeHandler()
actions = handler.get_actions()
assert "export" in actions
assert "import" in actions
def test_action_export_default_filename(self):
"""Test export with default filename."""
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
mock_knowledge.get_all.return_value = [
{"content": "doc1", "metadata": {}},
{"content": "doc2", "metadata": {}}
]
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_export([])
assert result is True
# Check that file was created
files = os.listdir(tmpdir)
export_files = [f for f in files if f.startswith("knowledge_export_")]
assert len(export_files) == 1
def test_action_export_custom_filename(self):
"""Test export with custom filename."""
import json
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
# Set up memory.get_all since that's checked first
mock_knowledge.memory = MagicMock()
mock_knowledge.memory.get_all.return_value = [
{"content": "test content", "metadata": {"source": "test"}}
]
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
output_file = os.path.join(tmpdir, "my_export.json")
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_export([output_file])
assert result is True
assert os.path.exists(output_file)
with open(output_file, 'r') as f:
data = json.load(f)
assert data["version"] == "1.0"
assert len(data["documents"]) == 1
def test_action_export_empty_knowledge(self):
"""Test export with empty knowledge base."""
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
mock_knowledge.get_all.return_value = []
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
output_file = os.path.join(tmpdir, "empty_export.json")
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_export([output_file])
assert result is True
def test_action_import_no_args(self):
"""Test import with no arguments."""
handler = KnowledgeHandler()
result = handler.action_import([])
assert result is False
def test_action_import_nonexistent_file(self):
"""Test import with non-existent file."""
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_import(["/nonexistent/file.json"])
assert result is False
def test_action_import_valid_file(self):
"""Test import with valid JSON file."""
import json
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
input_file = os.path.join(tmpdir, "import_test.json")
# Create test import file
import_data = {
"version": "1.0",
"documents": [
{"content": "doc1", "metadata": {}},
{"content": "doc2", "metadata": {"key": "value"}}
]
}
with open(input_file, 'w') as f:
json.dump(import_data, f)
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_import([input_file])
assert result is True
assert mock_knowledge.store.call_count == 2 or mock_knowledge.add.call_count == 2
def test_action_import_empty_documents(self):
"""Test import with empty documents list."""
import json
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
input_file = os.path.join(tmpdir, "empty_import.json")
import_data = {"version": "1.0", "documents": []}
with open(input_file, 'w') as f:
json.dump(import_data, f)
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_import([input_file])
assert result is True
def test_action_import_invalid_json(self):
"""Test import with invalid JSON file."""
handler = KnowledgeHandler()
mock_knowledge = MagicMock()
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
input_file = os.path.join(tmpdir, "invalid.json")
with open(input_file, 'w') as f:
f.write("not valid json {{{")
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge):
result = handler.action_import([input_file])
assert result is False
def test_export_import_roundtrip(self):
"""Test that export and import work together."""
handler = KnowledgeHandler()
with tempfile.TemporaryDirectory() as tmpdir:
handler.workspace = tmpdir
export_file = os.path.join(tmpdir, "roundtrip.json")
# Mock for export - set up memory.get_all since that's checked first
mock_knowledge_export = MagicMock()
mock_knowledge_export.memory = MagicMock()
mock_knowledge_export.memory.get_all.return_value = [
{"content": "test doc 1", "metadata": {"id": "1"}},
{"content": "test doc 2", "metadata": {"id": "2"}}
]
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge_export):
export_result = handler.action_export([export_file])
assert export_result is True
# Mock for import
mock_knowledge_import = MagicMock()
with patch.object(handler, '_get_knowledge', return_value=mock_knowledge_import):
import_result = handler.action_import([export_file])
assert import_result is True
# Verify documents were imported
assert mock_knowledge_import.store.call_count == 2 or mock_knowledge_import.add.call_count == 2
+4 -4
View File
@@ -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.3.3" },
{ name = "praisonaiagents", specifier = ">=0.3.4" },
{ 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.3.3"
version = "0.3.4"
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/85/c2/04d0415e75c53a066ef42861c4699c0502885ab41aebe70e02f098075a83/praisonaiagents-0.3.3.tar.gz", hash = "sha256:82a6941e4bb7178ced495e4e0ae595a86e5e808b0998a96c89ebc45ce25d68db", size = 757530, upload-time = "2025-12-26T05:53:45.798Z" }
sdist = { url = "https://files.pythonhosted.org/packages/83/87/512452be38f96bb22d4bc734d3e1e724d2a07c0da5c7bb16e18934227790/praisonaiagents-0.3.4.tar.gz", hash = "sha256:f34e7719bbac7b31aed2f677a7306fa6412d51eab8043204bef48a99c9d5998d", size = 757654, upload-time = "2025-12-26T06:49:01.069Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/90/d9c035ebfcd341aef2f47dbc393feb902f817dd781333f5436da38449087/praisonaiagents-0.3.3-py3-none-any.whl", hash = "sha256:4dd787c9b77d159b13a2fd9043ddf21c8b16e69a708ea315e42c21e998a0d921", size = 708130, upload-time = "2025-12-26T05:53:43.826Z" },
{ url = "https://files.pythonhosted.org/packages/02/e1/49671d346264bcd208921a345e6c38e8dcd7a2dad9ad0d049f3355b98d57/praisonaiagents-0.3.4-py3-none-any.whl", hash = "sha256:9d345c001ae7ef5e18dd1c66313231337b37abb1ecb45fa6dac17c1e7ed43247", size = 708253, upload-time = "2025-12-26T06:48:59.068Z" },
]
[[package]]