Compare commits

...
35 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
Mervin Praison 0213dea12b chore: update PraisonAI version to 2.2.47 in Dockerfiles, README, and related files 2025-07-10 23:30:27 +01: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
Mervin Praison c0d495fd48 Merge pull request #781 from MervinPraison/claude/issue-754-20250709_155906
refactor: eliminate sync/async duplication in llm.py
2025-07-10 23:21:04 +01:00
Mervin Praison 0e51ed0d6c Merge branch 'main' of https://github.com/MervinPraison/PraisonAI 2025-07-10 23:20:50 +01:00
Mervin Praison 32dce14f96 chore: update version to 0.0.120 in pyproject.toml and uv.lock 2025-07-10 23:20:48 +01:00
Mervin Praison 27aaac80c6 Merge branch 'main' into claude/issue-754-20250709_155906 2025-07-10 23:20:17 +01:00
Mervin Praison 5a0a3983e4 Merge pull request #794 from MervinPraison/claude/pr-791-20250710_214815
PR #791: Changes from Claude
2025-07-10 23:19:03 +01:00
claude[bot]andMervin Praison 400a4c934e fix: properly pass base_url and api_key parameters to OpenAI client and agents
- Create new OpenAIClient instance when custom base_url or api_key are provided
- Pass base_url and api_key to Agent instances for consistency
- Maintain backward compatibility by falling back to singleton client when no custom parameters

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 22:13:29 +00:00
Mervin Praison e88b0fd47b Merge pull request #793 from MervinPraison/claude/issue-754-20250709_155933
refactor: extract debug logging into centralized method
2025-07-10 23:11:57 +01:00
claude[bot]andMervin Praison e6c1c0679f fix: address review comments for debug logging refactor
- Restore backward compatibility for verbose parameter (verbose >= 10 now works)
- Fix condition ordering bug (check "parameters" before "_async")
- Fix API key masking for empty strings
- Add type check for system_prompt truncation
- Add iterable check for tools parameter
- Restore original prompt logging behavior for non-string prompts

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 22:03:01 +00:00
claude[bot]andMervin Praison 108aee0a54 fix: remove duplicate _handle_ollama_model method and fix double tool call processing
- Remove duplicate _handle_ollama_model method definition
- Fix bug where tool calls were processed twice in verbose streaming
- Keep the method with better type hints (Union[str, List[Dict]])
- Remove redundant _process_tool_calls_from_stream calls after _process_stream_delta

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 22:02:58 +00:00
Mervin Praison cb017aad70 Merge branch 'main' into claude/issue-754-20250709_155906 2025-07-10 22:53:33 +01:00
claude[bot]andMervin Praison ea29d3181b fix: properly handle OpenAI client API and store base_url/api_key
- Use OpenAIClient's parse_structured_output method instead of accessing client.beta directly
- Store base_url and api_key as instance variables to support custom endpoints
- This fixes the AttributeError when using AutoAgents with non-OpenAI providers

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 21:50:39 +00:00
claude[bot]andMervin Praison 7f0f265671 refactor: eliminate sync/async duplication in llm.py
- Extract common logic into shared helper methods
- Fix variable reference errors for ollama_params
- Reduce code duplication between get_response/get_response_async
- Reduce code duplication between response/aresponse
- Maintain 100% backward compatibility
- No API changes or breaking changes

Fixes #754

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 21:42:47 +00:00
Mervin Praison bc1c508419 Merge pull request #789 from MervinPraison/claude/issue-787-20250710_205703
fix: prevent double output when using litellm models
2025-07-10 22:37:16 +01:00
Mervin Praison bd108ba3e7 Merge pull request #791 from MervinPraison/claude/issue-788-20250710_210226
fix: add multi-provider support for AutoAgents
2025-07-10 22:34:58 +01:00
claude[bot]andMervin Praison ac5f2649df refactor: resolve merge conflicts with main branch
- Renamed _prepare_ollama_followup to _handle_ollama_model to match main branch
- Updated method signature to (response_text, tool_results, messages, original_prompt)
- Modified return value to dict with follow_up_messages and original_prompt
- Updated all call sites to use the new interface
- Maintained backward compatibility and refactoring benefits

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 21:34:28 +00:00
claude[bot]andMervin Praison 1b64762d54 fix: add verbose checks to all display_interaction calls for consistency
- Applied consistent pattern to all 4 display_interaction calls
- Now all checks follow pattern: if self.verbose and not self._using_custom_llm
- Ensures display is only shown when verbose mode is enabled AND not using custom LLM
- Maintains backward compatibility while fixing the inconsistency

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 21:29:45 +00:00
claude[bot]andMervin Praison df402bcfdb fix: correct indentation error in aresponse method
Fixed incorrect indentation in the async Ollama follow-up handling code
that was causing a syntax error at line 1364.

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 21:20:39 +00:00
claude[bot]andMervin Praison 061c8d58b4 fix: add multi-provider support for AutoAgents
- Replace OpenAI-specific beta.chat.completions.parse() with cross-provider LLM class
- Maintain backward compatibility by using structured output for OpenAI models
- Add support for all LiteLLM providers (Gemini, Anthropic, etc.)
- Include robust JSON parsing to handle markdown-wrapped responses
- Fixes #788: AttributeError when using AutoAgent with Gemini provider

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 21:18:52 +00:00
Mervin Praison 117c02cf30 chore: update PraisonAI and related package versions to 2.2.46 and 0.0.119
- Updated PraisonAI version in Dockerfiles and README to 2.2.46
- Updated version in pyproject.toml and uv.lock for PraisonAI and PraisonAI agents to 0.0.119
- Adjusted deployment script to reflect the new PraisonAI version
2025-07-10 22:16:47 +01:00
claude[bot]andMervin Praison 82c9ff80a7 fix: correct indentation error in get_response method
Fixed incorrect indentation in lines 779-804 of the get_response method
that was causing IndentationError. The code block for Ollama follow-up
handling is now properly nested within the if follow_up_prompt block.

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 21:15:53 +00:00
Mervin Praison f44b15d73b Merge pull request #782 from MervinPraison/claude/issue-754-20250709_155935
refactor: extract streaming chunk processor to eliminate code duplication
2025-07-10 22:13:29 +01:00
claude[bot]andMervin Praison f2c89c2012 fix: prevent double output when using litellm models
- Check for self._using_custom_llm before calling display_interaction
- Custom LLM already handles display in its get_response method
- Fixes issue #787 where task/response printed twice with litellm

