Compare commits

...
27 Commits
Author SHA1 Message Date
Mervin Praison 7b41444aad refactor: move test files into src/praisonai-agents directory for better organization 2025-07-14 21:13:34 +01:00
Mervin Praison 7c8b77063f chore: bump praisonai to v2.2.58 and praisonaiagents to v0.0.132 across all files 2025-07-14 21:12:31 +01:00
Mervin Praison 59eb8d6356 Merge branch 'main' of https://github.com/MervinPraison/PraisonAI 2025-07-14 21:07:47 +01:00
Mervin Praison 89bddc31c6 feat: add allowed tools configuration for Claude GitHub action workflow 2025-07-14 21:07:45 +01:00
Mervin Praison e6e908298b Merge pull request #916 from MervinPraison/claude/issue-878-20250714_190400
Fix: resolve duplicate callback execution in LLM interaction handling
2025-07-14 20:53:39 +01:00
Mervin Praison 2b3509c1d1 Delete reproduce_issue_878.py 2025-07-14 20:52:36 +01:00
Mervin Praison 6e8029055a Delete ISSUE_878_REPRODUCTION_GUIDE.md 2025-07-14 20:52:15 +01:00
Mervin Praison d2c9e8825a Merge pull request #915 from MervinPraison/claude/issue-911-20250714_190025
fix: add execute and aexecute methods to Agent class
2025-07-14 20:44:41 +01:00
Mervin Praison cd9816e218 Merge pull request #914 from MervinPraison/claude/issue-901-20250714_143624
Issue #901: Fix self-reflection with tools
2025-07-14 20:43:29 +01:00
Mervin Praison d400149075 chore: update claude review request message to include follow-up action 2025-07-14 20:42:15 +01:00
Mervin Praison 73b08e6b76 feat: add PR creation permissions and request in auto-issue workflow 2025-07-14 20:32:59 +01:00
github-actions[bot]andClaude 19c3a1f6a7 fix: address code review issues in test files
- Add missing sys.path setup to test_self_reflection_with_tools_verification.py
- Remove unnecessary f-string prefixes from print statements
- Add missing newlines at end of files for PEP 8 compliance

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2025-07-14 19:24:12 +00:00
71a7968318 fix: resolve duplicate callback execution in LLM interaction handling
- Fixed duplicate callback issue in src/praisonai-agents/praisonaiagents/llm/llm.py where callbacks were executed multiple times
- Added callback_executed flag to track and prevent duplicate callback invocations
- Ensured callbacks are only executed once per interaction, either through display_interaction or direct execute_sync_callback calls
- Improved generation_time consistency by calculating once and reusing the value
- Added comprehensive test files and reproduction scripts to verify the fix
- Added ISSUE_878_REPRODUCTION_GUIDE.md documenting the issue and solution

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
Co-Authored-By: Mervin Praison <[email protected]>
2025-07-14 19:15:39 +00:00
Mervin Praison 1b258229c1 Merge pull request #913 from MervinPraison/claude/issue-906-20250714_175631
fix: Add callback execution to synchronous display functions
2025-07-14 20:12:26 +01:00
github-actions[bot]andMervin Praison d0e09f51f3 fix: Ensure consistent callback execution across all display functions
- Fix async functions to use execute_callback() instead of direct async_display_callbacks access
- Add missing execute_sync_callback() calls to sync display functions (instruction, tool_call, error, generating)
- Ensures both sync and async callbacks are triggered consistently for all display types
- Maintains backward compatibility through existing parameter filtering

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2025-07-14 19:04:19 +00:00
github-actions[bot]andMervin Praison 5adc862ead fix: add execute and aexecute methods to Agent class for backward compatibility
Resolves AttributeError: type object ''Agent'' has no attribute ''execute''

- Added execute(task, context=None) method that delegates to chat()
- Added aexecute(task, context=None) method that delegates to achat()
- Both methods handle task objects with description attribute or string inputs
- Maintains complete backward compatibility with existing code
- Fixes issue #911

Co-authored-by: Mervin Praison <[email protected]>
2025-07-14 19:03:24 +00:00
Mervin Praison 856db54334 Merge pull request #912 from MervinPraison/claude/issue-904-20250714_171858
Fix issue #904: Loop task callback inheritance and enhanced metadata support
2025-07-14 19:58:26 +01:00
github-actions[bot]andMervin Praison f94e5bb3d6 fix: Add callback execution to synchronous display functions
Fixes the duplicate callback issue by:

- Adding execute_sync_callback calls to display_interaction and display_self_reflection
- Fixing test to patch the correct function location
- Ensuring backward compatibility by maintaining existing function signatures
- Adding comprehensive tests for callback system verification

This ensures that callbacks are called exactly once per LLM response while maintaining full backward compatibility.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2025-07-14 18:06:08 +00:00
Mervin Praison 1cece603ec Merge branch 'main' of https://github.com/MervinPraison/PraisonAI 2025-07-14 18:59:38 +01:00
Mervin Praison 9e2bbb9bbb refactor: update tool imports in test files from calculator to evaluate for improved clarity and consistency 2025-07-14 18:59:36 +01:00
Mervin Praison 35b4cff7f7 Merge pull request #910 from MervinPraison/claude/issue-905-20250714_163222
fix: resolve litellm.BadRequestError with unrecognized output_json/output_pydantic parameters
2025-07-14 18:44:18 +01:00
Mervin Praison a094d1ff30 Merge pull request #908 from MervinPraison/claude/issue-907-20250714_165230
fix: add string LLM support to LLMGuardrail for Gemini models
2025-07-14 18:43:11 +01:00
github-actions[bot]andMervin Praison 826ff56fc5 refactor: simplify LLMGuardrail string handling and consolidate imports
- Remove redundant if/else logic in string LLM initialization
- Consolidate duplicate LLM imports using _get_llm_class helper
- Maintain all functionality while improving code clarity
- Addresses code review feedback for cleaner, more maintainable code

Co-authored-by: Mervin Praison <[email protected]>
2025-07-14 17:31:52 +00:00
github-actions[bot]andClaude 4bd0195d5d Fix issue #904: Loop task callback inheritance and enhanced metadata support
- Add callback inheritance in all 6 locations where loop tasks create subtasks in process.py
- Implement enhanced callback metadata support in task.py with run_context tracking
- Ensure backward compatibility with existing callback implementations
- Maintain proper callback chain for nested loop executions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>
2025-07-14 17:28:28 +00:00
github-actions[bot]andMervin Praison 75c4d8432c fix: add string LLM support to LLMGuardrail for Gemini models
- Add _initialize_llm method to convert string identifiers to LLM instances
- Update __init__ to handle string LLM identifiers like ''gemini/gemini-2.5-flash-lite-preview-06-17''
- Maintain backward compatibility with existing LLM instances, dict configs, and None values
- Use same LLM initialization logic as Agent class for consistency
- Fixes issue #907 where LLMGuardrail failed with ''Unsupported LLM type'' error

Co-authored-by: Mervin Praison <[email protected]>
2025-07-14 16:57:24 +00:00
github-actions[bot]andMervin Praison 32537a227e fix: resolve litellm.BadRequestError with unrecognized output_json/output_pydantic parameters
- Fixed bug in _build_completion_params method where output_json and output_pydantic
  parameters were conditionally removed only when not None
- These custom parameters are now always removed before LiteLLM calls to prevent
  "Unrecognized request arguments supplied" errors
- Maintains backward compatibility and preserves all existing functionality
- Addresses issue #905

Co-authored-by: Mervin Praison <[email protected]>
2025-07-14 16:43:03 +00:00
github-actions[bot]andMervin Praison e252f86ac9 fix: self-reflection now works with tools
- Fixed issue where self-reflection failed when tools were present
- Added tools=formatted_tools parameter to reflection regeneration calls
- Ensures tools remain available throughout the reflection process
- Maintains backward compatibility with existing code
- Resolves issue #901

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Mervin Praison <[email protected]>
2025-07-14 14:43:12 +00:00
28 changed files with 1466 additions and 215 deletions

No files matched your search

