mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-09 03:39:37 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8e944c1d3 | ||
|
|
79b2b88f72 | ||
|
|
f5dfbd5670 | ||
|
|
aa6aa5933d |
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.5.2" \
|
||||
"praisonai>=2.5.4" \
|
||||
"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.5.2" \
|
||||
"praisonai>=2.5.4" \
|
||||
"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.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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+1
-1
@@ -2422,7 +2422,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.3.2"
|
||||
version = "0.3.4"
|
||||
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.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]"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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 @@
|
||||
__version__ = "2.5.2"
|
||||
__version__ = "2.5.4"
|
||||
@@ -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
|
||||
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.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]]
|
||||
|
||||
Reference in new issue
Block a user