Co-authored-by: Mervin Praison <[email protected]>
2025-07-10 21:12:41 +00:00
claude[bot]andMervin Praison f3f5f63b22 refactor: eliminate sync/async duplication in llm.py
- Extract common logic into shared helper methods
- Reduce code duplication between get_response/get_response_async
- Reduce code duplication between response/aresponse
- Maintain 100% backward compatibility
- No API changes or breaking changes

Addresses issue #754

Co-authored-by: Mervin Praison <[email protected]>
2025-07-09 16:07:51 +00:00
claude[bot]andMervin Praison ccf9470f41 refactor: extract debug logging into centralized _log_llm_config method
- Created private _log_llm_config method to handle all debug logging
- Eliminated ~116 lines of duplicated debug logging code
- Maintains 100% backward compatibility (no API changes)
- Centralizes security measures (API key masking)
- Consistent formatting across all logging calls
- No functional changes, only code organization

Addresses code duplication issue #754

Co-authored-by: Mervin Praison <[email protected]>
2025-07-09 16:05:59 +00:00
claude[bot]andMervin Praison d228a742b7 refactor: extract streaming chunk processor to eliminate code duplication
- Add _process_stream_delta() helper method to consolidate duplicate logic
- Refactor 4 duplicated streaming sections (sync/async, verbose/non-verbose)
- Eliminates ~50 lines of duplicated code
- Maintains 100% backward compatibility with no API changes
- Addresses original issue #754 about streaming code duplication

