Compare commits

...
9 Commits
Author SHA1 Message Date
Mervin Praison 40e5c13f47 chore: update PraisonAI version to 2.2.48 in Dockerfiles, README, and related files 2025-07-11 00:23:41 +01:00
Mervin Praison 5eea991a60 Merge pull request #801 from MervinPraison/claude/issue-798-20250710_224150
fix: update AutoAgents prompt to generate proper TaskConfig objects
2025-07-11 00:19:27 +01:00
Mervin Praison efb927137f refactor: remove unused autoagents-tools.py file
- Deleted the autoagents-tools.py file as it contained redundant code and instances of AutoAgents that are no longer needed.
2025-07-11 00:17:10 +01:00
claude[bot]andMervin Praison 162223162f test: add comprehensive unit tests for AutoAgents validation and retry logic
- Add test coverage for _validate_config method
- Test successful config generation via OpenAI and LLM paths
- Test retry mechanism with validation failures
- Test edge cases like max_agents truncation and warnings
- Mock LLM responses to avoid API calls in tests

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 23:15:15 +00:00
claude[bot]andMervin Praison 35b9d46bdf feat: add validation and retry mechanism for AutoAgents config generation
- Add _validate_config method to ensure tasks are proper TaskConfig objects
- Implement retry logic with up to 3 attempts
- Include previous response and error in retry prompt for better context
- Add explicit task structure reminder on validation failures

This ensures more robust configuration generation and better error recovery
when LLMs generate incorrect task formats.

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 23:09:16 +00:00
Mervin Praison 544672edea Merge pull request #800 from MervinPraison/claude/issue-796-20250710_222402
fix: handle JSON parsing for Gemini self-reflection
2025-07-10 23:51:12 +01:00
Mervin Praison 72dcc91b3a Rename test_gemini_fix.py to gemini_self_reflection.py 2025-07-10 23:50:51 +01:00
claude[bot]andMervin Praison 75e728f7d7 fix: update AutoAgents prompt to generate proper TaskConfig objects
The validation error was occurring because the LLM was generating tasks as simple strings instead of proper TaskConfig objects. This commit updates the prompt in the _generate_config method to:

1. Include an explicit JSON schema example showing the correct structure
2. Add a clear warning that tasks must be objects with specific fields
3. Provide a concrete example of the task object structure

This ensures tasks are generated with the required fields: name, description, expected_output, and tools.

Fixes #798

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 22:44:30 +00:00
claude[bot]andMervin Praison 841a7b52c4 fix: handle JSON parsing for Gemini self-reflection
- Disable streaming for custom LLMs during self-reflection to avoid incomplete JSON chunks
- Add manual JSON parsing for custom LLM reflection responses
- Use clean_json_output method to handle markdown-wrapped JSON
- Ensure reflection regeneration also uses non-streaming for custom LLMs

This fixes the "Expecting property name enclosed in double quotes" error
when using Gemini models with self-reflection enabled.