+6 -3
View File
@@ -1,4 +1,4 @@
name: Auto Issue Comment
name: Auto Issue Comment with PR Creation
on:
issues:
@@ -9,6 +9,9 @@ jobs:
runs-on: ubuntu-latest
permissions:
issues: write
contents: write
pull-requests: write
actions: read
steps:
- name: Add automated comment
uses: actions/github-script@v7
@@ -19,5 +22,5 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude review this issue 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 review the applied changes. Use @web to search if you dont know any information or to find the latest documentation or to find the latest version. Run the code if you think you need to run it to test it. Minimal code change to start with if required any changes.'
})
body: '@claude review this issue 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 review the applied changes. Use @web to search if you dont know any information or to find the latest documentation or to find the latest version. Run the code if you think you need to run it to test it. Minimal code change to start with if required any changes. **Please create a PR with your changes**.'
})
+19 -1
View File
@@ -36,6 +36,24 @@ jobs:
claude_code_oauth_token: ${{ secrets.CLAUDE_ACCESS_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
trigger_phrase: "@claude"
allowed_tools: |
Bash(git:*)
Bash(python:*)
Bash(pip:*)
Bash(conda:*)
Bash(pytest:*)
Bash(python -m pytest:*)
Bash(python -m pip:*)
Bash(poetry:*)
View
GlobTool
GrepTool
BatchTool
Edit
Replace
mcp__github__get_issue
mcp__github__get_issue_comments
mcp__github__update_issue
timeout_minutes: 30
post-review-comment:
@@ -129,5 +147,5 @@ jobs:
issue_number: issueNumber,
owner: context.repo.owner,
repo: context.repo.repo,
body: '@claude review if the changes made are as per the requirements'
body: '@claude review if the changes made are as per the requirements and make changes if issues still exist.'
});
+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.57" \
"praisonai>=2.2.58" \
"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.57" \
"praisonai>=2.2.58" \
"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.57" \
"praisonai>=2.2.58" \
"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.57" \
"praisonai>=2.2.58" \
"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.57`
- PraisonAI: `>=2.2.58`
- 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.57" "praisonaiagents==0.0.92"
RUN pip install "praisonai==2.2.58" "praisonaiagents==0.0.92"
```
## 🌐 Production Deployment
@@ -1926,6 +1926,26 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
"""Start the agent with a prompt. This is a convenience method that wraps chat()."""
return self.chat(prompt, **kwargs)
def execute(self, task, context=None):
"""Execute a task synchronously - backward compatibility method"""
if hasattr(task, 'description'):
prompt = task.description
elif isinstance(task, str):
prompt = task
else:
prompt = str(task)
return self.chat(prompt)
async def aexecute(self, task, context=None):
"""Execute a task asynchronously - backward compatibility method"""
if hasattr(task, 'description'):
prompt = task.description
elif isinstance(task, str):
prompt = task
else:
prompt = str(task)
return await self.achat(prompt)
async def execute_tool_async(self, function_name: str, arguments: Dict[str, Any]) -> Any:
"""Async version of execute_tool"""
try:
@@ -19,12 +19,54 @@ class LLMGuardrail:
Args:
description: Natural language description of what to validate
llm: The LLM instance to use for validation
llm: The LLM instance to use for validation (can be string or LLM instance)
"""
self.description = description
self.llm = llm
self.llm = self._initialize_llm(llm)
self.logger = logging.getLogger(__name__)
def _initialize_llm(self, llm: Any) -> Any:
"""Initialize the LLM instance from string identifier or existing instance.
Args:
llm: String identifier, LLM instance, or None
Returns:
LLM instance or None
"""
# Local import to avoid circular dependencies
def _get_llm_class():
from ..llm.llm import LLM
return LLM
if llm is None:
return None
# If it's already an LLM instance, return as-is
if hasattr(llm, 'chat') or hasattr(llm, 'get_response') or callable(llm):
return llm
# If it's a string, convert to LLM instance
if isinstance(llm, str):
try:
# Handle string identifiers (both provider/model and simple names)
return _get_llm_class()(model=llm)
except Exception as e:
self.logger.error(f"Failed to initialize LLM from string '{llm}': {str(e)}")
return None
# If it's a dict, pass parameters to LLM
if isinstance(llm, dict) and "model" in llm:
try:
return _get_llm_class()(**llm)
except Exception as e:
self.logger.error(f"Failed to initialize LLM from dict: {str(e)}")
return None
# Unknown type
self.logger.warning(f"Unknown LLM type: {type(llm)}, treating as-is")
return llm
def __call__(self, task_output: TaskOutput) -> Tuple[bool, Union[str, TaskOutput]]:
"""Validate the task output using the LLM.
+78 -61
View File
@@ -744,18 +744,9 @@ class LLM:
response_text = resp["choices"][0]["message"]["content"]
final_response = resp
# Always execute callbacks regardless of verbose setting
# Execute callbacks and display based on verbose setting
generation_time_val = time.time() - current_time
interaction_displayed = False
response_content = f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}" if reasoning_content else response_text
execute_sync_callback(
'interaction',
message=original_prompt,
response=response_content,
markdown=markdown,
generation_time=generation_time_val
)
# Optionally display reasoning if present
if verbose and reasoning_content and not interaction_displayed:
@@ -763,7 +754,7 @@ class LLM:
original_prompt,
f"Reasoning:\n{reasoning_content}\n\nAnswer:\n{response_text}",
markdown=markdown,
generation_time=time.time() - current_time,
generation_time=generation_time_val,
console=console,
agent_name=agent_name,
agent_role=agent_role,
@@ -773,12 +764,13 @@ class LLM:
task_id=task_id
)
interaction_displayed = True
callback_executed = True
elif verbose and not interaction_displayed:
display_interaction(
original_prompt,
response_text,
markdown=markdown,
generation_time=time.time() - current_time,
generation_time=generation_time_val,
console=console,
agent_name=agent_name,
agent_role=agent_role,
@@ -788,6 +780,17 @@ class LLM:
task_id=task_id
)
interaction_displayed = True
callback_executed = True
elif not callback_executed:
# Only execute callback if display_interaction hasn't been called (which would trigger callbacks internally)
execute_sync_callback(
'interaction',
message=original_prompt,
response=response_content,
markdown=markdown,
generation_time=generation_time_val
)
callback_executed = True
# Otherwise do the existing streaming approach
else:
@@ -847,14 +850,16 @@ class LLM:
response_text = response_text.strip() if response_text else ""
# Always execute callbacks after streaming completes
execute_sync_callback(
'interaction',
message=original_prompt,
response=response_text,
markdown=markdown,
generation_time=time.time() - current_time
)
# Execute callbacks after streaming completes (only if not verbose, since verbose will call display_interaction later)
if not verbose and not callback_executed:
execute_sync_callback(
'interaction',
message=original_prompt,
response=response_text,
markdown=markdown,
generation_time=time.time() - current_time
)
callback_executed = True
# Create a mock final_response with the captured data
@@ -881,18 +886,9 @@ class LLM:
)
response_text = final_response["choices"][0]["message"]["content"]
# Always execute callbacks regardless of verbose setting
execute_sync_callback(
'interaction',
message=original_prompt,
response=response_text,
markdown=markdown,
generation_time=time.time() - current_time
)
# Execute callbacks and display based on verbose setting
if verbose and not interaction_displayed:
# Display the complete response at once
# Display the complete response at once (this will trigger callbacks internally)
display_interaction(
original_prompt,
response_text,
@@ -907,6 +903,17 @@ class LLM:
task_id=task_id
)
interaction_displayed = True
callback_executed = True
elif not callback_executed:
# Only execute callback if display_interaction hasn't been called
execute_sync_callback(
'interaction',
message=original_prompt,
response=response_text,
markdown=markdown,
generation_time=time.time() - current_time
)
callback_executed = True
tool_calls = final_response["choices"][0]["message"].get("tool_calls")
@@ -1010,16 +1017,8 @@ class LLM:
return final_response_text
# No tool calls were made in this iteration, return the response
# Always execute callbacks regardless of verbose setting
generation_time_val = time.time() - start_time
response_content = f"Reasoning:\n{stored_reasoning_content}\n\nAnswer:\n{response_text}" if stored_reasoning_content else response_text
execute_sync_callback(
'interaction',
message=original_prompt,
response=response_content,
markdown=markdown,
generation_time=generation_time_val
)
if verbose and not interaction_displayed:
# If we have stored reasoning content from tool execution, display it
@@ -1028,7 +1027,7 @@ class LLM:
original_prompt,
f"Reasoning:\n{stored_reasoning_content}\n\nAnswer:\n{response_text}",
markdown=markdown,
generation_time=time.time() - start_time,
generation_time=generation_time_val,
console=console,
agent_name=agent_name,
agent_role=agent_role,
@@ -1042,7 +1041,7 @@ class LLM:
original_prompt,
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
generation_time=generation_time_val,
console=console,
agent_name=agent_name,
agent_role=agent_role,
@@ -1052,6 +1051,17 @@ class LLM:
task_id=task_id
)
interaction_displayed = True
callback_executed = True
elif not callback_executed:
# Only execute callback if display_interaction hasn't been called
execute_sync_callback(
'interaction',
message=original_prompt,
response=response_content,
markdown=markdown,
generation_time=generation_time_val
)
callback_executed = True
response_text = response_text.strip() if response_text else ""
@@ -1063,8 +1073,16 @@ class LLM:
if output_json or output_pydantic:
self.chat_history.append({"role": "user", "content": original_prompt})
self.chat_history.append({"role": "assistant", "content": response_text})
# Always execute callbacks regardless of verbose setting
if not interaction_displayed:
if verbose and not interaction_displayed:
display_interaction(original_prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
callback_executed = True
elif not callback_executed:
# Only execute callback if display_interaction hasn't been called
execute_sync_callback(
'interaction',
message=original_prompt,
@@ -1072,17 +1090,19 @@ class LLM:
markdown=markdown,
generation_time=time.time() - start_time
)
if verbose and not interaction_displayed:
display_interaction(original_prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
callback_executed = True
return response_text
if not self_reflect:
# Always execute callbacks regardless of verbose setting
if not interaction_displayed:
if verbose and not interaction_displayed:
display_interaction(original_prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
callback_executed = True
elif not callback_executed:
# Only execute callback if display_interaction hasn't been called
execute_sync_callback(
'interaction',
message=original_prompt,
@@ -1090,13 +1110,8 @@ class LLM:
markdown=markdown,
generation_time=time.time() - start_time
)
if verbose and not interaction_displayed:
display_interaction(original_prompt, response_text, markdown=markdown,
generation_time=time.time() - start_time, console=console,
agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools,
task_name=task_name, task_description=task_description, task_id=task_id)
interaction_displayed = True
callback_executed = True
# Return reasoning content if reasoning_steps is True
if reasoning_steps and stored_reasoning_content:
return stored_reasoning_content
@@ -1401,6 +1416,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
start_time = time.time()
reflection_count = 0
callback_executed = False # Track if callback has been executed for this interaction
interaction_displayed = False # Track if interaction has been displayed
# Format tools for LiteLLM using the shared helper
@@ -2031,10 +2047,11 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
output_json = override_params.get('output_json')
output_pydantic = override_params.get('output_pydantic')
# Always remove these from params as they're not native litellm parameters
params.pop('output_json', None)
params.pop('output_pydantic', None)
if output_json or output_pydantic:
# Always remove these from params as they're not native litellm parameters
params.pop('output_json', None)
params.pop('output_pydantic', None)
# Check if this is a Gemini model that supports native structured outputs
if self._is_gemini_model():
+39 -42
View File
@@ -159,35 +159,20 @@ def display_interaction(message, response, markdown=True, generation_time=None,
message = _clean_display_content(str(message))
response = _clean_display_content(str(response))
# Execute synchronous callback if registered
if 'interaction' in sync_display_callbacks:
callback = sync_display_callbacks['interaction']
import inspect
sig = inspect.signature(callback)
all_kwargs = {
'message': message,
'response': response,
'markdown': markdown,
'generation_time': generation_time,
'agent_name': agent_name,
'agent_role': agent_role,
'agent_tools': agent_tools,
'task_name': task_name,
'task_description': task_description,
'task_id': task_id
}
# Filter kwargs to what the callback accepts to maintain backward compatibility
if any(p.kind == inspect.Parameter.VAR_KEYWORD for p in sig.parameters.values()):
# Callback accepts **kwargs, so pass all arguments
supported_kwargs = all_kwargs
else:
# Only pass arguments that the callback signature supports
supported_kwargs = {k: v for k, v in all_kwargs.items() if k in sig.parameters}
callback(**supported_kwargs)
# Execute synchronous callbacks
execute_sync_callback(
'interaction',
message=message,
response=response,
markdown=markdown,
generation_time=generation_time,
agent_name=agent_name,
agent_role=agent_role,
agent_tools=agent_tools,
task_name=task_name,
task_description=task_description,
task_id=task_id
)
# Rest of the display logic...
if generation_time:
console.print(Text(f"Response generated in {generation_time:.1f}s", style="dim"))
@@ -206,6 +191,9 @@ def display_self_reflection(message: str, console=None):
console = Console()
message = _clean_display_content(str(message))
# Execute synchronous callbacks
execute_sync_callback('self_reflection', message=message)
console.print(Panel.fit(Text(message, style="bold yellow"), title="Self Reflection", border_style="magenta"))
def display_instruction(message: str, console=None, agent_name: str = None, agent_role: str = None, agent_tools: List[str] = None):
@@ -215,6 +203,9 @@ def display_instruction(message: str, console=None, agent_name: str = None, agen
console = Console()
message = _clean_display_content(str(message))
# Execute synchronous callbacks
execute_sync_callback('instruction', message=message, agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools)
# Display agent info if available
if agent_name:
agent_info = f"[bold #FF9B9B]👤 Agent:[/] [#FFE5E5]{agent_name}[/]"
@@ -239,6 +230,9 @@ def display_tool_call(message: str, console=None):
message = _clean_display_content(str(message))
logging.debug(f"Cleaned message in display_tool_call: {repr(message)}")
# Execute synchronous callbacks
execute_sync_callback('tool_call', message=message)
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
def display_error(message: str, console=None):
@@ -248,6 +242,9 @@ def display_error(message: str, console=None):
console = Console()
message = _clean_display_content(str(message))
# Execute synchronous callbacks
execute_sync_callback('error', message=message)
console.print(Panel.fit(Text(message, style="bold red"), title="Error", border_style="red"))
error_logs.append(message)
@@ -263,6 +260,9 @@ def display_generating(content: str = "", start_time: Optional[float] = None):
content = _clean_display_content(str(content))
# Execute synchronous callbacks
execute_sync_callback('generating', content=content, elapsed_time=elapsed_str.strip() if elapsed_str else None)
return Panel(Markdown(content), title=f"Generating...{elapsed_str}", border_style="green")
# Async versions with 'a' prefix
@@ -312,8 +312,8 @@ async def adisplay_self_reflection(message: str, console=None):
console = Console()
message = _clean_display_content(str(message))
if 'self_reflection' in async_display_callbacks:
await async_display_callbacks['self_reflection'](message=message)
# Execute callbacks
await execute_callback('self_reflection', message=message)
console.print(Panel.fit(Text(message, style="bold yellow"), title="Self Reflection", border_style="magenta"))
@@ -325,8 +325,8 @@ async def adisplay_instruction(message: str, console=None, agent_name: str = Non
console = Console()
message = _clean_display_content(str(message))
if 'instruction' in async_display_callbacks:
await async_display_callbacks['instruction'](message=message)
# Execute callbacks
await execute_callback('instruction', message=message, agent_name=agent_name, agent_role=agent_role, agent_tools=agent_tools)
# Display agent info if available
if agent_name:
@@ -353,8 +353,8 @@ async def adisplay_tool_call(message: str, console=None):
message = _clean_display_content(str(message))
logging.debug(f"Cleaned message in adisplay_tool_call: {repr(message)}")
if 'tool_call' in async_display_callbacks:
await async_display_callbacks['tool_call'](message=message)
# Execute callbacks
await execute_callback('tool_call', message=message)
console.print(Panel.fit(Text(message, style="bold cyan"), title="Tool Call", border_style="green"))
@@ -366,8 +366,8 @@ async def adisplay_error(message: str, console=None):
console = Console()
message = _clean_display_content(str(message))
if 'error' in async_display_callbacks:
await async_display_callbacks['error'](message=message)
# Execute callbacks
await execute_callback('error', message=message)
console.print(Panel.fit(Text(message, style="bold red"), title="Error", border_style="red"))
error_logs.append(message)
@@ -385,11 +385,8 @@ async def adisplay_generating(content: str = "", start_time: Optional[float] = N
content = _clean_display_content(str(content))
if 'generating' in async_display_callbacks:
await async_display_callbacks['generating'](
content=content,
elapsed_time=elapsed_str.strip() if elapsed_str else None
)
# Execute callbacks
await execute_callback('generating', content=content, elapsed_time=elapsed_str.strip() if elapsed_str else None)
return Panel(Markdown(content), title=f"Generating...{elapsed_str}", border_style="green")
@@ -59,10 +59,90 @@ class Process:
def _create_loop_subtasks(self, loop_task: Task):
"""Create subtasks for a loop task from input file."""
logging.warning(f"_create_loop_subtasks called for {loop_task.name} but method not fully implemented")
# TODO: Implement loop subtask creation from input file
# This should read loop_task.input_file and create subtasks
pass
if not loop_task.input_file:
logging.warning(f"_create_loop_subtasks called for {loop_task.name} but no input_file specified")
return
try:
file_ext = os.path.splitext(loop_task.input_file)[1].lower()
new_tasks = []
if file_ext == ".csv":
with open(loop_task.input_file, "r", encoding="utf-8") as f:
reader = csv.reader(f, quotechar='"', escapechar='\\')
previous_task = None
task_count = 0
for i, row in enumerate(reader):
if not row: # Skip empty rows
continue
# Handle Q&A pairs with potential commas
task_desc = row[0].strip() if row else ""
if len(row) > 1:
question = row[0].strip()
answer = ",".join(field.strip() for field in row[1:])
task_desc = f"Question: {question}\nAnswer: {answer}"
if not task_desc: # Skip rows with empty content
continue
task_count += 1
logging.debug(f"Creating subtask from CSV row {i+1}: {task_desc}")
row_task = Task(
description=f"{loop_task.description}\n{task_desc}" if loop_task.description else task_desc,
agent=loop_task.agent,
name=f"{loop_task.name}_{task_count}" if loop_task.name else task_desc,
expected_output=getattr(loop_task, 'expected_output', None),
callback=loop_task.callback, # Inherit callback from parent loop task
is_start=(task_count == 1),
task_type="task"
)
self.tasks[row_task.id] = row_task
new_tasks.append(row_task)
if previous_task:
previous_task.next_tasks = [row_task.name]
previous_task = row_task
logging.info(f"Created {task_count} subtasks from CSV file for {loop_task.name}")
else:
# Handle text files
with open(loop_task.input_file, "r", encoding="utf-8") as f:
lines = f.read().splitlines()
previous_task = None
for i, line in enumerate(lines):
if not line.strip(): # Skip empty lines
continue
row_task = Task(
description=f"{loop_task.description}\n{line.strip()}" if loop_task.description else line.strip(),
agent=loop_task.agent,
name=f"{loop_task.name}_{i+1}" if loop_task.name else line.strip(),
expected_output=getattr(loop_task, 'expected_output', None),
callback=loop_task.callback, # Inherit callback from parent loop task
is_start=(i == 0),
task_type="task"
)
self.tasks[row_task.id] = row_task
new_tasks.append(row_task)
if previous_task:
previous_task.next_tasks = [row_task.name]
previous_task = row_task
logging.info(f"Created {len(new_tasks)} subtasks from text file for {loop_task.name}")
if new_tasks and loop_task.next_tasks:
# Connect last subtask to loop task's next tasks
last_task = new_tasks[-1]
last_task.next_tasks = loop_task.next_tasks
except Exception as e:
logging.error(f"Failed to create subtasks for loop task {loop_task.name}: {e}")
import traceback
traceback.print_exc()
def _build_task_context(self, current_task: Task) -> str:
"""Build context for a task based on its retain_full_context setting"""
@@ -809,6 +889,7 @@ Provide a JSON with the structure:
agent=start_task.agent,
name=f"{start_task.name}_{task_count}" if start_task.name else task_desc,
expected_output=getattr(start_task, 'expected_output', None),
callback=start_task.callback, # Inherit callback from parent loop task
is_start=(task_count == 1),
task_type="decision", # Change to decision type
next_tasks=inherited_next_tasks, # Inherit parent's next tasks
@@ -842,6 +923,7 @@ Provide a JSON with the structure:
agent=start_task.agent,
name=f"{start_task.name}_{i+1}" if start_task.name else line.strip(),
expected_output=getattr(start_task, 'expected_output', None),
callback=start_task.callback, # Inherit callback from parent loop task
is_start=(i == 0),
task_type="task",
condition={
@@ -925,6 +1007,7 @@ Tasks by type:
agent=current_task.agent,
name=f"{current_task.name}_{i+1}" if current_task.name else task_desc,
expected_output=getattr(current_task, 'expected_output', None),
callback=current_task.callback, # Inherit callback from parent loop task
is_start=(i == 0),
task_type="task",
condition={
@@ -949,6 +1032,7 @@ Tasks by type:
agent=current_task.agent,
name=f"{current_task.name}_{i+1}" if current_task.name else line.strip(),
expected_output=getattr(current_task, 'expected_output', None),
callback=current_task.callback, # Inherit callback from parent loop task
is_start=(i == 0),
task_type="task",
condition={
@@ -381,13 +381,10 @@ class Task:
logger.exception(e) # Print full stack trace
# Continue execution even if memory operations fail
# Execute original callback
# Execute original callback with metadata support
if self.callback:
try:
if asyncio.iscoroutinefunction(self.callback):
await self.callback(task_output)
else:
self.callback(task_output)
await self._execute_callback_with_metadata(task_output)
except Exception as e:
logger.error(f"Task {self.id}: Failed to execute callback: {e}")
logger.exception(e)
@@ -470,4 +467,63 @@ Context:
success=False,
result=None,
error=f"Guardrail validation error: {str(e)}"
)
)
async def _execute_callback_with_metadata(self, task_output):
"""Execute callback with metadata support while maintaining backward compatibility.
This method automatically detects the callback signature:
- Single parameter callbacks receive only TaskOutput (backward compatible)
- Two parameter callbacks receive TaskOutput and metadata dict (enhanced)
Args:
task_output: The TaskOutput object to pass to the callback
"""
if not self.callback:
return
try:
# Inspect the callback signature to determine parameter count
sig = inspect.signature(self.callback)
param_count = len(sig.parameters)
if param_count == 1:
# Backward compatible: single parameter callback
if asyncio.iscoroutinefunction(self.callback):
await self.callback(task_output)
else:
self.callback(task_output)
elif param_count >= 2:
# Enhanced: two parameter callback with metadata
metadata = {
'task_id': self.id,
'task_name': self.name,
'agent_name': self.agent.name if self.agent else None,
'task_type': self.task_type,
'task_status': self.status,
'task_description': self.description,
'expected_output': self.expected_output,
'input_file': getattr(self, 'input_file', None),
'loop_state': self.loop_state,
'retry_count': getattr(self, 'retry_count', 0),
'async_execution': self.async_execution
}
if asyncio.iscoroutinefunction(self.callback):
await self.callback(task_output, metadata)
else:
self.callback(task_output, metadata)
else:
# No parameter callback - unusual but handle gracefully
if asyncio.iscoroutinefunction(self.callback):
await self.callback()
else:
self.callback()
except TypeError as e:
# Fallback for signature inspection issues
logger.warning(f"Task {self.id}: Callback signature inspection failed, falling back to single parameter: {e}")
if asyncio.iscoroutinefunction(self.callback):
await self.callback(task_output)
else:
self.callback(task_output)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.131"
version = "0.0.132"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
@@ -0,0 +1,341 @@
#!/usr/bin/env python3
"""
Simple test script to isolate the duplicate callback issue #878.
This script directly tests the LLM class to determine where the duplicate callbacks originate.
ROOT CAUSE IDENTIFIED:
In llm.py, there are two separate callback execution paths:
1. execute_sync_callback() called directly (lines 851-857, 885-891, etc.)
2. display_interaction() called when verbose=True (line 896, etc.)
Since display_interaction() ALSO executes sync callbacks internally (main.py lines 164-190),
this results in duplicate callback execution for the same LLM response.
The issue occurs specifically when verbose=True because both callback paths are active.
"""
import os
import sys
# Add the path to the praisonaiagents module
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'praisonai-agents'))
from praisonaiagents import register_display_callback
from praisonaiagents.llm.llm import LLM
# Global counter and log
callback_invocations = []
def track_callback(message=None, response=None, **kwargs):
"""
Callback function that tracks each invocation with detailed information.
"""
invocation = {
'count': len(callback_invocations) + 1,
'message': str(message)[:100] if message else None,
'response': str(response)[:100] if response else None,
'kwargs_keys': list(kwargs.keys()) if kwargs else [],
'stack_trace': get_simplified_stack_trace()
}
callback_invocations.append(invocation)
print(f"🔔 CALLBACK #{invocation['count']}")
print(f" Message: {invocation['message']}")
print(f" Response: {invocation['response']}")
print(f" Called from: {invocation['stack_trace']}")
print()
def get_simplified_stack_trace():
"""Get a simplified stack trace to see where the callback is called from."""
import traceback
import inspect
# Get the current stack
stack = inspect.stack()
# Find relevant frames (skip our own callback function)
relevant_frames = []
for frame in stack[2:8]: # Skip track_callback and get_simplified_stack_trace
filename = os.path.basename(frame.filename)
if 'praisonaiagents' in frame.filename:
relevant_frames.append(f"{filename}:{frame.lineno} in {frame.function}")
return " -> ".join(relevant_frames) if relevant_frames else "Unknown"
def test_llm_streaming():
"""Test LLM with streaming enabled."""
print("🧪 TEST 1: LLM with streaming")
print("-" * 40)
global callback_invocations
callback_invocations = []
# Register callback
register_display_callback('interaction', track_callback, is_async=False)
try:
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
response = llm.get_response(
prompt="Say exactly: Hello World",
verbose=True, # This should trigger the callback
stream=True # Test with streaming
)
print(f"✅ Response: {response}")
print(f"📊 Total callback invocations: {len(callback_invocations)}")
return len(callback_invocations)
except Exception as e:
print(f"❌ Error: {e}")
return -1
def test_llm_non_streaming():
"""Test LLM without streaming."""
print("🧪 TEST 2: LLM without streaming")
print("-" * 40)
global callback_invocations
callback_invocations = []
# Register callback
register_display_callback('interaction', track_callback, is_async=False)
try:
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
response = llm.get_response(
prompt="Say exactly: Hello World",
verbose=True, # This should trigger the callback
stream=False # Test without streaming
)
print(f"✅ Response: {response}")
print(f"📊 Total callback invocations: {len(callback_invocations)}")
return len(callback_invocations)
except Exception as e:
print(f"❌ Error: {e}")
return -1
def test_llm_with_self_reflection():
"""Test LLM with self-reflection enabled."""
print("🧪 TEST 3: LLM with self-reflection")
print("-" * 40)
global callback_invocations
callback_invocations = []
# Register callback
register_display_callback('interaction', track_callback, is_async=False)
try:
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False, self_reflect=True)
response = llm.get_response(
prompt="Say exactly: Hello World",
verbose=True, # This should trigger the callback
self_reflect=True,
min_reflect=1,
max_reflect=1
)
print(f"✅ Response: {response}")
print(f"📊 Total callback invocations: {len(callback_invocations)}")
return len(callback_invocations)
except Exception as e:
print(f"❌ Error: {e}")
return -1
def test_verbose_vs_non_verbose():
"""Test the key difference: verbose=True vs verbose=False to isolate the duplicate issue."""
print("🧪 TEST 4: Verbose vs Non-Verbose (Key Test for Duplicate Issue)")
print("-" * 60)
global callback_invocations
# Test with verbose=False (should trigger callback once via execute_sync_callback)
print(" 🔹 Testing with verbose=False:")
callback_invocations = []
register_display_callback('interaction', track_callback, is_async=False)
try:
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
response = llm.get_response(
prompt="Say exactly: Test Non-Verbose",
verbose=False # This should only trigger execute_sync_callback
)
print(f" Response: {response}")
print(f" Callbacks with verbose=False: {len(callback_invocations)}")
non_verbose_count = len(callback_invocations)
except Exception as e:
print(f" ❌ Error: {e}")
non_verbose_count = -1
print()
# Test with verbose=True (should trigger callback twice: execute_sync_callback + display_interaction)
print(" 🔹 Testing with verbose=True:")
callback_invocations = []
register_display_callback('interaction', track_callback, is_async=False)
try:
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
response = llm.get_response(
prompt="Say exactly: Test Verbose",
verbose=True # This should trigger BOTH execute_sync_callback AND display_interaction
)
print(f" Response: {response}")
print(f" Callbacks with verbose=True: {len(callback_invocations)}")
verbose_count = len(callback_invocations)
# Show the call stack differences
if len(callback_invocations) > 1:
print(f"\n 📋 Call stack analysis:")
for i, inv in enumerate(callback_invocations):
print(f" Callback #{i+1}: {inv['stack_trace']}")
except Exception as e:
print(f" ❌ Error: {e}")
verbose_count = -1
print(f"\n 📊 RESULT:")
print(f" verbose=False: {non_verbose_count} callback(s)")
print(f" verbose=True: {verbose_count} callback(s)")
if verbose_count > non_verbose_count and verbose_count > 1:
print(f" 🚨 DUPLICATE ISSUE CONFIRMED!")
print(f" verbose=True triggers {verbose_count - non_verbose_count} extra callback(s)")
return verbose_count
def test_multiple_calls():
"""Test multiple consecutive calls to see pattern."""
print("🧪 TEST 5: Multiple consecutive calls")
print("-" * 40)
global callback_invocations
callback_invocations = []
# Register callback
register_display_callback('interaction', track_callback, is_async=False)
try:
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
for i in range(3):
print(f" Call {i+1}:")
response = llm.get_response(
prompt=f"Say exactly: Call {i+1}",
verbose=True
)
print(f" Response: {response}")
print(f"📊 Total callback invocations across 3 calls: {len(callback_invocations)}")
return len(callback_invocations)
except Exception as e:
print(f"❌ Error: {e}")
return -1
def analyze_callback_patterns():
"""Analyze the patterns in callback invocations."""
print("\n📊 CALLBACK PATTERN ANALYSIS")
print("=" * 50)
if not callback_invocations:
print("❌ No callback invocations recorded")
return
print(f"Total invocations: {len(callback_invocations)}")
print("\nDetailed breakdown:")
for inv in callback_invocations:
print(f" #{inv['count']}: {inv['stack_trace']}")
if inv['message']:
print(f" Message: {inv['message']}")
if inv['response']:
print(f" Response: {inv['response']}")
print()
# Look for duplicate patterns
unique_traces = set(inv['stack_trace'] for inv in callback_invocations)
if len(unique_traces) < len(callback_invocations):
print("⚠️ POTENTIAL DUPLICATE PATTERN DETECTED!")
print("Some callbacks are coming from the same call stack.")
else:
print("✅ All callbacks appear to come from different call paths.")
if __name__ == "__main__":
print("🔬 ISOLATING DUPLICATE CALLBACK ISSUE #878")
print("=" * 60)
print("This script tests the LLM class directly to isolate where duplicate callbacks occur.")
print()
# Run different test scenarios
results = {}
# Test 1: Streaming
results['streaming'] = test_llm_streaming()
print()
# Test 2: Non-streaming
results['non_streaming'] = test_llm_non_streaming()
print()
# Test 3: Self-reflection
results['self_reflection'] = test_llm_with_self_reflection()
print()
# Test 4: Verbose vs Non-Verbose (KEY TEST)
results['verbose_vs_nonverbose'] = test_verbose_vs_non_verbose()
print()
# Test 5: Multiple calls
results['multiple_calls'] = test_multiple_calls()
print()
# Analyze patterns
analyze_callback_patterns()
# Summary
print("\n🏁 SUMMARY OF RESULTS")
print("=" * 40)
expected_single_call_tests = ['streaming', 'non_streaming', 'self_reflection']
issues_detected = []
for test_name, count in results.items():
if count == -1:
print(f"{test_name}: ERROR")
elif test_name == 'multiple_calls':
expected = 3
print(f"{test_name}: {count} invocations (expected ~{expected})")
if count > expected * 1.5: # Allow some tolerance
issues_detected.append(f"{test_name}: {count} > expected {expected}")
elif test_name == 'verbose_vs_nonverbose':
# This is the key test - verbose=True should show the duplicate issue
print(f"{test_name}: {count} invocations (this test shows verbose=True duplicates)")
if count > 1:
issues_detected.append(f"{test_name}: verbose=True triggered {count} callbacks (duplicate issue)")
else:
expected = 1
print(f"{test_name}: {count} invocations (expected {expected})")
if count > expected:
issues_detected.append(f"{test_name}: {count} > expected {expected}")
if issues_detected:
print(f"\n❌ DUPLICATE CALLBACK ISSUES DETECTED:")
for issue in issues_detected:
print(f" - {issue}")
print("\nThe callback system is triggering multiple times when it should trigger once.")
sys.exit(1)
else:
print(f"\n✅ NO DUPLICATE CALLBACK ISSUES DETECTED")
print("All callback invocations appear to be working as expected.")
sys.exit(0)
@@ -0,0 +1,112 @@
#!/usr/bin/env python3
"""
Simple test to verify that the callback fix is working
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__)))
from praisonaiagents import register_display_callback
from praisonaiagents.main import display_interaction, display_self_reflection
# Track callback calls
callback_calls = []
def test_callback(message, response, **kwargs):
"""Test callback function"""
callback_calls.append({
'type': 'interaction',
'message': message,
'response': response
})
print(f"[CALLBACK] Interaction: {message[:30]}... -> {response[:30]}...")
def test_self_reflection_callback(message, **kwargs):
"""Test self-reflection callback function"""
callback_calls.append({
'type': 'self_reflection',
'message': message
})
print(f"[CALLBACK] Self-reflection: {message[:50]}...")
def test_display_interaction_callback():
"""Test that display_interaction calls the callback"""
global callback_calls
callback_calls = []
# Register callback
register_display_callback('interaction', test_callback)
try:
# Call display_interaction
display_interaction(
message="Test message",
response="Test response",
markdown=True,
generation_time=1.5
)
print(f"Callback calls: {len(callback_calls)}")
# Verify callback was called
assert len(callback_calls) == 1, f"Expected 1 callback call, got {len(callback_calls)}"
assert callback_calls[0]['type'] == 'interaction'
assert callback_calls[0]['message'] == "Test message"
assert callback_calls[0]['response'] == "Test response"
print("✓ display_interaction callback test PASSED")
finally:
# Clean up
from praisonaiagents.main import sync_display_callbacks
sync_display_callbacks.pop('interaction', None)
def test_display_self_reflection_callback():
"""Test that display_self_reflection calls the callback"""
global callback_calls
callback_calls = []
# Register callback
register_display_callback('self_reflection', test_self_reflection_callback)
try:
# Call display_self_reflection
display_self_reflection("This is a self-reflection message")
print(f"Self-reflection callback calls: {len(callback_calls)}")
# Verify callback was called
assert len(callback_calls) == 1, f"Expected 1 callback call, got {len(callback_calls)}"
assert callback_calls[0]['type'] == 'self_reflection'
assert callback_calls[0]['message'] == "This is a self-reflection message"
print("✓ display_self_reflection callback test PASSED")
finally:
# Clean up
from praisonaiagents.main import sync_display_callbacks
sync_display_callbacks.pop('self_reflection', None)
if __name__ == "__main__":
print("Testing callback fix for display functions...\n")
try:
print("1. Testing display_interaction callback...")
test_display_interaction_callback()
print()
print("2. Testing display_self_reflection callback...")
test_display_self_reflection_callback()
print()
print("All callback tests passed! The fix is working correctly.")
except AssertionError as e:
print(f"✗ FAILED: {e}")
sys.exit(1)
except Exception as e:
print(f"✗ ERROR: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
@@ -9,28 +9,32 @@ import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src/praisonai-agents'))
from praisonaiagents.llm.llm import LLM
from praisonaiagents import register_display_callback
from unittest.mock import patch, MagicMock
import json
# Track display_interaction calls
display_calls = []
def mock_display_interaction(prompt, response, markdown=True, generation_time=0, console=None):
def mock_display_interaction(message, response, markdown=True, generation_time=0, **kwargs):
"""Mock display_interaction to track calls"""
display_calls.append({
'prompt': prompt,
'message': message,
'response': response,
'markdown': markdown,
'generation_time': generation_time
})
print(f"[DISPLAY] {prompt[:50]}... -> {response[:50]}...")
print(f"[DISPLAY] {message[:50]}... -> {response[:50]}...")
def test_single_display_no_tools():
"""Test that display_interaction is called only once without tools"""
global display_calls
display_calls = []
with patch('praisonaiagents.main.display_interaction', side_effect=mock_display_interaction):
# Register callback instead of patching
register_display_callback('interaction', mock_display_interaction)
try:
with patch('litellm.completion') as mock_completion:
# Mock streaming response
mock_completion.return_value = [
@@ -51,13 +55,20 @@ def test_single_display_no_tools():
assert len(display_calls) == 1, f"Expected 1 display call, got {len(display_calls)}"
assert response == "Hello world!"
finally:
# Clean up callback to prevent interference with other tests
from praisonaiagents.main import sync_display_callbacks
sync_display_callbacks.pop('interaction', None)
def test_single_display_with_reasoning():
"""Test that display_interaction is called only once with reasoning steps"""
global display_calls
display_calls = []
with patch('praisonaiagents.main.display_interaction', side_effect=mock_display_interaction):
# Register callback instead of patching
register_display_callback('interaction', mock_display_interaction)
try:
with patch('litellm.completion') as mock_completion:
# Mock non-streaming response with reasoning
mock_completion.return_value = {
@@ -83,13 +94,20 @@ def test_single_display_with_reasoning():
print(f"Display calls: {len(display_calls)}")
assert len(display_calls) == 1, f"Expected 1 display call, got {len(display_calls)}"
finally:
# Clean up callback to prevent interference with other tests
from praisonaiagents.main import sync_display_callbacks
sync_display_callbacks.pop('interaction', None)
def test_single_display_with_self_reflection():
"""Test that display_interaction is called appropriately with self-reflection"""
global display_calls
display_calls = []
with patch('praisonaiagents.main.display_interaction', side_effect=mock_display_interaction):
# Register callback instead of patching
register_display_callback('interaction', mock_display_interaction)
try:
with patch('praisonaiagents.main.display_self_reflection'):
with patch('litellm.completion') as mock_completion:
# First call - initial response
@@ -137,6 +155,10 @@ def test_single_display_with_self_reflection():
# Should display only the final response
assert len(display_calls) == 1, f"Expected 1 display call, got {len(display_calls)}"
assert response == "Better response"
finally:
# Clean up callback to prevent interference with other tests
from praisonaiagents.main import sync_display_callbacks
sync_display_callbacks.pop('interaction', None)
def test_async_single_display():
"""Test async version also prevents duplicate displays"""
@@ -145,30 +167,37 @@ def test_async_single_display():
import asyncio
async def run_test():
with patch('praisonaiagents.main.display_interaction', side_effect=mock_display_interaction):
with patch('litellm.acompletion') as mock_acompletion:
# Mock async streaming response
async def async_generator():
yield MagicMock(choices=[MagicMock(delta=MagicMock(content="Async"))])
yield MagicMock(choices=[MagicMock(delta=MagicMock(content=" response"))])
mock_acompletion.return_value = async_generator()
llm = LLM(model="gpt-4o-mini", verbose=False)
response = await llm.get_response_async(
prompt="Test async",
verbose=True,
stream=True
)
print(f"\nAsync Response: {response}")
print(f"Display calls: {len(display_calls)}")
assert len(display_calls) == 1, f"Expected 1 display call, got {len(display_calls)}"
assert response == "Async response"
# Register callback instead of patching
register_display_callback('interaction', mock_display_interaction)
asyncio.run(run_test())
async def run_test():
with patch('litellm.acompletion') as mock_acompletion:
# Mock async streaming response
async def async_generator():
yield MagicMock(choices=[MagicMock(delta=MagicMock(content="Async"))])
yield MagicMock(choices=[MagicMock(delta=MagicMock(content=" response"))])
mock_acompletion.return_value = async_generator()
llm = LLM(model="gpt-4o-mini", verbose=False)
response = await llm.get_response_async(
prompt="Test async",
verbose=True,
stream=True
)
print(f"\nAsync Response: {response}")
print(f"Display calls: {len(display_calls)}")
assert len(display_calls) == 1, f"Expected 1 display call, got {len(display_calls)}"
assert response == "Async response"
try:
asyncio.run(run_test())
finally:
# Clean up callback to prevent interference with other tests
from praisonaiagents.main import sync_display_callbacks
sync_display_callbacks.pop('interaction', None)
if __name__ == "__main__":
print("Testing duplicate callback fix for issue #878...\n")
+2 -2
View File
@@ -2,7 +2,7 @@
"""Simple test to verify the self-reflection fix works"""
from praisonaiagents import Agent
from praisonaiagents.tools import calculator
from praisonaiagents.tools import evaluate
def test_self_reflection_fix():
"""Test that self-reflection works with tools after the fix"""
@@ -16,7 +16,7 @@ def test_self_reflection_fix():
self_reflect=True,
llm="gpt-4o-mini", # Use a more widely available model
verbose=True,
tools=[calculator],
tools=[evaluate],
min_reflect=1,
max_reflect=2
)
+136 -43
View File
@@ -1,60 +1,153 @@
#!/usr/bin/env python3
"""Test to verify the self-reflection fix works with tools"""
"""
Test script to verify the duplicate callback fix for issue #878.
This tests that callbacks are triggered exactly once per LLM response.
"""
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.tools import calculator
import os
import sys
def test_self_reflection_fix_verification():
"""Test that self-reflection now works with tools"""
print("=== Testing Self-Reflection Fix Verification ===")
# Add the path to the praisonaiagents module
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src', 'praisonai-agents'))
from praisonaiagents import register_display_callback
from praisonaiagents.llm.llm import LLM
# Track callback invocations
callback_count = 0
callback_details = []
def test_callback(message=None, response=None, **kwargs):
"""Callback function to track invocations."""
global callback_count, callback_details
callback_count += 1
callback_details.append({
'count': callback_count,
'message': str(message)[:50] if message else None,
'response': str(response)[:50] if response else None,
'kwargs': list(kwargs.keys()) if kwargs else []
})
print(f"🔔 CALLBACK #{callback_count}: {response[:50] if response else 'No response'}")
def test_verbose_true():
"""Test with verbose=True (this was causing duplicates before the fix)."""
global callback_count, callback_details
callback_count = 0
callback_details = []
# Create an agent with self-reflection enabled and tools
agent = Agent(
name="MathAgent",
role="Math Assistant",
goal="Solve mathematical problems accurately",
backstory="You are a helpful math assistant who double-checks calculations",
tools=[calculator],
self_reflect=True, # This should now work with tools
min_reflect=1,
max_reflect=2,
verbose=True
)
# Define a task that would benefit from self-reflection
task = Task(
description="Calculate 123 * 456 and verify the result is correct",
expected_output="The calculation result with verification",
agent=agent,
name="math_calculation"
)
# Create and run the agents
agents = PraisonAIAgents(
agents=[agent],
tasks=[task],
process="sequential"
)
print("🧪 Testing with verbose=True...")
register_display_callback('interaction', test_callback, is_async=False)
try:
result = agents.start()
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
response = llm.get_response(
prompt="Say exactly: Hello World",
verbose=True # This should NOT cause duplicate callbacks after the fix
)
if result:
print(f"\n✅ SUCCESS: Self-reflection with tools is working!")
print(f"Result: {result}")
print(f"✅ Response: {response}")
print(f"📊 Callback count: {callback_count}")
if callback_count == 1:
print("✅ SUCCESS: Exactly 1 callback triggered (no duplicates)")
return True
else:
print(f"\n❌ FAILED: No result returned")
print(f"❌ FAIL: Expected 1 callback, got {callback_count}")
return False
except Exception as e:
print(f"\n❌ ERROR: {str(e)}")
print(f"❌ ERROR: {e}")
return False
def test_verbose_false():
"""Test with verbose=False (should still work)."""
global callback_count, callback_details
callback_count = 0
callback_details = []
print("\n🧪 Testing with verbose=False...")
register_display_callback('interaction', test_callback, is_async=False)
try:
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
response = llm.get_response(
prompt="Say exactly: Hello Non-Verbose",
verbose=False
)
print(f"✅ Response: {response}")
print(f"📊 Callback count: {callback_count}")
if callback_count == 1:
print("✅ SUCCESS: Exactly 1 callback triggered")
return True
else:
print(f"❌ FAIL: Expected 1 callback, got {callback_count}")
return False
except Exception as e:
print(f"❌ ERROR: {e}")
return False
def test_multiple_calls():
"""Test multiple consecutive calls."""
global callback_count, callback_details
callback_count = 0
callback_details = []
print("\n🧪 Testing multiple consecutive calls...")
register_display_callback('interaction', test_callback, is_async=False)
try:
llm = LLM(model="gemini/gemini-2.5-flash-lite-preview-06-17", verbose=False)
for i in range(3):
response = llm.get_response(
prompt=f"Say exactly: Call {i+1}",
verbose=True
)
print(f" Call {i+1}: {response}")
print(f"📊 Total callback count: {callback_count}")
if callback_count == 3:
print("✅ SUCCESS: Exactly 3 callbacks for 3 calls (1 each)")
return True
else:
print(f"❌ FAIL: Expected 3 callbacks, got {callback_count}")
return False
except Exception as e:
print(f"❌ ERROR: {e}")
return False
if __name__ == "__main__":
success = test_self_reflection_fix_verification()
print("🔧 TESTING DUPLICATE CALLBACK FIX FOR ISSUE #878")
print("=" * 60)
if success:
print("\n🎉 Fix verification successful! Self-reflection now works with tools.")
results = []
# Test 1: verbose=True (the main issue)
results.append(test_verbose_true())
# Test 2: verbose=False (should still work)
results.append(test_verbose_false())
# Test 3: multiple calls
results.append(test_multiple_calls())
# Summary
print(f"\n🏁 SUMMARY")
print("=" * 30)
success_count = sum(results)
total_tests = len(results)
print(f"Tests passed: {success_count}/{total_tests}")
if success_count == total_tests:
print("✅ ALL TESTS PASSED! The duplicate callback issue is FIXED.")
sys.exit(0)
else:
print("\n💥 Fix verification failed. Self-reflection with tools is still not working.")
print("❌ SOME TESTS FAILED! The duplicate callback issue still exists.")
sys.exit(1)
+2 -2
View File
@@ -2,7 +2,7 @@
"""Test LLM class directly to verify self-reflection fix"""
from praisonaiagents.llm import LLM
from praisonaiagents.tools import calculator
from praisonaiagents.tools import evaluate
def test_llm_direct():
"""Test LLM class directly with self-reflection and tools"""
@@ -15,7 +15,7 @@ def test_llm_direct():
response = llm.get_response(
prompt="Calculate 15 * 23 and verify your answer",
system_prompt="You are a helpful math assistant. Use tools when needed.",
tools=[calculator],
tools=[evaluate],
self_reflect=True,
min_reflect=1,
max_reflect=2,
@@ -0,0 +1,177 @@
#!/usr/bin/env python3
"""
Test script to verify self-reflection fix for issue #901
Tests both with and without tools to ensure fix works correctly
"""
import sys
import os
from pathlib import Path
# Add the src directory to the path
sys.path.insert(0, str(Path(__file__).parent / "src" / "praisonai-agents"))
from praisonaiagents import Agent, Task, PraisonAIAgents
def create_mock_tool():
"""Create a mock tool that mimics the user's custom search tool"""
def mock_search_tool(query: str) -> str:
"""Mock tool that returns a fake search result"""
return f"Mock search result for: {query}"
# Set the function name as expected by the tool system
mock_search_tool.__name__ = "mock_search_tool"
return mock_search_tool
def test_self_reflection_with_tools():
"""Test that self-reflection works when tools are present"""
print("Testing self-reflection WITH tools...")
# Create mock tool
mock_tool = create_mock_tool()
# Create an agent with self-reflection and tools
agent = Agent(
role="Senior Research Analyst",
goal="Analyze and provide insights on given topics",
backstory="You are an expert analyst with strong critical thinking skills",
self_reflect=True,
llm="gpt-4o-mini", # Using OpenAI model for testing
verbose=True,
tools=[mock_tool],
max_reflect=2, # Keep it small for testing
min_reflect=1
)
# Create a task
task = Task(
description="Analyze the importance of AI in modern business",
expected_output="A brief analysis report",
agent=agent
)
# Create and start the agents
try:
agents = PraisonAIAgents(
agents=[agent],
tasks=[task],
process="sequential"
)
result = agents.start()
print("✅ Self-reflection with tools completed successfully!")
print(f"Result: {result}")
return True
except Exception as e:
print(f"❌ Self-reflection with tools failed: {e}")
return False
def test_self_reflection_without_tools():
"""Test that self-reflection still works without tools (baseline)"""
print("\nTesting self-reflection WITHOUT tools...")
# Create an agent with self-reflection but no tools
agent = Agent(
role="Senior Research Analyst",
goal="Analyze and provide insights on given topics",
backstory="You are an expert analyst with strong critical thinking skills",
self_reflect=True,
llm="gpt-4o-mini", # Using OpenAI model for testing
verbose=True,
max_reflect=2, # Keep it small for testing
min_reflect=1
)
# Create a task
task = Task(
description="Analyze the importance of AI in modern business",
expected_output="A brief analysis report",
agent=agent
)
# Create and start the agents
try:
agents = PraisonAIAgents(
agents=[agent],
tasks=[task],
process="sequential"
)
result = agents.start()
print("✅ Self-reflection without tools completed successfully!")
print(f"Result: {result}")
return True
except Exception as e:
print(f"❌ Self-reflection without tools failed: {e}")
return False
def test_llm_directly():
"""Test LLM class directly to isolate the issue"""
print("\nTesting LLM class directly...")
from praisonaiagents.llm import LLM
# Create mock tool
mock_tool = create_mock_tool()
# Create LLM instance
llm = LLM(model="gpt-4o-mini")
def mock_tool_executor(function_name, arguments):
"""Mock tool executor that simulates tool execution"""
if function_name == "mock_search_tool":
return mock_tool(arguments.get("query", "test"))
return None
try:
# Test with tools and self-reflection
response = llm.get_response(
prompt="Analyze the importance of AI in modern business",
tools=[mock_tool],
self_reflect=True,
max_reflect=2,
min_reflect=1,
verbose=True,
execute_tool_fn=mock_tool_executor
)
print("✅ LLM direct test with tools completed successfully!")
print(f"Response: {response}")
return True
except Exception as e:
print(f"❌ LLM direct test with tools failed: {e}")
return False
if __name__ == "__main__":
# Check if we have required environment variables
if not os.getenv("OPENAI_API_KEY"):
print("❌ OPENAI_API_KEY environment variable is required for testing")
sys.exit(1)
print("Testing self-reflection fix for issue #901")
print("=" * 50)
# Run tests
test_results = []
# Test 1: Without tools (baseline)
test_results.append(test_self_reflection_without_tools())
# Test 2: With tools (the fixed case)
test_results.append(test_self_reflection_with_tools())
# Test 3: Direct LLM test
test_results.append(test_llm_directly())
# Summary
print("\n" + "=" * 50)
print("Test Summary:")
print(f"✅ Tests passed: {sum(test_results)}")
print(f"❌ Tests failed: {len(test_results) - sum(test_results)}")
if all(test_results):
print("🎉 All tests passed! Self-reflection fix is working correctly.")
sys.exit(0)
else:
print("💥 Some tests failed. Please check the output above.")
sys.exit(1)
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""Test self-reflection functionality with tools to verify the fix for issue #901"""
import sys
import os
from pathlib import Path
# Add the src directory to the path
sys.path.insert(0, str(Path(__file__).parent / "src" / "praisonai-agents"))
from praisonaiagents import Agent
from praisonaiagents.llm import LLM
def simple_calculator(operation: str, a: int, b: int) -> int:
"""A simple calculator tool"""
if operation == "add":
return a + b
elif operation == "subtract":
return a - b
elif operation == "multiply":
return a * b
elif operation == "divide":
return a / b if b != 0 else "Cannot divide by zero"
else:
return "Unknown operation"
def test_llm_self_reflection_with_tools():
"""Test self-reflection in LLM class directly with tools"""
print("=== Testing LLM Self-Reflection WITH Tools ===")
llm = LLM(model="gpt-4o-mini")
def mock_tool_executor(function_name, arguments):
"""Mock tool executor for testing"""
if function_name == "simple_calculator":
return simple_calculator(
arguments.get("operation", "add"),
arguments.get("a", 0),
arguments.get("b", 0)
)
return None
response = llm.get_response(
prompt="Calculate 5 + 3 and then reflect on your answer",
system_prompt="You are a helpful assistant with access to a calculator tool.",
tools=[simple_calculator],
self_reflect=True,
min_reflect=1,
max_reflect=2,
verbose=True,
execute_tool_fn=mock_tool_executor
)
print(f"LLM Response with tools: {response}")
print()
def test_llm_self_reflection_without_tools():
"""Test self-reflection in LLM class directly without tools"""
print("=== Testing LLM Self-Reflection WITHOUT Tools ===")
llm = LLM(model="gpt-4o-mini")
response = llm.get_response(
prompt="Calculate 5 + 3 and then reflect on your answer",
system_prompt="You are a helpful assistant.",
self_reflect=True,
min_reflect=1,
max_reflect=2,
verbose=True
)
print(f"LLM Response without tools: {response}")
print()
def test_agent_self_reflection_with_tools():
"""Test self-reflection in Agent class with tools"""
print("=== Testing Agent Self-Reflection WITH Tools ===")
agent = Agent(
name="CalculatorAgent",
instructions="You are a helpful assistant with access to a calculator tool.",
llm="gpt-4o-mini",
self_reflect=True,
min_reflect=1,
max_reflect=2,
tools=[simple_calculator]
)
response = agent.start("Calculate 5 + 3 and then reflect on your answer")
print(f"Agent Response with tools: {response}")
print()
def test_agent_self_reflection_without_tools():
"""Test self-reflection in Agent class without tools"""
print("=== Testing Agent Self-Reflection WITHOUT Tools ===")
agent = Agent(
name="BasicAgent",
instructions="You are a helpful assistant.",
llm="gpt-4o-mini",
self_reflect=True,
min_reflect=1,
max_reflect=2
)
response = agent.start("Calculate 5 + 3 and then reflect on your answer")
print(f"Agent Response without tools: {response}")
print()
if __name__ == "__main__":
import os
# Check if we have the required API key
if not os.getenv("OPENAI_API_KEY"):
print("❌ OPENAI_API_KEY environment variable is required for testing")
print("Set it with: export OPENAI_API_KEY='your-api-key-here'")
exit(1)
print("Testing self-reflection with tools fix for issue #901")
print("=" * 60)
try:
# Test baseline (without tools)
test_llm_self_reflection_without_tools()
test_agent_self_reflection_without_tools()
# Test the fix (with tools)
test_llm_self_reflection_with_tools()
test_agent_self_reflection_with_tools()
print("✅ All tests completed successfully!")
print("🎉 Self-reflection with tools is now working correctly!")
except Exception as e:
print(f"❌ Test failed: {e}")
import traceback
traceback.print_exc()
exit(1)
@@ -0,0 +1,131 @@
#!/usr/bin/env python3
"""
Test script that replicates the exact user code pattern from issue #901
"""
import sys
import os
from pathlib import Path
# Add the src directory to the path
sys.path.insert(0, str(Path(__file__).parent / "src" / "praisonai-agents"))
from praisonaiagents import Agent, Task, PraisonAIAgents
def google_web_search_llm(query: str) -> str:
"""Mock implementation of the user's custom search tool"""
return f"Mock Google search result for: {query}"
def test_user_code_pattern_with_tools():
"""Test the exact user code pattern WITH tools"""
print("Testing user code pattern WITH tools...")
# Create an agent with self-reflection (exact user code pattern)
agent = Agent(
role="Senior Research Analyst",
goal="Analyze and provide insights on given topics",
backstory="You are an expert analyst with strong critical thinking skills",
self_reflect=True,
llm="gpt-4o-mini", # Using OpenAI instead of Gemini for testing
verbose=True,
tools=[google_web_search_llm]
)
# Create a task
task = Task(
description="Analyze recent developments in AI",
expected_output="A detailed analysis report",
agent=agent
)
# Create and start the agents
try:
agents = PraisonAIAgents(
agents=[agent],
tasks=[task],
process="sequential"
)
# Start execution
result = agents.start()
print("✅ User code pattern WITH tools completed successfully!")
print(f"Result type: {type(result)}")
print(f"Result: {result}")
return True
except Exception as e:
print(f"❌ User code pattern WITH tools failed: {e}")
import traceback
traceback.print_exc()
return False
def test_user_code_pattern_without_tools():
"""Test the exact user code pattern WITHOUT tools"""
print("\nTesting user code pattern WITHOUT tools...")
# Create an agent with self-reflection (exact user code pattern, no tools)
agent = Agent(
role="Senior Research Analyst",
goal="Analyze and provide insights on given topics",
backstory="You are an expert analyst with strong critical thinking skills",
self_reflect=True,
llm="gpt-4o-mini", # Using OpenAI instead of Gemini for testing
verbose=True
)
# Create a task
task = Task(
description="Analyze recent developments in AI",
expected_output="A detailed analysis report",
agent=agent
)
# Create and start the agents
try:
agents = PraisonAIAgents(
agents=[agent],
tasks=[task],
process="sequential"
)
# Start execution
result = agents.start()
print("✅ User code pattern WITHOUT tools completed successfully!")
print(f"Result type: {type(result)}")
print(f"Result: {result}")
return True
except Exception as e:
print(f"❌ User code pattern WITHOUT tools failed: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
# Check if we have required environment variables
if not os.getenv("OPENAI_API_KEY"):
print("❌ OPENAI_API_KEY environment variable is required for testing")
print("Please set it with: export OPENAI_API_KEY='your-api-key-here'")
sys.exit(1)
print("Testing exact user code pattern from issue #901")
print("=" * 60)
# Run tests
test_results = []
# Test 1: Without tools (baseline - should work)
test_results.append(test_user_code_pattern_without_tools())
# Test 2: With tools (the problematic case that should now work)
test_results.append(test_user_code_pattern_with_tools())
# Summary
print("\n" + "=" * 60)
print("Test Summary:")
print(f"✅ Tests passed: {sum(test_results)}")
print(f"❌ Tests failed: {len(test_results) - sum(test_results)}")
if all(test_results):
print("🎉 All tests passed! The user's exact code pattern now works with self-reflection and tools.")
sys.exit(0)
else:
print("💥 Some tests failed. The fix may not be complete.")
sys.exit(1)
+1 -1
View File
@@ -2375,7 +2375,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.131"
version = "0.0.132"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.57.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.57.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.58.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.58.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.57 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.2.58 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.57"
version = "2.2.58"
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.131",
"praisonaiagents>=0.0.132",
"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.57"
version = "2.2.58"
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.131"
praisonaiagents = ">=0.0.132"
python-dotenv = ">=0.19.0"
instructor = ">=1.3.3"
PyYAML = ">=6.0"
+5 -5
View File
@@ -3932,7 +3932,7 @@ wheels = [
[[package]]
name = "praisonai"
version = "2.2.57"
version = "2.2.58"
source = { editable = "." }
dependencies = [
{ name = "instructor" },
@@ -4074,7 +4074,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.131" },
{ name = "praisonaiagents", specifier = ">=0.0.132" },
{ 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" },
@@ -4132,7 +4132,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.131"
version = "0.0.132"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohttp" },
@@ -4142,9 +4142,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/a8/6c8b10f2254a5914a0c282e8691bea7f3fff60ea99d975db11e8d3382c9f/praisonaiagents-0.0.131.tar.gz", hash = "sha256:8b6ceef477f2c1d4e33af1242c52cf1956826066ef4d7ed3bc2dcd50ce1c68c8", size = 198480, upload-time = "2025-07-14T17:35:51.53Z" }
sdist = { url = "https://files.pythonhosted.org/packages/6e/a9/5b9433915eadb1401ae2207f5e76915dc66d060ce80637238242747d92f4/praisonaiagents-0.0.132.tar.gz", hash = "sha256:a418eea0c54796071a85d3759bca489d15a1e8896556f61da22f7c1bf76888af", size = 200255, upload-time = "2025-07-14T20:11:49.494Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/d7/c443377ac2eaf68154b4ae6f87e5bc681f80774f8af9fcb368454ea9a7d8/praisonaiagents-0.0.131-py3-none-any.whl", hash = "sha256:fac2cdfe78da0fea89a74d54aab2fa9e920d61ca8d08ccebeb58132fc3770dc8", size = 218389, upload-time = "2025-07-14T17:35:49.94Z" },
{ url = "https://files.pythonhosted.org/packages/20/ad/90acd0d5f1ffed9d723d59d73d63d50b4d9ee1ac2af6dd34bc62d0a4634a/praisonaiagents-0.0.132-py3-none-any.whl", hash = "sha256:408f08be024a4856608690e8d583655f14060cacb78df4c85cf83df4b15e90ce", size = 220196, upload-time = "2025-07-14T20:11:48.265Z" },
]
[[package]]