Co-authored-by: Mervin Praison <[email protected]>
2025-07-09 16:03:24 +00:00
22 changed files with 1311 additions and 402 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.45" \
"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.45" \
"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.45" \
"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.45" \
"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.45`
- 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.45" "praisonaiagents==0.0.92"
RUN pip install "praisonai==2.2.48" "praisonaiagents==0.0.92"
```
## 🌐 Production Deployment
+62
View File
@@ -0,0 +1,62 @@
from praisonaiagents import AutoAgents
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=[tavily],
process="sequential",
verbose=True,
max_agents=3 # Maximum number of agents to create
)
# Start the agents
result = agents.start()
print(result)
##or
from praisonaiagents import AutoAgents
from praisonaiagents.tools import (
evaluate, solve_equation, convert_units,
calculate_statistics, calculate_financial
)
def get_stock_price(company_name: str) -> str:
"""
Get the stock price of a company
Args:
company_name (str): The name of the company
Returns:
str: The stock price of the company
"""
if company_name.lower() == "apple" or company_name.lower() == "aapl":
return f"The stock price of {company_name} is 100"
elif company_name.lower() == "google" or company_name.lower() == "googl":
return f"The stock price of {company_name} is 200"
else:
return f"The stock price of {company_name} is 50"
# Create AutoAgents instance
agents = AutoAgents(
instructions="Get the stock price of google and compare it to the stock price of apple",
tools=[evaluate, get_stock_price],
process="sequential",
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
self_reflect=False,
verbose=False,
max_agents=3 # Maximum number of agents to create
)
# Start the agents
result = agents.start()
print(result)
@@ -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()
+6
View File
@@ -0,0 +1,6 @@
from praisonaiagents import Agent, Agents, Tools
research_agent = Agent(name="Research", instructions="You are a research agent to search internet about AI 2024", tools=[Tools.internet_search])
summarise_agent = Agent(name="Summarise", instructions="You are a summarize agent to summarise in points")
agents = Agents(agents=[research_agent, summarise_agent])
agents.launch(path="/agents", port=3030)
@@ -1233,7 +1233,8 @@ Your Goal: {self.goal}
# Add to chat history and return raw response
self.chat_history.append({"role": "user", "content": original_prompt})
self.chat_history.append({"role": "assistant", "content": response_text})
if self.verbose:
# 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(original_prompt, response_text, markdown=self.markdown,
generation_time=time.time() - start_time, console=self.console)
return response_text
@@ -1243,7 +1244,9 @@ Your Goal: {self.goal}
self.chat_history.append({"role": "assistant", "content": response_text})
if self.verbose:
logging.debug(f"Agent {self.name} final response: {response_text}")
display_interaction(original_prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time, console=self.console)
# 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(original_prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time, console=self.console)
# Return only reasoning content if reasoning_steps is True
if reasoning_steps and hasattr(response.choices[0].message, 'reasoning_content'):
# Apply guardrail to reasoning content
@@ -1271,25 +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})
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)
@@ -1302,7 +1320,9 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
display_self_reflection("Agent marked the response as satisfactory after meeting minimum reflections", console=self.console)
self.chat_history.append({"role": "user", "content": prompt})
self.chat_history.append({"role": "assistant", "content": response_text})
display_interaction(prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time, console=self.console)
# 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)
# Apply guardrail validation after satisfactory reflection
try:
validated_response = self._apply_guardrail_with_retry(response_text, prompt, temperature, tools)
@@ -1317,7 +1337,9 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
display_self_reflection("Maximum reflection count reached, returning current response", console=self.console)
self.chat_history.append({"role": "user", "content": prompt})
self.chat_history.append({"role": "assistant", "content": response_text})
display_interaction(prompt, response_text, markdown=self.markdown, generation_time=time.time() - start_time, console=self.console)
# 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)
# Apply guardrail validation after max reflections
try:
validated_response = self._apply_guardrail_with_retry(response_text, prompt, temperature, tools)
@@ -1328,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
@@ -13,7 +13,8 @@ import logging
import os
from pydantic import BaseModel, ConfigDict
from ..main import display_instruction, display_tool_call, display_interaction
from ..llm import get_openai_client
from ..llm import get_openai_client, LLM, OpenAIClient
import json
# Define Pydantic models for structured output
class TaskConfig(BaseModel):
@@ -108,6 +109,8 @@ class AutoAgents(PraisonAIAgents):
self.max_execution_time = max_execution_time
self.max_iter = max_iter
self.reflect_llm = reflect_llm
self.base_url = base_url
self.api_key = api_key
# Display initial instruction
if self.verbose:
@@ -211,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:
@@ -234,40 +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:
# Get OpenAI client
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:
client = get_openai_client()
except ValueError as e:
# AutoAgents requires OpenAI for structured output generation
raise ValueError(
"AutoAgents requires OpenAI API for automatic agent generation. "
"Please set OPENAI_API_KEY environment variable or use PraisonAIAgents class directly "
"with manually configured agents for non-OpenAI providers."
) from e
# Try to use OpenAI's structured output if available
use_openai_structured = False
client = None
response = client.beta.chat.completions.parse(
model=self.llm,
response_format=AutoAgentsConfig,
messages=[
{"role": "system", "content": "You are a helpful assistant designed to generate AI agent configurations."},
{"role": "user", "content": prompt}
]
)
# Ensure we have exactly max_agents number of agents
if len(response.choices[0].message.parsed.agents) > self.max_agents:
response.choices[0].message.parsed.agents = response.choices[0].message.parsed.agents[:self.max_agents]
elif len(response.choices[0].message.parsed.agents) < self.max_agents:
logging.warning(f"Generated {len(response.choices[0].message.parsed.agents)} agents, expected {self.max_agents}")
return response.choices[0].message.parsed
except Exception as e:
logging.error(f"Error generating configuration: {e}")
raise
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
)
# 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
)
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"""
@@ -308,7 +463,9 @@ Return the configuration in a structured JSON format matching the AutoAgentsConf
max_rpm=self.max_rpm,
max_execution_time=self.max_execution_time,
max_iter=self.max_iter,
reflect_llm=self.reflect_llm
reflect_llm=self.reflect_llm,
base_url=self.base_url,
api_key=self.api_key
)
agents.append(agent)
+379 -325
View File
@@ -87,6 +87,65 @@ class LLM:
"llama-3.2-90b-text-preview": 6144 # 8,192 actual
}
def _log_llm_config(self, method_name: str, **config):
"""Centralized debug logging for LLM configuration and parameters.
Args:
method_name: The name of the method calling this logger (e.g., '__init__', 'get_response')
**config: Configuration parameters to log
"""
# Check for debug logging - either global debug level OR explicit verbose mode
verbose = config.get('verbose', self.verbose if hasattr(self, 'verbose') else False)
should_log = logging.getLogger().getEffectiveLevel() == logging.DEBUG or (not isinstance(verbose, bool) and verbose >= 10)
if should_log:
# Mask sensitive information
safe_config = config.copy()
if 'api_key' in safe_config:
safe_config['api_key'] = "***" if safe_config['api_key'] is not None else None
if 'extra_settings' in safe_config and isinstance(safe_config['extra_settings'], dict):
safe_config['extra_settings'] = {k: v for k, v in safe_config['extra_settings'].items() if k not in ["api_key"]}
# Handle special formatting for certain fields
if 'prompt' in safe_config:
prompt = safe_config['prompt']
# Convert to string first for consistent logging behavior
prompt_str = str(prompt) if not isinstance(prompt, str) else prompt
if len(prompt_str) > 100:
safe_config['prompt'] = prompt_str[:100] + "..."
else:
safe_config['prompt'] = prompt_str
if 'system_prompt' in safe_config:
sp = safe_config['system_prompt']
if sp and isinstance(sp, str) and len(sp) > 100:
safe_config['system_prompt'] = sp[:100] + "..."
if 'chat_history' in safe_config:
ch = safe_config['chat_history']
safe_config['chat_history'] = f"[{len(ch)} messages]" if ch else None
if 'tools' in safe_config:
tools = safe_config['tools']
# Check if tools is iterable before processing
if tools and hasattr(tools, '__iter__') and not isinstance(tools, str):
safe_config['tools'] = [t.__name__ if hasattr(t, "__name__") else str(t) for t in tools]
else:
safe_config['tools'] = None
if 'output_json' in safe_config:
oj = safe_config['output_json']
safe_config['output_json'] = str(oj.__class__.__name__) if oj else None
if 'output_pydantic' in safe_config:
op = safe_config['output_pydantic']
safe_config['output_pydantic'] = str(op.__class__.__name__) if op else None
# Log based on method name - check more specific conditions first
if method_name == '__init__':
logging.debug(f"LLM instance initialized with: {json.dumps(safe_config, indent=2, default=str)}")
elif "parameters" in method_name:
logging.debug(f"{method_name}: {json.dumps(safe_config, indent=2, default=str)}")
elif "_async" in method_name:
logging.debug(f"LLM async instance configuration: {json.dumps(safe_config, indent=2, default=str)}")
else:
logging.debug(f"{method_name} configuration: {json.dumps(safe_config, indent=2, default=str)}")
def __init__(
self,
model: str,
@@ -181,35 +240,34 @@ class LLM:
litellm.modify_params = True
self._setup_event_tracking(events)
# Log all initialization parameters when in debug mode
if not isinstance(verbose, bool) and verbose >= 10:
debug_info = {
"model": self.model,
"timeout": self.timeout,
"temperature": self.temperature,
"top_p": self.top_p,
"n": self.n,
"max_tokens": self.max_tokens,
"presence_penalty": self.presence_penalty,
"frequency_penalty": self.frequency_penalty,
"logit_bias": self.logit_bias,
"response_format": self.response_format,
"seed": self.seed,
"logprobs": self.logprobs,
"top_logprobs": self.top_logprobs,
"api_version": self.api_version,
"stop_phrases": self.stop_phrases,
"api_key": "***" if self.api_key else None, # Mask API key for security
"base_url": self.base_url,
"verbose": self.verbose,
"markdown": self.markdown,
"self_reflect": self.self_reflect,
"max_reflect": self.max_reflect,
"min_reflect": self.min_reflect,
"reasoning_steps": self.reasoning_steps,
"extra_settings": {k: v for k, v in self.extra_settings.items() if k not in ["api_key"]}
}
logging.debug(f"LLM instance initialized with: {json.dumps(debug_info, indent=2, default=str)}")
# Log all initialization parameters when in debug mode or verbose >= 10
self._log_llm_config(
'__init__',
model=self.model,
timeout=self.timeout,
temperature=self.temperature,
top_p=self.top_p,
n=self.n,
max_tokens=self.max_tokens,
presence_penalty=self.presence_penalty,
frequency_penalty=self.frequency_penalty,
logit_bias=self.logit_bias,
response_format=self.response_format,
seed=self.seed,
logprobs=self.logprobs,
top_logprobs=self.top_logprobs,
api_version=self.api_version,
stop_phrases=self.stop_phrases,
api_key=self.api_key,
base_url=self.base_url,
verbose=self.verbose,
markdown=self.markdown,
self_reflect=self.self_reflect,
max_reflect=self.max_reflect,
min_reflect=self.min_reflect,
reasoning_steps=self.reasoning_steps,
extra_settings=self.extra_settings
)
def _is_ollama_provider(self) -> bool:
"""Detect if this is an Ollama provider regardless of naming convention"""
@@ -229,6 +287,39 @@ class LLM:
return any(endpoint in base_url or endpoint in api_base for endpoint in ollama_endpoints)
def _process_stream_delta(self, delta, response_text: str, tool_calls: List[Dict], formatted_tools: Optional[List] = None) -> tuple:
"""
Process a streaming delta chunk to extract content and tool calls.
Args:
delta: The delta object from a streaming chunk
response_text: The accumulated response text so far
tool_calls: The accumulated tool calls list so far
formatted_tools: Optional list of formatted tools for tool call support check
Returns:
tuple: (updated_response_text, updated_tool_calls)
"""
# Process content
if delta.content:
response_text += delta.content
# Capture tool calls from streaming chunks if provider supports it
if formatted_tools and self._supports_streaming_tools() and hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc in delta.tool_calls:
if tc.index >= len(tool_calls):
tool_calls.append({
"id": tc.id,
"type": "function",
"function": {"name": "", "arguments": ""}
})
if tc.function.name:
tool_calls[tc.index]["function"]["name"] = tc.function.name
if tc.function.arguments:
tool_calls[tc.index]["function"]["arguments"] += tc.function.arguments
return response_text, tool_calls
def _parse_tool_call_arguments(self, tool_call: Dict, is_ollama: bool = False) -> tuple:
"""
Safely parse tool call arguments with proper error handling
@@ -497,54 +588,53 @@ class LLM:
"""Enhanced get_response with all OpenAI-like features"""
logging.info(f"Getting response from {self.model}")
# Log all self values when in debug mode
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
debug_info = {
"model": self.model,
"timeout": self.timeout,
"temperature": self.temperature,
"top_p": self.top_p,
"n": self.n,
"max_tokens": self.max_tokens,
"presence_penalty": self.presence_penalty,
"frequency_penalty": self.frequency_penalty,
"logit_bias": self.logit_bias,
"response_format": self.response_format,
"seed": self.seed,
"logprobs": self.logprobs,
"top_logprobs": self.top_logprobs,
"api_version": self.api_version,
"stop_phrases": self.stop_phrases,
"api_key": "***" if self.api_key else None, # Mask API key for security
"base_url": self.base_url,
"verbose": self.verbose,
"markdown": self.markdown,
"self_reflect": self.self_reflect,
"max_reflect": self.max_reflect,
"min_reflect": self.min_reflect,
"reasoning_steps": self.reasoning_steps
}
logging.debug(f"LLM instance configuration: {json.dumps(debug_info, indent=2, default=str)}")
# Log the parameter values passed to get_response
param_info = {
"prompt": str(prompt)[:100] + "..." if isinstance(prompt, str) and len(str(prompt)) > 100 else str(prompt),
"system_prompt": system_prompt[:100] + "..." if system_prompt and len(system_prompt) > 100 else system_prompt,
"chat_history": f"[{len(chat_history)} messages]" if chat_history else None,
"temperature": temperature,
"tools": [t.__name__ if hasattr(t, "__name__") else str(t) for t in tools] if tools else None,
"output_json": str(output_json.__class__.__name__) if output_json else None,
"output_pydantic": str(output_pydantic.__class__.__name__) if output_pydantic else None,
"verbose": verbose,
"markdown": markdown,
"self_reflect": self_reflect,
"max_reflect": max_reflect,
"min_reflect": min_reflect,
"agent_name": agent_name,
"agent_role": agent_role,
"agent_tools": agent_tools,
"kwargs": str(kwargs)
}
logging.debug(f"get_response parameters: {json.dumps(param_info, indent=2, default=str)}")
self._log_llm_config(
'LLM instance',
model=self.model,
timeout=self.timeout,
temperature=self.temperature,
top_p=self.top_p,
n=self.n,
max_tokens=self.max_tokens,
presence_penalty=self.presence_penalty,
frequency_penalty=self.frequency_penalty,
logit_bias=self.logit_bias,
response_format=self.response_format,
seed=self.seed,
logprobs=self.logprobs,
top_logprobs=self.top_logprobs,
api_version=self.api_version,
stop_phrases=self.stop_phrases,
api_key=self.api_key,
base_url=self.base_url,
verbose=self.verbose,
markdown=self.markdown,
self_reflect=self.self_reflect,
max_reflect=self.max_reflect,
min_reflect=self.min_reflect,
reasoning_steps=self.reasoning_steps
)
# Log the parameter values passed to get_response
self._log_llm_config(
'get_response parameters',
prompt=prompt,
system_prompt=system_prompt,
chat_history=chat_history,
temperature=temperature,
tools=tools,
output_json=output_json,
output_pydantic=output_pydantic,
verbose=verbose,
markdown=markdown,
self_reflect=self_reflect,
max_reflect=max_reflect,
min_reflect=min_reflect,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
kwargs=str(kwargs)
)
try:
import litellm
# This below **kwargs** is passed to .completion() directly. so reasoning_steps has to be popped. OR find alternate best way of handling this.
@@ -651,23 +741,12 @@ class LLM:
):
if chunk and chunk.choices and chunk.choices[0].delta:
delta = chunk.choices[0].delta
response_text, tool_calls = self._process_stream_delta(
delta, response_text, tool_calls, formatted_tools
)
if delta.content:
response_text += delta.content
live.update(display_generating(response_text, current_time))
# Capture tool calls from streaming chunks if provider supports it
if formatted_tools and self._supports_streaming_tools() and hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc in delta.tool_calls:
if tc.index >= len(tool_calls):
tool_calls.append({
"id": tc.id,
"type": "function",
"function": {"name": "", "arguments": ""}
})
if tc.function.name:
tool_calls[tc.index]["function"]["name"] = tc.function.name
if tc.function.arguments:
tool_calls[tc.index]["function"]["arguments"] += tc.function.arguments
else:
# Non-verbose streaming
for chunk in litellm.completion(
@@ -685,18 +764,8 @@ class LLM:
response_text += delta.content
# Capture tool calls from streaming chunks if provider supports it
if formatted_tools and self._supports_streaming_tools() and hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc in delta.tool_calls:
if tc.index >= len(tool_calls):
tool_calls.append({
"id": tc.id,
"type": "function",
"function": {"name": "", "arguments": ""}
})
if tc.function.name:
tool_calls[tc.index]["function"]["name"] = tc.function.name
if tc.function.arguments:
tool_calls[tc.index]["function"]["arguments"] += tc.function.arguments
if formatted_tools and self._supports_streaming_tools():
tool_calls = self._process_tool_calls_from_stream(delta, tool_calls)
response_text = response_text.strip()
@@ -737,20 +806,7 @@ class LLM:
# Handle tool calls - Sequential tool calling logic
if tool_calls and execute_tool_fn:
# Convert tool_calls to a serializable format for all providers
serializable_tool_calls = []
for tc in tool_calls:
if isinstance(tc, dict):
serializable_tool_calls.append(tc) # Already a dict
else:
# Convert object to dict
serializable_tool_calls.append({
"id": tc.id,
"type": getattr(tc, 'type', "function"),
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
})
serializable_tool_calls = self._serialize_tool_calls(tool_calls)
messages.append({
"role": "assistant",
"content": response_text,
@@ -761,20 +817,8 @@ class LLM:
tool_results = [] # Store all tool results
for tool_call in tool_calls:
# Handle both object and dict access patterns
if isinstance(tool_call, dict):
is_ollama = self._is_ollama_provider()
function_name, arguments, tool_call_id = self._parse_tool_call_arguments(tool_call, is_ollama)
else:
# Handle object-style tool calls
try:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments) if tool_call.function.arguments else {}
tool_call_id = tool_call.id
except (json.JSONDecodeError, AttributeError) as e:
logging.error(f"Error parsing object-style tool call: {e}")
function_name = "unknown_function"
arguments = {}
tool_call_id = f"tool_{id(tool_call)}"
is_ollama = self._is_ollama_provider()
function_name, arguments, tool_call_id = self._extract_tool_call_info(tool_call, is_ollama)
logging.debug(f"[TOOL_EXEC_DEBUG] About to execute tool {function_name} with args: {arguments}")
tool_result = execute_tool_fn(function_name, arguments)
@@ -1178,54 +1222,53 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
import litellm
logging.info(f"Getting async response from {self.model}")
# Log all self values when in debug mode
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
debug_info = {
"model": self.model,
"timeout": self.timeout,
"temperature": self.temperature,
"top_p": self.top_p,
"n": self.n,
"max_tokens": self.max_tokens,
"presence_penalty": self.presence_penalty,
"frequency_penalty": self.frequency_penalty,
"logit_bias": self.logit_bias,
"response_format": self.response_format,
"seed": self.seed,
"logprobs": self.logprobs,
"top_logprobs": self.top_logprobs,
"api_version": self.api_version,
"stop_phrases": self.stop_phrases,
"api_key": "***" if self.api_key else None, # Mask API key for security
"base_url": self.base_url,
"verbose": self.verbose,
"markdown": self.markdown,
"self_reflect": self.self_reflect,
"max_reflect": self.max_reflect,
"min_reflect": self.min_reflect,
"reasoning_steps": self.reasoning_steps
}
logging.debug(f"LLM async instance configuration: {json.dumps(debug_info, indent=2, default=str)}")
# Log the parameter values passed to get_response_async
param_info = {
"prompt": str(prompt)[:100] + "..." if isinstance(prompt, str) and len(str(prompt)) > 100 else str(prompt),
"system_prompt": system_prompt[:100] + "..." if system_prompt and len(system_prompt) > 100 else system_prompt,
"chat_history": f"[{len(chat_history)} messages]" if chat_history else None,
"temperature": temperature,
"tools": [t.__name__ if hasattr(t, "__name__") else str(t) for t in tools] if tools else None,
"output_json": str(output_json.__class__.__name__) if output_json else None,
"output_pydantic": str(output_pydantic.__class__.__name__) if output_pydantic else None,
"verbose": verbose,
"markdown": markdown,
"self_reflect": self_reflect,
"max_reflect": max_reflect,
"min_reflect": min_reflect,
"agent_name": agent_name,
"agent_role": agent_role,
"agent_tools": agent_tools,
"kwargs": str(kwargs)
}
logging.debug(f"get_response_async parameters: {json.dumps(param_info, indent=2, default=str)}")
self._log_llm_config(
'get_response_async',
model=self.model,
timeout=self.timeout,
temperature=self.temperature,
top_p=self.top_p,
n=self.n,
max_tokens=self.max_tokens,
presence_penalty=self.presence_penalty,
frequency_penalty=self.frequency_penalty,
logit_bias=self.logit_bias,
response_format=self.response_format,
seed=self.seed,
logprobs=self.logprobs,
top_logprobs=self.top_logprobs,
api_version=self.api_version,
stop_phrases=self.stop_phrases,
api_key=self.api_key,
base_url=self.base_url,
verbose=self.verbose,
markdown=self.markdown,
self_reflect=self.self_reflect,
max_reflect=self.max_reflect,
min_reflect=self.min_reflect,
reasoning_steps=self.reasoning_steps
)
# Log the parameter values passed to get_response_async
self._log_llm_config(
'get_response_async parameters',
prompt=prompt,
system_prompt=system_prompt,
chat_history=chat_history,
temperature=temperature,
tools=tools,
output_json=output_json,
output_pydantic=output_pydantic,
verbose=verbose,
markdown=markdown,
self_reflect=self_reflect,
max_reflect=max_reflect,
min_reflect=min_reflect,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
kwargs=str(kwargs)
)
reasoning_steps = kwargs.pop('reasoning_steps', self.reasoning_steps)
litellm.set_verbose = False
@@ -1297,24 +1340,13 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
):
if chunk and chunk.choices and chunk.choices[0].delta:
delta = chunk.choices[0].delta
response_text, tool_calls = self._process_stream_delta(
delta, response_text, tool_calls, formatted_tools
)
if delta.content:
response_text += delta.content
print("\033[K", end="\r")
print(f"Generating... {time.time() - start_time:.1f}s", end="\r")
# Capture tool calls from streaming chunks if provider supports it
if formatted_tools and self._supports_streaming_tools() and hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc in delta.tool_calls:
if tc.index >= len(tool_calls):
tool_calls.append({
"id": tc.id,
"type": "function",
"function": {"name": "", "arguments": ""}
})
if tc.function.name:
tool_calls[tc.index]["function"]["name"] = tc.function.name
if tc.function.arguments:
tool_calls[tc.index]["function"]["arguments"] += tc.function.arguments
else:
# Non-verbose streaming
async for chunk in await litellm.acompletion(
@@ -1332,18 +1364,8 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
response_text += delta.content
# Capture tool calls from streaming chunks if provider supports it
if formatted_tools and self._supports_streaming_tools() and hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc in delta.tool_calls:
if tc.index >= len(tool_calls):
tool_calls.append({
"id": tc.id,
"type": "function",
"function": {"name": "", "arguments": ""}
})
if tc.function.name:
tool_calls[tc.index]["function"]["name"] = tc.function.name
if tc.function.arguments:
tool_calls[tc.index]["function"]["arguments"] += tc.function.arguments
if formatted_tools and self._supports_streaming_tools():
tool_calls = self._process_tool_calls_from_stream(delta, tool_calls)
response_text = response_text.strip()
@@ -1378,20 +1400,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
if tool_calls:
# Convert tool_calls to a serializable format for all providers
serializable_tool_calls = []
for tc in tool_calls:
if isinstance(tc, dict):
serializable_tool_calls.append(tc) # Already a dict
else:
# Convert object to dict
serializable_tool_calls.append({
"id": tc.id,
"type": getattr(tc, 'type', "function"),
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
})
serializable_tool_calls = self._serialize_tool_calls(tool_calls)
messages.append({
"role": "assistant",
"content": response_text,
@@ -1401,20 +1410,8 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
tool_results = [] # Store all tool results
for tool_call in tool_calls:
# Handle both object and dict access patterns
if isinstance(tool_call, dict):
is_ollama = self._is_ollama_provider()
function_name, arguments, tool_call_id = self._parse_tool_call_arguments(tool_call, is_ollama)
else:
# Handle object-style tool calls
try:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments) if tool_call.function.arguments else {}
tool_call_id = tool_call.id
except (json.JSONDecodeError, AttributeError) as e:
logging.error(f"Error parsing object-style tool call: {e}")
function_name = "unknown_function"
arguments = {}
tool_call_id = f"tool_{id(tool_call)}"
is_ollama = self._is_ollama_provider()
function_name, arguments, tool_call_id = self._extract_tool_call_info(tool_call, is_ollama)
tool_result = await execute_tool_fn(function_name, arguments)
tool_results.append(tool_result) # Store the result
@@ -1860,6 +1857,90 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
return params
def _prepare_response_logging(self, temperature: float, stream: bool, verbose: bool, markdown: bool, **kwargs) -> Optional[Dict[str, Any]]:
"""Prepare debug logging information for response methods"""
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
debug_info = {
"model": self.model,
"timeout": self.timeout,
"temperature": temperature,
"top_p": self.top_p,
"n": self.n,
"max_tokens": self.max_tokens,
"presence_penalty": self.presence_penalty,
"frequency_penalty": self.frequency_penalty,
"stream": stream,
"verbose": verbose,
"markdown": markdown,
"kwargs": str(kwargs)
}
return debug_info
return None
def _process_streaming_chunk(self, chunk) -> Optional[str]:
"""Extract content from a streaming chunk"""
if chunk and chunk.choices and chunk.choices[0].delta.content:
return chunk.choices[0].delta.content
return None
def _process_tool_calls_from_stream(self, delta, tool_calls: List[Dict]) -> List[Dict]:
"""Process tool calls from streaming delta chunks.
This handles the accumulation of tool call data from streaming chunks,
building up the complete tool call information incrementally.
"""
if hasattr(delta, 'tool_calls') and delta.tool_calls:
for tc in delta.tool_calls:
if tc.index >= len(tool_calls):
tool_calls.append({
"id": tc.id,
"type": "function",
"function": {"name": "", "arguments": ""}
})
if tc.function.name:
tool_calls[tc.index]["function"]["name"] = tc.function.name
if tc.function.arguments:
tool_calls[tc.index]["function"]["arguments"] += tc.function.arguments
return tool_calls
def _serialize_tool_calls(self, tool_calls) -> List[Dict]:
"""Convert tool calls to a serializable format for all providers."""
serializable_tool_calls = []
for tc in tool_calls:
if isinstance(tc, dict):
serializable_tool_calls.append(tc) # Already a dict
else:
# Convert object to dict
serializable_tool_calls.append({
"id": tc.id,
"type": getattr(tc, 'type', "function"),
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
})
return serializable_tool_calls
def _extract_tool_call_info(self, tool_call, is_ollama: bool = False) -> tuple:
"""Extract function name, arguments, and tool_call_id from a tool call.
Handles both dict and object formats for tool calls.
"""
if isinstance(tool_call, dict):
return self._parse_tool_call_arguments(tool_call, is_ollama)
else:
# Handle object-style tool calls
try:
function_name = tool_call.function.name
arguments = json.loads(tool_call.function.arguments) if tool_call.function.arguments else {}
tool_call_id = tool_call.id
except (json.JSONDecodeError, AttributeError) as e:
logging.error(f"Error parsing object-style tool call: {e}")
function_name = "unknown_function"
arguments = {}
tool_call_id = f"tool_{id(tool_call)}"
return function_name, arguments, tool_call_id
# Response without tool calls
def response(
self,
@@ -1884,22 +1965,21 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
logger.debug("Using synchronous response function")
# Log all self values when in debug mode
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
debug_info = {
"model": self.model,
"timeout": self.timeout,
"temperature": temperature,
"top_p": self.top_p,
"n": self.n,
"max_tokens": self.max_tokens,
"presence_penalty": self.presence_penalty,
"frequency_penalty": self.frequency_penalty,
"stream": stream,
"verbose": verbose,
"markdown": markdown,
"kwargs": str(kwargs)
}
logger.debug(f"Response method configuration: {json.dumps(debug_info, indent=2, default=str)}")
self._log_llm_config(
'Response method',
model=self.model,
timeout=self.timeout,
temperature=temperature,
top_p=self.top_p,
n=self.n,
max_tokens=self.max_tokens,
presence_penalty=self.presence_penalty,
frequency_penalty=self.frequency_penalty,
stream=stream,
verbose=verbose,
markdown=markdown,
kwargs=str(kwargs)
)
# Build messages list using shared helper (simplified version without JSON output)
messages, _ = self._build_messages(
@@ -1908,42 +1988,29 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
)
# Get response from LiteLLM
response_text = ""
completion_params = self._build_completion_params(
messages=messages,
temperature=temperature,
stream=stream,
**kwargs
)
if stream:
response_text = ""
if verbose:
with Live(display_generating("", start_time), console=console or self.console, refresh_per_second=4) as live:
for chunk in litellm.completion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=True,
**kwargs
)
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
for chunk in litellm.completion(**completion_params):
content = self._process_streaming_chunk(chunk)
if content:
response_text += content
live.update(display_generating(response_text, start_time))
else:
for chunk in litellm.completion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=True,
**kwargs
)
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
response_text += chunk.choices[0].delta.content
for chunk in litellm.completion(**completion_params):
content = self._process_streaming_chunk(chunk)
if content:
response_text += content
else:
response = litellm.completion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=False,
**kwargs
)
)
response = litellm.completion(**completion_params)
response_text = response.choices[0].message.content.strip()
if verbose:
@@ -1984,23 +2051,23 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
logger.debug("Using asynchronous response function")
# Log all self values when in debug mode
if logging.getLogger().getEffectiveLevel() == logging.DEBUG:
debug_info = {
"model": self.model,
"timeout": self.timeout,
"temperature": temperature,
"top_p": self.top_p,
"n": self.n,
"max_tokens": self.max_tokens,
"presence_penalty": self.presence_penalty,
"frequency_penalty": self.frequency_penalty,
"stream": stream,
"verbose": verbose,
"markdown": markdown,
"kwargs": str(kwargs)
}
logger.debug(f"Async response method configuration: {json.dumps(debug_info, indent=2, default=str)}")
self._log_llm_config(
'Async response method',
model=self.model,
timeout=self.timeout,
temperature=temperature,
top_p=self.top_p,
n=self.n,
max_tokens=self.max_tokens,
presence_penalty=self.presence_penalty,
frequency_penalty=self.frequency_penalty,
stream=stream,
verbose=verbose,
markdown=markdown,
kwargs=str(kwargs)
)
# Build messages list using shared helper (simplified version without JSON output)
messages, _ = self._build_messages(
@@ -2009,42 +2076,29 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
)
# Get response from LiteLLM
response_text = ""
completion_params = self._build_completion_params(
messages=messages,
temperature=temperature,
stream=stream,
**kwargs
)
if stream:
response_text = ""
if verbose:
with Live(display_generating("", start_time), console=console or self.console, refresh_per_second=4) as live:
async for chunk in await litellm.acompletion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=True,
**kwargs
)
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
async for chunk in await litellm.acompletion(**completion_params):
content = self._process_streaming_chunk(chunk)
if content:
response_text += content
live.update(display_generating(response_text, start_time))
else:
async for chunk in await litellm.acompletion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=True,
**kwargs
)
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
response_text += chunk.choices[0].delta.content
async for chunk in await litellm.acompletion(**completion_params):
content = self._process_streaming_chunk(chunk)
if content:
response_text += content
else:
response = await litellm.acompletion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=False,
**kwargs
)
)
response = await litellm.acompletion(**completion_params)
response_text = response.choices[0].message.content.strip()
if verbose:
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.118"
version = "0.0.122"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
+8
View File
@@ -0,0 +1,8 @@
from praisonaiagents import Agent, MCP
search_agent = Agent(
instructions="""You are a Tweet.""",
llm="openai/gpt-4o-mini",
tools=MCP("http://localhost:8080/sse")
)
search_agent.launch(path="/tweet", port=3030)
@@ -0,0 +1,4 @@
from praisonaiagents import Agent
agent = Agent(instructions="Create a Tweet")
agent.launch(port=8080, protocol="mcp")
+1 -1
View File
@@ -2375,7 +2375,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.118"
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.45.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.45.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.45 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.45"
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.118",
"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.45"
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.118"
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.45"
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.118" },
{ 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.118"
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/ba/53/0363b4e986b3916a246af2798cecaba08d47d46acc5faf313789d8de07cf/praisonaiagents-0.0.118.tar.gz", hash = "sha256:0e7cb593fc1733e33c6f17d55e4670ca973cfe4233c86747c7d97be41347a3da", size = 172108 }
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/9a/a1/605677f442a983259be11079b492fff2a1cca78686c8a6385f8adcdea285/praisonaiagents-0.0.118-py3-none-any.whl", hash = "sha256:5e3f27ffc9af64c2db1726dc1174f348b5bb0dea07b110651fcde6f480ca36a1", size = 192626 },
{ url = "https://files.pythonhosted.org/packages/4c/c4/5a5a7df835abba25ac047f0eef4b50d5534407f6a45478c5837959d3662e/praisonaiagents-0.0.122-py3-none-any.whl", hash = "sha256:a7da819f49f23fffc15c3cdc1dbbb2f2560d16775f741f8118adb5d7fd45767b", size = 194479 },
]
[[package]]