Compare commits

...
4 Commits
Author SHA1 Message Date
Mervin Praison d8e944c1d3 Release v2.5.4 2025-12-26 06:49:33 +00:00
Mervin Praison 79b2b88f72 Release v2.5.3 2025-12-26 05:54:09 +00:00
Mervin Praison f5dfbd5670 fix: resolve Core Tests failures in knowledge/reranker integration
- Fix #1: MemoryConfig Pydantic validation - add _prepare_mem0_config() to strip
  PraisonAI-specific reranker fields (enabled, default_rerank) before passing to
  mem0, which only accepts provider and config fields

- Fix #2: LLM Reranker 'Chat' object not callable - check if client.chat is
  callable, not just if it exists. OpenAI client has chat as namespace object.
  Also handle max_completion_tokens vs max_tokens for different model versions.

- Fix #3: Chroma vector store metadata validation - ensure non-empty metadata
  dicts by adding placeholder key when metadata is empty or None

- Fix test file issues: add user_id parameter (required by mem0 API), handle
  dict return type from knowledge.search, use gpt-4o-mini for reranker test
  (gpt-5-nano returns empty responses with max_completion_tokens=10)
2025-12-26 05:52:46 +00:00
Mervin Praison aa6aa5933d fix: update test files to use gpt-5-nano for cheaper CI runs
- Update test_workflow_patterns.py to use gpt-5-nano instead of gpt-4o/gpt-4o-mini
- Also set OPENAI_MODEL_NAME=gpt-5-nano as CI secret
2025-12-26 05:26:40 +00:00
21 changed files with 459 additions and 124 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.2" \
"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.2" \
"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.2" \
"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
@@ -500,6 +504,33 @@ class Knowledge:
base_config["graph_store"] = self._config["graph_store"]
return base_config
def _prepare_mem0_config(self, config):
"""Prepare config for mem0 by removing PraisonAI-specific fields.
mem0's RerankerConfig only accepts 'provider' and 'config' fields.
PraisonAI adds 'enabled' and 'default_rerank' for internal use.
"""
mem0_config = config.copy()
# Strip PraisonAI-specific reranker fields that mem0 doesn't accept
if "reranker" in mem0_config:
reranker = mem0_config["reranker"]
if isinstance(reranker, dict):
# Keep only mem0-compatible fields: provider, config
mem0_reranker = {}
if "provider" in reranker:
mem0_reranker["provider"] = reranker["provider"]
if "config" in reranker:
mem0_reranker["config"] = reranker["config"]
# If no valid mem0 fields, remove reranker entirely
if mem0_reranker:
mem0_config["reranker"] = mem0_reranker
else:
del mem0_config["reranker"]
return mem0_config
@cached_property
def memory(self):
# Check if MongoDB provider is specified
@@ -511,19 +542,22 @@ class Knowledge:
# Fall back to default memory
pass
# Prepare config for mem0 (strip PraisonAI-specific fields)
mem0_config = self._prepare_mem0_config(self.config)
# Default Mem0 memory
try:
return CustomMemory.from_config(self.config)
return CustomMemory.from_config(mem0_config)
except (NotImplementedError, ValueError) as e:
if "list_collections" in str(e) or "Extra fields not allowed" in str(e):
# Keep only allowed fields
vector_store_config = {
"collection_name": self.config["vector_store"]["config"]["collection_name"],
"path": self.config["vector_store"]["config"]["path"]
"collection_name": mem0_config["vector_store"]["config"]["collection_name"],
"path": mem0_config["vector_store"]["config"]["path"]
}
self.config["vector_store"]["config"] = vector_store_config
mem0_config["vector_store"]["config"] = vector_store_config
from mem0 import Memory
return Memory.from_config(self.config)
return Memory.from_config(mem0_config)
raise
@cached_property
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.3.2"
version = "0.3.4"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
readme = "README.md"
requires-python = ">=3.10"
@@ -1189,16 +1189,16 @@ class TestWorkflowConfiguration:
"""Test that workflow planning setting is stored."""
def step1(ctx): return StepResult(output="Done")
workflow = Workflow(steps=[step1], planning=True, planning_llm="gpt-4o")
workflow = Workflow(steps=[step1], planning=True, planning_llm="gpt-5-nano")
assert workflow.planning == True
assert workflow.planning_llm == "gpt-4o"
assert workflow.planning_llm == "gpt-5-nano"
def test_workflow_default_llm(self):
"""Test default LLM setting."""
def step1(ctx): return StepResult(output="Done")
workflow = Workflow(steps=[step1], default_llm="gpt-4o-mini")
assert workflow.default_llm == "gpt-4o-mini"
workflow = Workflow(steps=[step1], default_llm="gpt-5-nano")
assert workflow.default_llm == "gpt-5-nano"
def test_workflow_default_agent_config(self):
"""Test default agent config."""
@@ -1393,10 +1393,10 @@ class TestPlanningAndReasoning:
workflow = Workflow(
steps=[lambda ctx: StepResult(output="test")],
planning=True,
planning_llm="gpt-4o"
planning_llm="gpt-5-nano"
)
assert workflow.planning == True
assert workflow.planning_llm == "gpt-4o"
assert workflow.planning_llm == "gpt-5-nano"
def test_workflow_reasoning_parameter_exists(self):
"""Test that Workflow accepts reasoning parameter."""
@@ -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.2"
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.2.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.5.2.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]"
+15 -15
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()
@@ -131,18 +128,21 @@ Document: {document[:1000]}
Relevance score (0-10):"""
if hasattr(client, 'chat'):
# PraisonAI Agent or similar
# Check if client has a callable 'chat' method (PraisonAI Agent)
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
response = client.chat.completions.create(
# 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,
temperature=0
max_tokens=10
)
response = response.choices[0].message.content
response = completion.choices[0].message.content
# Parse score
if response:
@@ -107,7 +107,17 @@ class ChromaVectorStore:
) -> List[str]:
"""Add vectors to ChromaDB."""
ids = ids or [str(uuid_module.uuid4()) for _ in texts]
metadatas = metadatas or [{} for _ in texts]
# ChromaDB requires non-empty metadata dicts
# Ensure each metadata dict has at least one key
if metadatas is None:
metadatas = [{"_source": "praisonai"} for _ in texts]
else:
# Ensure no empty dicts - add placeholder if empty
metadatas = [
m if m else {"_source": "praisonai"}
for m in metadatas
]
# Use different collection if namespace specified
if namespace and namespace != self.namespace:
+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.2 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.2"
__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.2",
"praisonaiagents>=0.3.4",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
@@ -59,12 +59,12 @@ class TestKnowledgeIntegration:
}
})
# Add document
result = knowledge.add(test_file)
# Add document (mem0 requires user_id, agent_id, or run_id)
result = knowledge.add(test_file, user_id="test_user")
assert result is not None
# Search
results = knowledge.search("Who created Python?")
results = knowledge.search("Who created Python?", user_id="test_user")
assert len(results) > 0
@requires_openai
@@ -83,15 +83,17 @@ class TestKnowledgeIntegration:
}
})
# Store text
knowledge.store("The capital of France is Paris.")
knowledge.store("The capital of Germany is Berlin.")
# Store text (mem0 requires user_id, agent_id, or run_id)
knowledge.store("The capital of France is Paris.", user_id="test_user")
knowledge.store("The capital of Germany is Berlin.", user_id="test_user")
# Search
results = knowledge.search("What is the capital of France?")
assert len(results) > 0
results = knowledge.search("What is the capital of France?", user_id="test_user")
# Results is a dict with 'results' key containing list of memories
result_list = results.get("results", results) if isinstance(results, dict) else results
assert len(result_list) > 0
# Check that Paris is mentioned in results
result_text = str(results[0]).lower()
result_text = str(result_list[0]).lower()
assert "paris" in result_text or "france" in result_text
@@ -187,7 +189,7 @@ class TestRerankerIntegration:
except ImportError:
pytest.skip("Reranker not available")
reranker = LLMReranker(model="gpt-5-nano")
reranker = LLMReranker(model="gpt-4o-mini")
documents = [
"Python is a programming language.",
@@ -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.2" },
{ 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.2"
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/30/8a/a33381c4f69e4ca1b639ad6b32d516c1adcd1d521ed78c8cdb2a947249fc/praisonaiagents-0.3.2.tar.gz", hash = "sha256:569fed4d865491cb67e7f3b6e2a603902f15bbeea1e104eb1691f634280b3155", size = 757144, upload-time = "2025-12-26T05:20:13.557Z" }
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/e4/30/57526915881e5a9d1c3c24f6369c055df93613e4e5f64db6a3fc32579ce2/praisonaiagents-0.3.2-py3-none-any.whl", hash = "sha256:50f7b6aa0a825b16f001e7a09b7eb120b89f36b010743eca6b1ec6322d9662ca", size = 707842, upload-time = "2025-12-26T05:20:11.181Z" },
{ 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]]