Fixes #796

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 22:29:42 +00:00
18 changed files with 838 additions and 116 deletions

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
name: Auto Issue Comment
on:
issues:
types: [opened]
jobs:
add-comment:
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- name: Add automated comment
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude review this issue with multiple parallel running agents and do a detailed analysis and fix this if the existing code doesn\'t have the solution implemented. Making sure it has backward compatibility, no existing features removed. After making those changes, again use multi agents to review the applied changes'
})
+22
View File
@@ -0,0 +1,22 @@
name: Auto PR Comment
on:
pull_request:
types: [opened]
jobs:
add-comment:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Add automated comment
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude review this pull request with multiple parallel running agents and do a detailed analysis and fix this if the existing code doesn\'t have the solution implemented. Making sure it has backward compatibility, no existing features removed. After making those changes, again use multi agents to review the applied changes'
})
+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 \
flask \
"praisonai>=2.2.47" \
"praisonai>=2.2.48" \
"praisonai[api]" \
gunicorn \
markdown
+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.2.47" \
"praisonai>=2.2.48" \
"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.2.47" \
"praisonai>=2.2.48" \
"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.2.47" \
"praisonai>=2.2.48" \
"praisonai[ui]" \
"praisonai[crewai]"
+2 -2
View File
@@ -121,7 +121,7 @@ healthcheck:
## 📦 Package Versions
All Docker images use consistent, up-to-date versions:
- PraisonAI: `>=2.2.47`
- PraisonAI: `>=2.2.48`
- PraisonAI Agents: `>=0.0.92`
- Python: `3.11-slim`
@@ -218,7 +218,7 @@ docker-compose up -d
### Version Pinning
To use specific versions, update the Dockerfile:
```dockerfile
RUN pip install "praisonai==2.2.47" "praisonaiagents==0.0.92"
RUN pip install "praisonai==2.2.48" "praisonaiagents==0.0.92"
```
## 🌐 Production Deployment
@@ -1,10 +1,15 @@
from praisonaiagents import AutoAgents
from praisonaiagents.tools import duckduckgo
from langchain_community.utilities.tavily_search import TavilySearchAPIWrapper
def tavily(query: str):
api_wrapper = TavilySearchAPIWrapper()
results = api_wrapper.results(query=query, max_results=5)
return results
# Create AutoAgents instance
agents = AutoAgents(
instructions="Search for information about AI Agents",
tools=[duckduckgo],
tools=[tavily],
process="sequential",
verbose=True,
max_agents=3 # Maximum number of agents to create
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
"""Test script to verify the Gemini JSON parsing fix."""
from praisonaiagents import Agent
# Test with minimal configuration to isolate the issue
llm_config = {
"model": "gemini/gemini-1.5-flash-latest",
"temperature": 0.7,
"max_tokens": 500,
}
# Create agent with self-reflection enabled
agent = Agent(
instructions="You are a helpful assistant. Be concise and clear.",
llm=llm_config,
verbose=True,
self_reflect=True,
max_reflect=2,
min_reflect=1
)
# Test with a simple prompt
print("Testing Gemini with self-reflection...")
try:
response = agent.start("What is 2+2? Explain briefly.")
print(f"\nFinal response: {response}")
print("\nTest completed successfully!")
except Exception as e:
print(f"\nError occurred: {e}")
import traceback
traceback.print_exc()
@@ -1274,27 +1274,40 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
messages.append({"role": "user", "content": reflection_prompt})
try:
# Check if OpenAI client is available
if self._openai_client is None:
# For custom LLMs, self-reflection with structured output is not supported
if self.verbose:
display_self_reflection(f"Agent {self.name}: Self-reflection with structured output is not supported for custom LLM providers. Skipping reflection.", console=self.console)
# Return the original response without reflection
self.chat_history.append({"role": "user", "content": prompt})
self.chat_history.append({"role": "assistant", "content": response_text})
# Only display interaction if not using custom LLM (to avoid double output) and verbose is True
if self.verbose and not self._using_custom_llm:
display_interaction(prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time, console=self.console)
return response_text
reflection_response = self._openai_client.sync_client.beta.chat.completions.parse(
model=self.reflect_llm if self.reflect_llm else self.llm,
messages=messages,
temperature=temperature,
response_format=ReflectionOutput
)
# Check if we're using a custom LLM (like Gemini)
if self._using_custom_llm or self._openai_client is None:
# For custom LLMs, we need to handle reflection differently
# Use non-streaming to get complete JSON response
reflection_response = self._chat_completion(messages, temperature=temperature, tools=None, stream=False, reasoning_steps=False)
if not reflection_response or not reflection_response.choices:
raise Exception("No response from reflection request")
reflection_text = reflection_response.choices[0].message.content.strip()
# Clean the JSON output
cleaned_json = self.clean_json_output(reflection_text)
# Parse the JSON manually
reflection_data = json.loads(cleaned_json)
# Create a reflection output object manually
class CustomReflectionOutput:
def __init__(self, data):
self.reflection = data.get('reflection', '')
self.satisfactory = data.get('satisfactory', 'no').lower()
reflection_output = CustomReflectionOutput(reflection_data)
else:
# Use OpenAI's structured output for OpenAI models
reflection_response = self._openai_client.sync_client.beta.chat.completions.parse(
model=self.reflect_llm if self.reflect_llm else self.llm,
messages=messages,
temperature=temperature,
response_format=ReflectionOutput
)
reflection_output = reflection_response.choices[0].message.parsed
reflection_output = reflection_response.choices[0].message.parsed
if self.verbose:
display_self_reflection(f"Agent {self.name} self reflection (using {self.reflect_llm if self.reflect_llm else self.llm}): reflection='{reflection_output.reflection}' satisfactory='{reflection_output.satisfactory}'", console=self.console)
@@ -1337,7 +1350,9 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
logging.debug(f"{self.name} reflection count {reflection_count + 1}, continuing reflection process")
messages.append({"role": "user", "content": "Now regenerate your response using the reflection you made"})
response = self._chat_completion(messages, temperature=temperature, tools=None, stream=self.stream)
# For custom LLMs during reflection, always use non-streaming to ensure complete responses
use_stream = self.stream if not self._using_custom_llm else False
response = self._chat_completion(messages, temperature=temperature, tools=None, stream=use_stream)
response_text = response.choices[0].message.content.strip()
reflection_count += 1
continue # Continue the loop for more reflections
@@ -214,9 +214,37 @@ Tools: {', '.join(agent_tools)}"""
return assigned_tools
def _validate_config(self, config: AutoAgentsConfig) -> tuple[bool, str]:
"""
Validate that the configuration has proper TaskConfig objects.
Returns:
Tuple of (is_valid, error_message)
"""
for agent_idx, agent in enumerate(config.agents):
if not hasattr(agent, 'tasks') or not agent.tasks:
return False, f"Agent '{agent.name}' has no tasks defined"
for task_idx, task in enumerate(agent.tasks):
# Check if task is a proper TaskConfig instance
if not isinstance(task, TaskConfig):
return False, f"Task at index {task_idx} for agent '{agent.name}' is not a proper TaskConfig object"
# Check required fields
if not task.name:
return False, f"Task at index {task_idx} for agent '{agent.name}' has no name"
if not task.description:
return False, f"Task at index {task_idx} for agent '{agent.name}' has no description"
if not task.expected_output:
return False, f"Task at index {task_idx} for agent '{agent.name}' has no expected_output"
if task.tools is None:
return False, f"Task at index {task_idx} for agent '{agent.name}' has no tools field"
return True, ""
def _generate_config(self) -> AutoAgentsConfig:
"""Generate the configuration for agents and tasks"""
prompt = f"""
"""Generate the configuration for agents and tasks with retry logic"""
base_prompt = f"""
Generate a configuration for AI agents to accomplish this task: "{self.instructions}"
The configuration should include:
@@ -237,84 +265,164 @@ Requirements:
4. The process type should match the task requirements
5. Generate maximum {self.max_agents} agents to handle this task efficiently
Return the configuration in a structured JSON format matching the AutoAgentsConfig schema.
Return the configuration in a structured JSON format matching this exact schema:
{{
"main_instruction": "Overall goal description",
"process_type": "sequential|workflow|hierarchical",
"agents": [
{{
"name": "Agent Name",
"role": "Agent Role",
"goal": "Agent Goal",
"backstory": "Agent Backstory",
"tools": ["tool1", "tool2"],
"tasks": [
{{
"name": "Task Name",
"description": "Detailed task description",
"expected_output": "What the task should produce",
"tools": ["tool1", "tool2"]
}}
]
}}
]
}}
IMPORTANT: Each task MUST be an object with name, description, expected_output, and tools fields, NOT a simple string.
"""
try:
# Try to use OpenAI's structured output if available
use_openai_structured = False
client = None
max_retries = 3
last_response = None
last_error = None
for attempt in range(max_retries):
# Prepare prompt for this attempt
if attempt > 0 and last_response and last_error:
# On retry, include the previous response and error
prompt = f"""{base_prompt}
PREVIOUS ATTEMPT FAILED!
Your previous response was:
```json
{last_response}
```
Error: {last_error}
REMEMBER: Tasks MUST be objects with the following structure:
{{
"name": "Task Name",
"description": "Task Description",
"expected_output": "Expected Output",
"tools": ["tool1", "tool2"]
}}
DO NOT use strings for tasks. Each task MUST be a complete object with all four fields."""
else:
prompt = base_prompt
try:
# Check if we have OpenAI API and the model supports structured output
if self.llm and (self.llm.startswith('gpt-') or self.llm.startswith('o1-') or self.llm.startswith('o3-')):
# Create a new client instance if custom parameters are provided
if self.api_key or self.base_url:
client = OpenAIClient(api_key=self.api_key, base_url=self.base_url)
else:
client = get_openai_client()
use_openai_structured = True
except:
# If OpenAI client is not available, we'll use the LLM class
pass
if use_openai_structured and client:
# Use OpenAI's structured output for OpenAI models (backward compatibility)
config = client.parse_structured_output(
messages=[
{"role": "system", "content": "You are a helpful assistant designed to generate AI agent configurations."},
{"role": "user", "content": prompt}
],
response_format=AutoAgentsConfig,
model=self.llm
)
else:
# Use LLM class for all other providers (Gemini, Anthropic, etc.)
llm_instance = LLM(
model=self.llm,
base_url=self.base_url,
api_key=self.api_key
)
# Try to use OpenAI's structured output if available
use_openai_structured = False
client = None
response_text = llm_instance.response(
prompt=prompt,
system_prompt="You are a helpful assistant designed to generate AI agent configurations.",
output_pydantic=AutoAgentsConfig,
temperature=0.7,
stream=False,
verbose=False
)
# Parse the JSON response
try:
# First try to parse as is
config_dict = json.loads(response_text)
config = AutoAgentsConfig(**config_dict)
except json.JSONDecodeError:
# If that fails, try to extract JSON from the response
# Handle cases where the model might wrap JSON in markdown blocks
cleaned_response = response_text.strip()
if cleaned_response.startswith("```json"):
cleaned_response = cleaned_response[7:]
if cleaned_response.startswith("```"):
cleaned_response = cleaned_response[3:]
if cleaned_response.endswith("```"):
cleaned_response = cleaned_response[:-3]
cleaned_response = cleaned_response.strip()
# Check if we have OpenAI API and the model supports structured output
if self.llm and (self.llm.startswith('gpt-') or self.llm.startswith('o1-') or self.llm.startswith('o3-')):
# Create a new client instance if custom parameters are provided
if self.api_key or self.base_url:
client = OpenAIClient(api_key=self.api_key, base_url=self.base_url)
else:
client = get_openai_client()
use_openai_structured = True
except:
# If OpenAI client is not available, we'll use the LLM class
pass
if use_openai_structured and client:
# Use OpenAI's structured output for OpenAI models (backward compatibility)
config = client.parse_structured_output(
messages=[
{"role": "system", "content": "You are a helpful assistant designed to generate AI agent configurations."},
{"role": "user", "content": prompt}
],
response_format=AutoAgentsConfig,
model=self.llm
)
# Store the response for potential retry
last_response = json.dumps(config.model_dump(), indent=2)
else:
# Use LLM class for all other providers (Gemini, Anthropic, etc.)
llm_instance = LLM(
model=self.llm,
base_url=self.base_url,
api_key=self.api_key
)
config_dict = json.loads(cleaned_response)
config = AutoAgentsConfig(**config_dict)
# Ensure we have exactly max_agents number of agents
if len(config.agents) > self.max_agents:
config.agents = config.agents[:self.max_agents]
elif len(config.agents) < self.max_agents:
logging.warning(f"Generated {len(config.agents)} agents, expected {self.max_agents}")
return config
except Exception as e:
logging.error(f"Error generating configuration: {e}")
raise
response_text = llm_instance.response(
prompt=prompt,
system_prompt="You are a helpful assistant designed to generate AI agent configurations.",
output_pydantic=AutoAgentsConfig,
temperature=0.7,
stream=False,
verbose=False
)
# Store the raw response for potential retry
last_response = response_text
# Parse the JSON response
try:
# First try to parse as is
config_dict = json.loads(response_text)
config = AutoAgentsConfig(**config_dict)
except json.JSONDecodeError:
# If that fails, try to extract JSON from the response
# Handle cases where the model might wrap JSON in markdown blocks
cleaned_response = response_text.strip()
if cleaned_response.startswith("```json"):
cleaned_response = cleaned_response[7:]
if cleaned_response.startswith("```"):
cleaned_response = cleaned_response[3:]
if cleaned_response.endswith("```"):
cleaned_response = cleaned_response[:-3]
cleaned_response = cleaned_response.strip()
config_dict = json.loads(cleaned_response)
config = AutoAgentsConfig(**config_dict)
# Validate the configuration
is_valid, error_msg = self._validate_config(config)
if not is_valid:
last_error = error_msg
if attempt < max_retries - 1:
logging.warning(f"Configuration validation failed (attempt {attempt + 1}/{max_retries}): {error_msg}")
continue
else:
raise ValueError(f"Configuration validation failed after {max_retries} attempts: {error_msg}")
# Ensure we have exactly max_agents number of agents
if len(config.agents) > self.max_agents:
config.agents = config.agents[:self.max_agents]
elif len(config.agents) < self.max_agents:
logging.warning(f"Generated {len(config.agents)} agents, expected {self.max_agents}")
return config
except ValueError as e:
# Re-raise validation errors
raise
except Exception as e:
last_error = str(e)
if attempt < max_retries - 1:
logging.warning(f"Error generating configuration (attempt {attempt + 1}/{max_retries}): {e}")
continue
else:
logging.error(f"Error generating configuration after {max_retries} attempts: {e}")
raise
# This should never be reached due to the raise statements above
raise RuntimeError(f"Failed to generate valid configuration after {max_retries} attempts")
def _create_agents_and_tasks(self, config: AutoAgentsConfig) -> Tuple[List[Agent], List[Task]]:
"""Create agents and tasks from configuration"""
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.121"
version = "0.0.122"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
+1 -1
View File
@@ -2375,7 +2375,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.121"
version = "0.0.122"
source = { editable = "." }
dependencies = [
{ name = "mcp" },
+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.2.47.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.47.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.48.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.48.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+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.2.47 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.2.48 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+4 -4
View File
@@ -1,6 +1,6 @@
[project]
name = "PraisonAI"
version = "2.2.47"
version = "2.2.48"
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
readme = "README.md"
license = ""
@@ -12,7 +12,7 @@ dependencies = [
"rich>=13.7",
"markdown>=3.5",
"pyparsing>=3.0.0",
"praisonaiagents>=0.0.121",
"praisonaiagents>=0.0.122",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
@@ -95,7 +95,7 @@ autogen = ["pyautogen>=0.2.19", "praisonai-tools>=0.0.15", "crewai"]
[tool.poetry]
name = "PraisonAI"
version = "2.2.47"
version = "2.2.48"
description = "PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration."
authors = ["Mervin Praison"]
license = ""
@@ -113,7 +113,7 @@ python = ">=3.10,<3.13"
rich = ">=13.7"
markdown = ">=3.5"
pyparsing = ">=3.0.0"
praisonaiagents = ">=0.0.121"
praisonaiagents = ">=0.0.122"
python-dotenv = ">=0.19.0"
instructor = ">=1.3.3"
PyYAML = ">=6.0"
+518
View File
@@ -0,0 +1,518 @@
"""
Unit tests for AutoAgents class
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from praisonaiagents.agents.autoagents import AutoAgents, AutoAgentsConfig, AgentConfig, TaskConfig
from praisonaiagents.llm import OpenAIClient, LLM
import json
import logging
class TestAutoAgents:
"""Test suite for AutoAgents functionality"""
@pytest.fixture
def sample_valid_config(self):
"""Fixture providing a valid AutoAgentsConfig"""
return AutoAgentsConfig(
main_instruction="Create a blog post about AI",
process_type="sequential",
agents=[
AgentConfig(
name="Research Agent",
role="Researcher",
goal="Research AI topics",
backstory="Expert researcher",
tools=["web_search"],
tasks=[
TaskConfig(
name="Research Task",
description="Research latest AI trends",
expected_output="Research summary",
tools=["web_search"]
)
]
),
AgentConfig(
name="Writer Agent",
role="Writer",
goal="Write blog post",
backstory="Professional writer",
tools=["text_editor"],
tasks=[
TaskConfig(
name="Writing Task",
description="Write blog post based on research",
expected_output="Complete blog post",
tools=["text_editor"]
)
]
)
]
)
@pytest.fixture
def sample_invalid_config_string_tasks(self):
"""Fixture providing an invalid config with string tasks"""
return {
"main_instruction": "Create a blog post about AI",
"process_type": "sequential",
"agents": [
{
"name": "Research Agent",
"role": "Researcher",
"goal": "Research AI topics",
"backstory": "Expert researcher",
"tools": ["web_search"],
"tasks": ["Research latest AI trends"] # Invalid: string instead of object
}
]
}
@pytest.fixture
def sample_invalid_config_missing_fields(self):
"""Fixture providing an invalid config with missing task fields"""
return AutoAgentsConfig(
main_instruction="Create a blog post about AI",
process_type="sequential",
agents=[
AgentConfig(
name="Research Agent",
role="Researcher",
goal="Research AI topics",
backstory="Expert researcher",
tools=["web_search"],
tasks=[
TaskConfig(
name="Research Task",
description="", # Missing description
expected_output="Research summary",
tools=["web_search"]
)
]
)
]
)
@pytest.fixture
def mock_tools(self):
"""Fixture providing mock tools"""
web_search = Mock()
web_search.__name__ = "web_search"
text_editor = Mock()
text_editor.__name__ = "text_editor"
return [web_search, text_editor]
def test_validate_config_success(self, sample_valid_config):
"""Test successful validation of a valid configuration"""
auto_agents = AutoAgents(
instructions="Test instructions",
max_agents=3
)
is_valid, error_msg = auto_agents._validate_config(sample_valid_config)
assert is_valid is True
assert error_msg == ""
def test_validate_config_failure_not_taskconfig(self):
"""Test validation failure when task is not a TaskConfig instance"""
auto_agents = AutoAgents(
instructions="Test instructions",
max_agents=3
)
# Create a config with a non-TaskConfig task
config = AutoAgentsConfig(
main_instruction="Test",
process_type="sequential",
agents=[
AgentConfig(
name="Agent",
role="Role",
goal="Goal",
backstory="Story",
tools=[],
tasks=["This is a string task"] # This will cause validation to fail
)
]
)
# Mock the task to bypass Pydantic validation for testing
config.agents[0].tasks = ["This is a string task"]
is_valid, error_msg = auto_agents._validate_config(config)
assert is_valid is False
assert "is not a proper TaskConfig object" in error_msg
def test_validate_config_failure_missing_name(self):
"""Test validation failure when task is missing name"""
auto_agents = AutoAgents(
instructions="Test instructions",
max_agents=3
)
config = AutoAgentsConfig(
main_instruction="Test",
process_type="sequential",
agents=[
AgentConfig(
name="Agent",
role="Role",
goal="Goal",
backstory="Story",
tools=[],
tasks=[
TaskConfig(
name="", # Empty name
description="Description",
expected_output="Output",
tools=[]
)
]
)
]
)
is_valid, error_msg = auto_agents._validate_config(config)
assert is_valid is False
assert "has no name" in error_msg
def test_validate_config_failure_missing_description(self, sample_invalid_config_missing_fields):
"""Test validation failure when task is missing description"""
auto_agents = AutoAgents(
instructions="Test instructions",
max_agents=3
)
is_valid, error_msg = auto_agents._validate_config(sample_invalid_config_missing_fields)
assert is_valid is False
assert "has no description" in error_msg
@patch('praisonaiagents.agents.autoagents.get_openai_client')
def test_generate_config_openai_success(self, mock_get_client, sample_valid_config, mock_tools):
"""Test successful config generation using OpenAI structured output"""
# Mock OpenAI client
mock_client = Mock(spec=OpenAIClient)
mock_client.parse_structured_output.return_value = sample_valid_config
mock_get_client.return_value = mock_client
auto_agents = AutoAgents(
instructions="Create a blog post about AI",
tools=mock_tools,
max_agents=2,
llm="gpt-4"
)
# The config should have been generated in __init__
assert hasattr(auto_agents, 'agents')
assert len(auto_agents.agents) == 2
# Verify OpenAI client was called
mock_client.parse_structured_output.assert_called_once()
@patch('praisonaiagents.agents.autoagents.LLM')
def test_generate_config_llm_success(self, mock_llm_class, sample_valid_config, mock_tools):
"""Test successful config generation using generic LLM"""
# Mock LLM instance
mock_llm = Mock()
mock_llm.response.return_value = json.dumps(sample_valid_config.model_dump())
mock_llm_class.return_value = mock_llm
auto_agents = AutoAgents(
instructions="Create a blog post about AI",
tools=mock_tools,
max_agents=2,
llm="claude-3" # Non-OpenAI model
)
# The config should have been generated in __init__
assert hasattr(auto_agents, 'agents')
assert len(auto_agents.agents) == 2
# Verify LLM was called
mock_llm.response.assert_called_once()
@patch('praisonaiagents.agents.autoagents.LLM')
def test_generate_config_with_markdown_response(self, mock_llm_class, sample_valid_config, mock_tools):
"""Test config generation when LLM returns markdown-wrapped JSON"""
# Mock LLM instance
mock_llm = Mock()
# Return JSON wrapped in markdown code block
wrapped_response = f"```json\n{json.dumps(sample_valid_config.model_dump(), indent=2)}\n```"
mock_llm.response.return_value = wrapped_response
mock_llm_class.return_value = mock_llm
auto_agents = AutoAgents(
instructions="Create a blog post about AI",
tools=mock_tools,
max_agents=2,
llm="claude-3"
)
# The config should have been generated successfully despite markdown wrapping
assert hasattr(auto_agents, 'agents')
assert len(auto_agents.agents) == 2
@patch('praisonaiagents.agents.autoagents.get_openai_client')
def test_generate_config_retry_on_validation_failure(self, mock_get_client, sample_valid_config, mock_tools):
"""Test retry mechanism when validation fails"""
# Mock OpenAI client
mock_client = Mock(spec=OpenAIClient)
# First attempt: return invalid config with string tasks
invalid_config = AutoAgentsConfig(
main_instruction="Test",
process_type="sequential",
agents=[
AgentConfig(
name="Agent",
role="Role",
goal="Goal",
backstory="Story",
tools=[],
tasks=[] # This will be mocked to contain strings
)
]
)
# Mock the tasks to be strings (bypassing Pydantic validation)
invalid_config.agents[0].tasks = ["Invalid string task"]
# Second attempt: return valid config
mock_client.parse_structured_output.side_effect = [invalid_config, sample_valid_config]
mock_get_client.return_value = mock_client
with patch('logging.warning') as mock_warning:
auto_agents = AutoAgents(
instructions="Create a blog post about AI",
tools=mock_tools,
max_agents=2,
llm="gpt-4"
)
# Should succeed after retry
assert hasattr(auto_agents, 'agents')
assert len(auto_agents.agents) == 2
# Verify retry occurred
assert mock_client.parse_structured_output.call_count == 2
mock_warning.assert_called_once()
assert "Configuration validation failed" in str(mock_warning.call_args)
@patch('praisonaiagents.agents.autoagents.get_openai_client')
def test_generate_config_max_retries_exceeded(self, mock_get_client, mock_tools):
"""Test that max retries are properly enforced"""
# Mock OpenAI client
mock_client = Mock(spec=OpenAIClient)
# Always return invalid config
invalid_config = AutoAgentsConfig(
main_instruction="Test",
process_type="sequential",
agents=[
AgentConfig(
name="Agent",
role="Role",
goal="Goal",
backstory="Story",
tools=[],
tasks=[]
)
]
)
# Mock the tasks to be strings
invalid_config.agents[0].tasks = ["Invalid string task"]
mock_client.parse_structured_output.return_value = invalid_config
mock_get_client.return_value = mock_client
with pytest.raises(ValueError, match="Configuration validation failed after 3 attempts"):
AutoAgents(
instructions="Create a blog post about AI",
tools=mock_tools,
max_agents=2,
llm="gpt-4"
)
# Verify all retries were attempted
assert mock_client.parse_structured_output.call_count == 3
@patch('praisonaiagents.agents.autoagents.get_openai_client')
def test_max_agents_truncation(self, mock_get_client, mock_tools):
"""Test that agents are truncated when exceeding max_agents"""
# Create config with 4 agents
config_with_many_agents = AutoAgentsConfig(
main_instruction="Test",
process_type="sequential",
agents=[
AgentConfig(
name=f"Agent {i}",
role="Role",
goal="Goal",
backstory="Story",
tools=[],
tasks=[
TaskConfig(
name=f"Task {i}",
description="Description",
expected_output="Output",
tools=[]
)
]
)
for i in range(4)
]
)
mock_client = Mock(spec=OpenAIClient)
mock_client.parse_structured_output.return_value = config_with_many_agents
mock_get_client.return_value = mock_client
# Create AutoAgents with max_agents=2
auto_agents = AutoAgents(
instructions="Test",
tools=mock_tools,
max_agents=2,
llm="gpt-4"
)
# Should only have 2 agents
assert len(auto_agents.agents) == 2
assert auto_agents.agents[0].name == "Agent 0"
assert auto_agents.agents[1].name == "Agent 1"
@patch('praisonaiagents.agents.autoagents.get_openai_client')
def test_insufficient_agents_warning(self, mock_get_client, mock_tools):
"""Test warning when fewer agents than max_agents are generated"""
# Create config with only 1 agent
config_with_few_agents = AutoAgentsConfig(
main_instruction="Test",
process_type="sequential",
agents=[
AgentConfig(
name="Single Agent",
role="Role",
goal="Goal",
backstory="Story",
tools=[],
tasks=[
TaskConfig(
name="Task",
description="Description",
expected_output="Output",
tools=[]
)
]
)
]
)
mock_client = Mock(spec=OpenAIClient)
mock_client.parse_structured_output.return_value = config_with_few_agents
mock_get_client.return_value = mock_client
with patch('logging.warning') as mock_warning:
auto_agents = AutoAgents(
instructions="Test",
tools=mock_tools,
max_agents=3,
llm="gpt-4"
)
# Should have 1 agent
assert len(auto_agents.agents) == 1
# Verify warning was logged
mock_warning.assert_called()
warning_msg = str(mock_warning.call_args)
assert "Generated 1 agents, expected 3" in warning_msg
def test_max_agents_validation(self):
"""Test max_agents parameter validation"""
# Test too low
with pytest.raises(ValueError, match="max_agents must be at least 1"):
AutoAgents(instructions="Test", max_agents=0)
# Test too high
with pytest.raises(ValueError, match="max_agents cannot exceed 10"):
AutoAgents(instructions="Test", max_agents=11)
@patch('praisonaiagents.agents.autoagents.LLM')
def test_retry_with_previous_response_in_prompt(self, mock_llm_class, sample_valid_config, mock_tools):
"""Test that retry includes previous response and error in prompt"""
# Mock LLM instance
mock_llm = Mock()
# First response: invalid JSON that will fail validation
first_response = json.dumps({
"main_instruction": "Test",
"process_type": "sequential",
"agents": [{
"name": "Agent",
"role": "Role",
"goal": "Goal",
"backstory": "Story",
"tools": [],
"tasks": ["String task instead of object"]
}]
})
# Second response: valid config
second_response = json.dumps(sample_valid_config.model_dump())
mock_llm.response.side_effect = [first_response, second_response]
mock_llm_class.return_value = mock_llm
auto_agents = AutoAgents(
instructions="Create a blog post about AI",
tools=mock_tools,
max_agents=2,
llm="claude-3"
)
# Should succeed after retry
assert hasattr(auto_agents, 'agents')
# Check that the second call included the previous response
assert mock_llm.response.call_count == 2
second_call_prompt = mock_llm.response.call_args_list[1][1]['prompt']
assert "PREVIOUS ATTEMPT FAILED!" in second_call_prompt
assert first_response in second_call_prompt
assert "Error:" in second_call_prompt
@patch('praisonaiagents.agents.autoagents.OpenAIClient')
@patch('praisonaiagents.agents.autoagents.get_openai_client')
def test_custom_api_key_and_base_url(self, mock_get_client, mock_openai_class, sample_valid_config, mock_tools):
"""Test that custom API key and base URL are used correctly"""
# Mock OpenAI client instance
mock_client = Mock(spec=OpenAIClient)
mock_client.parse_structured_output.return_value = sample_valid_config
mock_openai_class.return_value = mock_client
custom_api_key = "custom-api-key"
custom_base_url = "https://custom.api.url"
auto_agents = AutoAgents(
instructions="Test",
tools=mock_tools,
max_agents=2,
llm="gpt-4",
api_key=custom_api_key,
base_url=custom_base_url
)
# Verify custom client was created with correct parameters
mock_openai_class.assert_called_once_with(
api_key=custom_api_key,
base_url=custom_base_url
)
# Verify get_openai_client was not called
mock_get_client.assert_not_called()
+5 -5
View File
@@ -3931,7 +3931,7 @@ wheels = [
[[package]]
name = "praisonai"
version = "2.2.47"
version = "2.2.48"
source = { editable = "." }
dependencies = [
{ name = "instructor" },
@@ -4073,7 +4073,7 @@ requires-dist = [
{ name = "plotly", marker = "extra == 'realtime'", specifier = ">=5.24.0" },
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.15" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.15" },
{ name = "praisonaiagents", specifier = ">=0.0.121" },
{ name = "praisonaiagents", specifier = ">=0.0.122" },
{ name = "pyautogen", marker = "extra == 'autogen'", specifier = ">=0.2.19" },
{ name = "pydantic", marker = "extra == 'chat'", specifier = "<=2.10.1" },
{ name = "pydantic", marker = "extra == 'code'", specifier = "<=2.10.1" },
@@ -4130,7 +4130,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.121"
version = "0.0.122"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mcp" },
@@ -4139,9 +4139,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a6/3f/249dc75d8d66c692a2f487286907358afc5f92a9d52ffb2fbee140213be8/praisonaiagents-0.0.121.tar.gz", hash = "sha256:ca121d455f78b30afe5c41ae785ff4b4273b4dfd058b43d326f0255471a828c2", size = 172455 }
sdist = { url = "https://files.pythonhosted.org/packages/03/4c/ca1cfce5be6faff1e1ddbe1370ecc9292a92da8d4f7cc8938d4f019449e5/praisonaiagents-0.0.122.tar.gz", hash = "sha256:82c711000c4cf4ca4b8126a7412b4cfe894fbae4944b969fdfb0ec22f2239ba1", size = 173864 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/52/2fad476c91c5ff848d844773c045a8302a9336860e6fdc5cf10636ed79f1/praisonaiagents-0.0.121-py3-none-any.whl", hash = "sha256:4ad623a2ece1ecc5dd1c02e3bba3e79847fe6129bcbafafb380670b383abf299", size = 193136 },
{ url = "https://files.pythonhosted.org/packages/4c/c4/5a5a7df835abba25ac047f0eef4b50d5534407f6a45478c5837959d3662e/praisonaiagents-0.0.122-py3-none-any.whl", hash = "sha256:a7da819f49f23fffc15c3cdc1dbbb2f2560d16775f741f8118adb5d7fd45767b", size = 194479 },
]
[[package]]