Compare commits

...
30 Commits
Author SHA1 Message Date
Mervin Praison 31bae3a231 chore: bump PraisonAI to 2.2.54 and update Gemini workflows 2025-07-13 21:49:21 +01:00
Mervin Praison 466bcfedd7 Merge pull request #863 from MervinPraison/gemini/issue-862-fix-async-loop
Fix #862: Agent gets stuck in infinite loop when running multiple async tasks
2025-07-13 20:46:57 +01:00
Mervin Praison c24bf80314 Merge pull request #858 from MervinPraison/gemini/issue-854-fix-ollama-sequential-1690000000
Fix #854: Ollama sequential still failing
2025-07-13 20:46:21 +01:00
Mervin Praison c36b3aee6e Merge pull request #865 from MervinPraison/gemini/issue-864-fix-parallel-task-loop
Fix #864: Fix parallel task loop
2025-07-13 20:45:55 +01:00
Mervin Praison 08512ba6d3 Merge pull request #861 from MervinPraison/gemini/issue-860-fix-hierarchical-llm-fallback
Fix #860: Hierarchical Process LLM Fallback
2025-07-13 20:43:53 +01:00
claude[bot]andMervin Praison 63ebfc0b54 fix: Complete removal of Ollama special handling for async method
This completes the fix for issue #854 by removing the Ollama special
handling from the async method (get_response_async) to match the sync
method fix. Also removes the now-unused _handle_ollama_model method.

The special handling was causing infinite loops in sequential tool
calling. By removing it, Ollama models now work correctly through
the standard litellm interface.

Co-authored-by: Mervin Praison <[email protected]>
2025-07-13 19:21:40 +00:00
claude[bot]andMervin Praison b29269071d fix: properly handle parallel task reset logic to prevent infinite loops
- Uncommented the task reset logic that was causing issue #864
- Added check for async_execution to prevent resetting parallel tasks
- Preserves backward compatibility for non-parallel tasks that need rerun
- Fixes infinite loop issue while maintaining existing functionality

Co-authored-by: Mervin Praison <[email protected]>
2025-07-13 19:14:59 +00:00
Gemini Bot b6a86bf348 Gemini: [Issue #864] Fix parallel task loop 2025-07-13 12:49:40 +00:00
Gemini Bot 02ba71be3d Gemini: [Issue #862] Fix infinite loop in async workflow 2025-07-13 12:17:37 +00:00
Gemini Bot e60f1c1aa1 Gemini: [Issue #860] Fix hierarchical process LLM fallback 2025-07-13 11:31:08 +00:00
Mervin Praison 450f237805 feat: add description field to TaskOutput in validation feedback tests 2025-07-13 01:17:26 +01:00
Mervin Praison 66e53fc82e refactor: move validation test files to src directory and update LLM model references 2025-07-13 01:09:52 +01:00
Mervin Praison 7e6ee5f9f2 chore: enhance variable handling in PR review workflow for improved clarity 2025-07-13 01:04:41 +01:00
Mervin Praison b669b9815d fix: correct variable interpolation syntax in PR review workflow 2025-07-13 00:50:39 +01:00
Mervin Praison 6d89cd603e chore: remove debug workflow trigger step from Gemini PR review action 2025-07-13 00:44:38 +01:00
Mervin Praison 6e955b5941 feat: restrict Gemini PR review to repo owners, members and collaborators 2025-07-13 00:34:48 +01:00
Mervin Praison 87a67b5f5d refactor: improve PR checkout logic in Gemini review workflow using dedicated step 2025-07-13 00:25:08 +01:00
Mervin Praison baec9cd0a4 fix: update checkout ref to handle both PR and issue comment triggers 2025-07-13 00:23:16 +01:00
Mervin Praison e144dc497d feat: allow all users to trigger Gemini PR review and add debug logging 2025-07-13 00:19:06 +01:00
Gemini Bot 8a77ab0ab5 Gemini: [Issue #854] Fix Ollama sequential tool calling loop 2025-07-12 23:10:23 +00:00
Mervin Praison 0034b5c964 Merge branch 'main' of https://github.com/MervinPraison/PraisonAI 2025-07-13 00:04:14 +01:00
Mervin Praison 543c627374 feat: expand GitHub Actions permissions to include PR list and granular git commands 2025-07-13 00:04:13 +01:00
Mervin Praison 93154adf9e Merge pull request #857 from MervinPraison/gemini/issue-854-fix-ollama-sequential-fail
Fix #854: Ollama sequential tool calling failing
2025-07-13 00:03:55 +01:00
Gemini Bot 3ab2462d96 Gemini: [Issue #854] Fix Ollama sequential tool calling loop 2025-07-12 23:00:05 +00:00
Mervin Praison aecf65a130 Merge branch 'main' of https://github.com/MervinPraison/PraisonAI 2025-07-12 23:52:47 +01:00
Mervin Praison 0a453d30a2 chore: add .gemini and issue_details.json to gitignore 2025-07-12 23:52:46 +01:00
Mervin Praison 354a2c717b Merge pull request #856 from MervinPraison/gemini/issue-854-fix-ollama-sequential
Fix #854: Ollama sequential tool calling failing
2025-07-12 23:52:38 +01:00
Mervin Praison 62514fbef4 Delete issue_details.json 2025-07-12 23:51:43 +01:00
Mervin Praison f3c77a2e8b Delete .gemini/settings.json 2025-07-12 23:51:32 +01:00
Gemini Bot 50dde4fa02 Gemini: [Issue #854] Fix Ollama sequential tool calling 2025-07-12 22:45:09 +00:00
25 changed files with 3138 additions and 2993 deletions

No files matched your search

+1
View File
@@ -40,5 +40,6 @@ jobs:
uses: docker/build-push-action@v2
with:
context: .
file: ./docker/Dockerfile
push: true
tags: ghcr.io/mervinpraison/praisonai:latest
@@ -1,11 +1,18 @@
name: Gemini Automated Issue Triage
on:
issues:
types: [opened, reopened]
issue_comment:
types: [created]
jobs:
triage-issue:
if: >
github.event_name == 'issue_comment' &&
!github.event.issue.pull_request &&
contains(github.event.comment.body, '@gemini triage') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR')
timeout-minutes: 5
permissions:
issues: write
+1 -4
View File
@@ -2,8 +2,6 @@ name: Gemini Issue Review
on:
workflow_dispatch:
issues:
types: [opened, edited]
issue_comment:
types: [created]
@@ -16,10 +14,9 @@ jobs:
review-issue:
if: >
github.event_name == 'workflow_dispatch' ||
github.event_name == 'issues' ||
(github.event_name == 'issue_comment' &&
!github.event.issue.pull_request &&
contains(github.event.comment.body, '@gemini') &&
contains(github.event.comment.body, '@gemini review') &&
(github.event.comment.author_association == 'OWNER' ||
github.event.comment.author_association == 'MEMBER' ||
github.event.comment.author_association == 'COLLABORATOR'))
+57 -19
View File
@@ -35,11 +35,20 @@ jobs:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
- name: Determine checkout ref
id: checkout_ref
run: |
if [ "${{ github.event_name }}" = "issue_comment" ]; then
echo "ref=refs/pull/${{ github.event.issue.number }}/head" >> "$GITHUB_OUTPUT"
else
echo "ref=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
fi
- name: Checkout PR code
uses: actions/checkout@v4
with:
token: ${{ steps.generate_token.outputs.token }}
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ steps.checkout_ref.outputs.ref }}
fetch-depth: 0
- name: Get PR details
@@ -96,12 +105,19 @@ jobs:
"run_shell_command(gh pr view)",
"run_shell_command(gh pr diff)",
"run_shell_command(gh pr comment)",
"run_shell_command(gh pr list)",
"run_shell_command(cat)",
"run_shell_command(head)",
"run_shell_command(tail)",
"run_shell_command(grep)",
"run_shell_command(find)",
"run_shell_command(git)",
"run_shell_command(git checkout)",
"run_shell_command(git add)",
"run_shell_command(git commit)",
"run_shell_command(git push)",
"run_shell_command(git status)",
"run_shell_command(git branch)",
"run_shell_command(git fetch)",
"list_directory",
"read_file",
"read_many_files",
@@ -120,25 +136,47 @@ jobs:
"sandbox": false
}
prompt: |
You are an expert code reviewer. You have access to shell commands to gather PR information and perform the review.
You are an expert code reviewer and AI assistant for Pull Requests. You can review code AND make modifications to improve the PR.
IMPORTANT: Use the available shell commands to gather information. Do not ask for information to be provided.
**PR Details:**
- PR Number: #$PR_NUMBER
- Repository: $REPOSITORY
- Additional Instructions: $ADDITIONAL_INSTRUCTIONS
Start by running these commands to gather the required data:
1. Run: echo "$PR_DATA" to get PR details (JSON format)
2. Run: echo "$CHANGED_FILES" to get the list of changed files
3. Run: echo "$PR_NUMBER" to get the PR number
4. Run: echo "$ADDITIONAL_INSTRUCTIONS" to see any specific review instructions from the user
5. Run: gh pr diff $PR_NUMBER to see the full diff
6. For any specific files, use: cat filename, head -50 filename, or tail -50 filename
Additional Review Instructions:
If ADDITIONAL_INSTRUCTIONS contains text, prioritize those specific areas or focus points in your review.
Common instruction examples: "focus on security", "check performance", "review error handling", "check for breaking changes"
Once you have the information, provide a comprehensive code review by:
1. Writing your review to a file: write_file("review.md", "<your detailed review feedback here>")
2. Posting the review: gh pr comment $PR_NUMBER --body-file review.md --repo $REPOSITORY
**CRITICAL: When making code modifications, follow this EXACT workflow:**
**STEP 1: Analyze the PR**
- Use: `echo "$PR_DATA"` to get PR details (JSON format)
- Use: `echo "$CHANGED_FILES"` to get changed files list
- Use: `gh pr diff ${PR_NUMBER}` to see the full diff
- Use: `gh pr view ${PR_NUMBER} --json title,body,headRefName` to get PR info
- Analyze the code changes thoroughly
**STEP 2: If Code Improvements Are Needed**
**IMPORTANT:** DO NOT create a new branch! Work on the EXISTING PR branch.
- Use: `git fetch origin` to get latest changes
- Get PR branch name: `gh pr view ${PR_NUMBER} --json headRefName`
- Switch to PR branch: `git checkout BRANCH_NAME` (use actual branch name from PR)
- Use: `git pull origin BRANCH_NAME` to get latest PR changes
**STEP 3: Make Code Modifications**
- Use `read_file` to examine existing code
- Use `write_file` or `replace` to make improvements
- Use `search_file_content` to find related code
- Make focused, targeted improvements to the PR
**STEP 4: Test Changes (if applicable)**
- Run relevant tests if test files exist
- Use `run_shell_command` to execute test commands
**STEP 5: Commit and Push Changes to EXISTING PR Branch**
- Use: `git add .` (or specific files)
- Use: `git commit -m "Gemini: [PR #${PR_NUMBER}] Brief description of improvements"`
- Use: `git push origin BRANCH_NAME` (push to EXISTING PR branch)
**STEP 6: Review and Comment on PR**
Write comprehensive review and post comment with your analysis and any improvements made.
Use: `gh pr comment ${PR_NUMBER} -b "MESSAGE"`
Review Guidelines:
- Focus on code quality, security, performance, and maintainability
+3 -1
View File
@@ -75,4 +75,6 @@ agents/praisonaiagents/praisonaiagents.egg-info
# Local Netlify folder
.netlify
.qodo
CopilotKit*
CopilotKit*
.gemini
issue_details.json
+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.53" \
"praisonai>=2.2.54" \
"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.53" \
"praisonai>=2.2.54" \
"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.53" \
"praisonai>=2.2.54" \
"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.53" \
"praisonai>=2.2.54" \
"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.53`
- PraisonAI: `>=2.2.54`
- 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.53" "praisonaiagents==0.0.92"
RUN pip install "praisonai==2.2.54" "praisonaiagents==0.0.92"
```
## 🌐 Production Deployment
-70
View File
@@ -1,70 +0,0 @@
#!/usr/bin/env python3
"""Example demonstrating validation feedback in workflow retry logic"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src/praisonai-agents'))
from praisonaiagents import Agent, Task, PraisonAIAgents
# This example shows how validation feedback is now passed to retry tasks
print("""
==========================================
Validation Feedback Feature Demonstration
==========================================
This example shows how the workflow retry logic now includes validation feedback.
Before: When validation failed, the retry task would start fresh without knowing why it failed.
After: The retry task receives detailed feedback about what went wrong.
Example Scenario:
1. A data collection task produces output
2. A validation task checks if the output meets criteria
3. If validation fails, the data collection task is retried WITH feedback about why it failed
The retry task will see:
- Previous attempt failed validation with reason: invalid
- Validation feedback: [specific reason for failure]
- Rejected output: [the output that failed validation]
- Please try again with a different approach based on this feedback.
This enables context-dependent tasks to improve on retry instead of repeating the same mistakes.
==========================================
Implementation Details:
1. Added 'validation_feedback' field to Task class
2. When routing from decision task to retry task, capture validation details
3. Include feedback in task context when building retry task description
4. Clear feedback after use to prevent persistence
The solution is minimal and backward compatible - existing workflows continue to work unchanged.
""")
# Example workflow structure (without actual execution)
print("\nExample Workflow Structure:")
print("""
collect_task = Task(
name="collect_data",
description="Collect data from source",
is_start=True,
next_tasks=["validate_data"]
)
validate_task = Task(
name="validate_data",
task_type="decision",
description="Validate if data meets criteria",
condition={
"valid": [], # End workflow
"invalid": ["collect_data"] # Retry with feedback
}
)
# When validation returns "invalid":
# 1. The workflow captures the validation output
# 2. Stores it in collect_task.validation_feedback
# 3. On retry, collect_data task sees the feedback in its context
# 4. Agent can adjust approach based on specific failure reason
""")
-52
View File
@@ -1,52 +0,0 @@
#!/usr/bin/env python3
"""Multi-agent review of validation feedback implementation"""
print("""
========================================
Multi-Agent Code Review Summary
========================================
Agent 1: Code Quality Reviewer
------------------------------
✅ Minimal code changes - only essential modifications made
✅ Clean implementation with clear variable names
✅ Proper logging added for debugging
✅ No existing functionality modified
Agent 2: Backward Compatibility Checker
--------------------------------------
✅ New field 'validation_feedback' defaults to None
✅ Existing workflows continue to work unchanged
✅ Context building falls back to original behavior when no feedback
✅ No breaking changes to public APIs
Agent 3: Feature Implementation Validator
----------------------------------------
✅ Captures validation failure reason correctly
✅ Includes rejected output from failed attempt
✅ Provides clear feedback message to retry task
✅ Automatically clears feedback after use
Agent 4: Security and Performance Auditor
----------------------------------------
✅ No security vulnerabilities introduced
✅ No sensitive data exposure risks
✅ Minimal performance impact (simple dict operations)
✅ Memory efficient - feedback cleared after use
Agent 5: Test Coverage Analyst
------------------------------
✅ Unit tests cover all new functionality
✅ Tests verify backward compatibility
✅ Integration examples provided
✅ Edge cases handled (no feedback, empty feedback)
CONSENSUS: Implementation is APPROVED
====================================
The implementation successfully addresses the issue:
- Retry tasks now receive validation feedback
- Context-dependent tasks can improve on retry
- Fully backward compatible
- Minimal, clean code changes
""")
+179
View File
@@ -0,0 +1,179 @@
from praisonaiagents import Agent
from praisonaiagents.agent.handoff import handoff, handoff_filters
from pydantic import BaseModel
from typing import Dict, Any
import logging
# Mock data for testing
MOCK_ORDERS = {
"12345": {
"id": "12345",
"status": "shipped",
"items": ["Widget A", "Widget B"],
"total": 99.99,
"tracking_number": "TRK123456789",
"shipping_address": "123 Main St, City, ST 12345"
},
"67890": {
"id": "67890",
"status": "processing",
"items": ["Gadget X"],
"total": 149.99,
"tracking_number": None,
"shipping_address": "456 Oak Ave, Town, ST 67890"
}
}
MOCK_SHIPMENTS = {
"TRK123456789": {
"tracking_number": "TRK123456789",
"status": "in_transit",
"location": "Distribution Center - Chicago",
"estimated_delivery": "2024-01-15",
"updates": [
{"date": "2024-01-10", "status": "picked_up", "location": "Warehouse"},
{"date": "2024-01-12", "status": "in_transit", "location": "Chicago"}
]
}
}
MOCK_TICKETS = []
# Mock tool functions
def check_order_status(order_id: str) -> Dict[str, Any]:
"""Check the status of an order"""
if order_id in MOCK_ORDERS:
return {"success": True, "order": MOCK_ORDERS[order_id]}
return {"success": False, "error": f"Order {order_id} not found"}
def track_shipment(tracking_number: str) -> Dict[str, Any]:
"""Track a shipment by tracking number"""
if tracking_number in MOCK_SHIPMENTS:
return {"success": True, "shipment": MOCK_SHIPMENTS[tracking_number]}
return {"success": False, "error": f"Tracking number {tracking_number} not found"}
def process_refund(order_id: str, reason: str = "customer_request") -> Dict[str, Any]:
"""Process a refund for an order"""
if order_id in MOCK_ORDERS:
order = MOCK_ORDERS[order_id]
return {
"success": True,
"refund_id": f"REF{order_id}",
"amount": order["total"],
"status": "processing",
"estimated_days": 3-5
}
return {"success": False, "error": f"Order {order_id} not found"}
def check_refund_policy(item_type: str = "general") -> Dict[str, Any]:
"""Check refund policy for different item types"""
policies = {
"general": {"days": 30, "condition": "unopened", "shipping": "customer_pays"},
"electronics": {"days": 15, "condition": "original_packaging", "shipping": "free"},
"clothing": {"days": 60, "condition": "tags_attached", "shipping": "customer_pays"}
}
return {"success": True, "policy": policies.get(item_type, policies["general"])}
def diagnose_issue(description: str) -> Dict[str, Any]:
"""Diagnose a technical issue"""
common_solutions = {
"login": "Clear cookies and cache, try password reset",
"payment": "Check card details, try different payment method",
"loading": "Check internet connection, try different browser",
"mobile": "Update app, restart device, check storage space"
}
# Simple keyword matching for demo
for issue_type, solution in common_solutions.items():
if issue_type in description.lower():
return {"success": True, "diagnosis": solution, "severity": "low"}
return {"success": True, "diagnosis": "Please provide more details", "severity": "medium"}
def create_ticket(issue_description: str, priority: str = "medium") -> Dict[str, Any]:
"""Create a support ticket"""
ticket_id = f"TICK{len(MOCK_TICKETS) + 1001}"
ticket = {
"id": ticket_id,
"description": issue_description,
"priority": priority,
"status": "open",
"created_at": "2024-01-13T10:00:00Z"
}
MOCK_TICKETS.append(ticket)
return {"success": True, "ticket": ticket}
def log_escalation(source_agent: str, escalation_data: Dict[str, Any]) -> None:
"""Log escalation for audit purposes"""
logging.info(f"Escalation from {source_agent}: {escalation_data}")
# Escalation with structured input
class EscalationRequest(BaseModel):
issue_type: str
severity: str
description: str
# Specialist agents
order_agent = Agent(
name="Order Management",
instructions="You help with order status and tracking.",
tools=[check_order_status, track_shipment]
)
refund_agent = Agent(
name="Refund Specialist",
instructions="You process refunds and handle return requests.",
tools=[process_refund, check_refund_policy]
)
technical_agent = Agent(
name="Technical Support",
instructions="You solve technical issues and provide guidance.",
tools=[diagnose_issue, create_ticket]
)
escalation_agent = Agent(
name="Senior Manager",
instructions="You handle escalated issues requiring management attention."
)
# Main triage agent
triage_agent = Agent(
name="Customer Service",
instructions="""You are the first point of contact. Route customers to the right specialist:
- Order issues → Order Management
- Refunds → Refund Specialist
- Technical problems → Technical Support
- Complex issues → Escalate to management
""",
handoffs=[
order_agent,
refund_agent,
technical_agent,
handoff(
escalation_agent,
tool_name_override="escalate_to_management",
input_type=EscalationRequest,
on_handoff=lambda src, data: log_escalation(src.name, data)
)
]
)
# Start the service
if __name__ == "__main__":
print("Customer Service Handoff System Demo")
print("=" * 40)
# Test different scenarios
test_queries = [
"I need a refund for order #12345",
"What's the status of order #67890?",
"I'm having trouble logging into my account",
"This is urgent - I need to speak to a manager about a serious billing issue"
]
for query in test_queries:
print(f"\nCustomer: {query}")
response = triage_agent.chat(query)
print(f"Response: {response}")
print("-" * 40)
@@ -480,24 +480,22 @@ Context:
)
if self.process == "workflow":
# Collect all tasks that should run in parallel
parallel_tasks = []
tasks_to_run = []
async for task_id in process.aworkflow():
if self.tasks[task_id].async_execution and self.tasks[task_id].is_start:
parallel_tasks.append(task_id)
elif parallel_tasks:
# Execute collected parallel tasks
await asyncio.gather(*[self.arun_task(t) for t in parallel_tasks])
parallel_tasks = []
# Run the current non-parallel task
if self.tasks[task_id].async_execution:
await self.arun_task(task_id)
else:
self.run_task(task_id)
# Execute any remaining parallel tasks
if parallel_tasks:
await asyncio.gather(*[self.arun_task(t) for t in parallel_tasks])
if self.tasks[task_id].async_execution:
tasks_to_run.append(self.arun_task(task_id))
else:
# If we encounter a sync task, we must wait for the previous async tasks to finish.
if tasks_to_run:
await asyncio.gather(*tasks_to_run)
tasks_to_run = []
# Run sync task in an executor to avoid blocking the event loop
loop = asyncio.get_event_loop()
await loop.run_in_executor(None, self.run_task, task_id)
if tasks_to_run:
await asyncio.gather(*tasks_to_run)
elif self.process == "sequential":
async for task_id in process.asequential():
@@ -136,7 +136,7 @@ class AutoAgents(PraisonAIAgents):
completion_checker=completion_checker,
max_retries=max_retries,
process=process,
manager_llm=manager_llm
manager_llm=manager_llm or self.llm
)
def _display_agents_and_tasks(self, agents: List[Agent], tasks: List[Task]):
+3 -220
View File
@@ -858,102 +858,6 @@ class LLM:
iteration_count += 1
continue
# Special handling for Ollama models that don't automatically process tool results
ollama_handled = False
ollama_params = self._handle_ollama_model(response_text, tool_results, messages, original_prompt)
if ollama_params:
# Get response based on streaming mode
if stream:
# Streaming approach
if verbose:
with Live(display_generating("", start_time), console=console, refresh_per_second=4) as live:
response_text = ""
for chunk in litellm.completion(
**self._build_completion_params(
messages=ollama_params["follow_up_messages"],
temperature=temperature,
stream=True
)
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
response_text += content
live.update(display_generating(response_text, start_time))
else:
response_text = ""
for chunk in litellm.completion(
**self._build_completion_params(
messages=ollama_params["follow_up_messages"],
temperature=temperature,
stream=True
)
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
response_text += chunk.choices[0].delta.content
else:
# Non-streaming approach
resp = litellm.completion(
**self._build_completion_params(
messages=ollama_params["follow_up_messages"],
temperature=temperature,
stream=False
)
)
response_text = resp.get("choices", [{}])[0].get("message", {}).get("content", "") or ""
# Set flag to indicate Ollama was handled
ollama_handled = True
final_response_text = response_text.strip() if response_text else ""
logging.debug(f"[OLLAMA_DEBUG] Ollama follow-up response: {final_response_text[:200]}...")
# Display the response if we got one
if final_response_text and verbose:
display_interaction(
ollama_params["original_prompt"],
final_response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
# Update messages and continue the loop instead of returning
if final_response_text:
# Update messages with the response to maintain conversation context
messages.append({
"role": "assistant",
"content": final_response_text
})
# Continue the loop to check if more tools are needed
iteration_count += 1
continue
else:
logging.warning("[OLLAMA_DEBUG] Ollama follow-up returned empty response")
# Handle reasoning_steps after tool execution if not already handled by Ollama
if reasoning_steps and not ollama_handled:
# Make a non-streaming call to capture reasoning content
reasoning_resp = litellm.completion(
**self._build_completion_params(
messages=messages,
temperature=temperature,
stream=False, # force non-streaming
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
)
reasoning_content = reasoning_resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
response_text = reasoning_resp["choices"][0]["message"]["content"]
# Store reasoning content for later use
if reasoning_content:
stored_reasoning_content = reasoning_content
# Update messages with the response
messages.append({
"role": "assistant",
"content": response_text
})
# After tool execution, continue the loop to check if more tools are needed
# instead of immediately trying to get a final response
iteration_count += 1
@@ -1430,68 +1334,8 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
# Get response after tool calls
response_text = ""
# Special handling for Ollama models that don't automatically process tool results
ollama_handled = False
ollama_params = self._handle_ollama_model(response_text, tool_results, messages, original_prompt)
if ollama_params:
# Get response with streaming
if verbose:
response_text = ""
async for chunk in await litellm.acompletion(
**self._build_completion_params(
messages=ollama_params["follow_up_messages"],
temperature=temperature,
stream=stream
)
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
response_text += content
print("\033[K", end="\r")
print(f"Processing results... {time.time() - start_time:.1f}s", end="\r")
else:
response_text = ""
async for chunk in await litellm.acompletion(
**self._build_completion_params(
messages=ollama_params["follow_up_messages"],
temperature=temperature,
stream=stream
)
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
response_text += chunk.choices[0].delta.content
# Set flag to indicate Ollama was handled
ollama_handled = True
final_response_text = response_text.strip()
logging.debug(f"[OLLAMA_DEBUG] Ollama follow-up response: {final_response_text[:200]}...")
# Display the response if we got one
if final_response_text and verbose:
display_interaction(
ollama_params["original_prompt"],
final_response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console
)
# Store the response for potential final return
if final_response_text:
# Update messages with the response to maintain conversation context
messages.append({
"role": "assistant",
"content": final_response_text
})
# Continue the loop to check if more tools are needed
iteration_count += 1
continue
else:
logging.warning("[OLLAMA_DEBUG] Ollama follow-up returned empty response")
# If no special handling was needed or if it's not an Ollama model
if reasoning_steps and not ollama_handled:
# If no special handling was needed
if reasoning_steps:
# Non-streaming call to capture reasoning
resp = await litellm.acompletion(
**self._build_completion_params(
@@ -1521,7 +1365,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
generation_time=time.time() - start_time,
console=console
)
elif not ollama_handled:
else:
# Get response after tool calls with streaming if not already handled
if verbose:
async for chunk in await litellm.acompletion(
@@ -1790,67 +1634,6 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
litellm.callbacks = events
def _handle_ollama_model(self, response_text: str, tool_results: List[Any], messages: List[Dict], original_prompt: Union[str, List[Dict]]) -> Optional[Dict[str, Any]]:
"""
Handle special Ollama model requirements when processing tool results.
Args:
response_text: The initial response text from the model
tool_results: List of tool execution results
messages: The conversation messages list
original_prompt: The original user prompt
Returns:
Dict with follow-up parameters if Ollama needs special handling, None otherwise
"""
if not self._is_ollama_provider() or not tool_results:
return None
# Check if the response is just a JSON tool call
try:
json_response = json.loads(response_text.strip() if response_text else "{}")
if not (('name' in json_response or 'function' in json_response) and
not any(word in response_text.lower() for word in ['summary', 'option', 'result', 'found'])):
return None
logging.debug("Detected Ollama returning only tool call JSON, preparing follow-up call to process results")
# Extract the original user query from messages
original_query = ""
for msg in reversed(messages): # Look from the end to find the most recent user message
if msg.get("role") == "user":
content = msg.get("content", "")
# Handle list content (multimodal)
if isinstance(content, list):
for item in content:
if isinstance(item, dict) and item.get("type") == "text":
original_query = item.get("text", "")
break
else:
original_query = content
if original_query:
break
# Create a shorter follow-up prompt with all tool results
# If there's only one result, use it directly; otherwise combine them
if len(tool_results) == 1:
results_text = json.dumps(tool_results[0], indent=2)
else:
results_text = json.dumps(tool_results, indent=2)
follow_up_prompt = f"Results:\n{results_text}\nProvide Answer to this Original Question based on the above results: '{original_query}'"
logging.debug(f"[OLLAMA_DEBUG] Original query extracted: {original_query}")
logging.debug(f"[OLLAMA_DEBUG] Follow-up prompt: {follow_up_prompt[:200]}...")
# Return parameters for follow-up call
return {
"follow_up_messages": [{"role": "user", "content": follow_up_prompt}],
"original_prompt": original_prompt
}
except (json.JSONDecodeError, KeyError):
# Not a JSON response or not a tool call format
return None
def _build_completion_params(self, **override_params) -> Dict[str, Any]:
"""Build parameters for litellm completion calls with all necessary config"""
@@ -469,16 +469,18 @@ Subtask: {st.name}
logging.debug(f"Task type: {task_to_check.task_type}")
logging.debug(f"Task status before reset check: {task_to_check.status}")
logging.debug(f"Task rerun: {getattr(task_to_check, 'rerun', True)}") # default to True if not set
logging.debug(f"Task async_execution: {task_to_check.async_execution}")
if (getattr(task_to_check, 'rerun', True) and # Corrected condition - reset only if rerun is True (or default True)
task_to_check.task_type != "loop" and # Removed "decision" from exclusion
not any(t.task_type == "loop" and subtask_name.startswith(t.name + "_")
for t in self.tasks.values())):
logging.debug(f"=== Resetting non-loop, non-decision task {subtask_name} to 'not started' ===")
for t in self.tasks.values()) and
not task_to_check.async_execution): # Don't reset async parallel tasks
logging.debug(f"=== Resetting non-loop, non-decision, non-parallel task {subtask_name} to 'not started' ===")
self.tasks[task_id].status = "not started"
logging.debug(f"Task status after reset: {self.tasks[task_id].status}")
else:
logging.debug(f"=== Skipping reset for loop/decision/subtask or rerun=False: {subtask_name} ===")
logging.debug(f"=== Skipping reset for loop/decision/subtask/parallel or rerun=False: {subtask_name} ===")
logging.debug(f"Keeping status as: {self.tasks[task_id].status}")
# Handle loop progression
@@ -1099,16 +1101,18 @@ Subtask: {st.name}
logging.debug(f"Task type: {task_to_check.task_type}")
logging.debug(f"Task status before reset check: {task_to_check.status}")
logging.debug(f"Task rerun: {getattr(task_to_check, 'rerun', True)}") # default to True if not set
logging.debug(f"Task async_execution: {task_to_check.async_execution}")
if (getattr(task_to_check, 'rerun', True) and # Corrected condition - reset only if rerun is True (or default True)
task_to_check.task_type != "loop" and # Removed "decision" from exclusion
not any(t.task_type == "loop" and subtask_name.startswith(t.name + "_")
for t in self.tasks.values())):
logging.debug(f"=== Resetting non-loop, non-decision task {subtask_name} to 'not started' ===")
for t in self.tasks.values()) and
not task_to_check.async_execution): # Don't reset async parallel tasks
logging.debug(f"=== Resetting non-loop, non-decision, non-parallel task {subtask_name} to 'not started' ===")
self.tasks[task_id].status = "not started"
logging.debug(f"Task status after reset: {self.tasks[task_id].status}")
else:
logging.debug(f"=== Skipping reset for loop/decision/subtask or rerun=False: {subtask_name} ===")
logging.debug(f"=== Skipping reset for loop/decision/subtask/parallel or rerun=False: {subtask_name} ===")
logging.debug(f"Keeping status as: {self.tasks[task_id].status}")
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.127"
version = "0.0.128"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
requires-python = ">=3.10"
authors = [
@@ -16,7 +16,7 @@ def test_example_one():
validator_agent = Agent(
name="validator_agent",
instructions="You are a helpful assistant",
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
llm="gemini/gemini-2.5-flash",
self_reflect=False,
verbose=True
)
@@ -24,7 +24,7 @@ def test_example_one():
quote_agent = Agent(
name="quote_agent",
instructions="You are a helpful assistant who is a pro at quoting philosophers",
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
llm="gemini/gemini-2.5-flash",
self_reflect=False,
verbose=False
)
@@ -84,7 +84,7 @@ def test_example_two():
agent = Agent(
name="agent",
instructions="You are a helpful assistant",
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
llm="gemini/gemini-2.5-flash",
self_reflect=False,
verbose=True
)
@@ -94,7 +94,7 @@ def test_example_two():
role="Web Search Specialist",
goal="Search the web for relevant information",
instructions="You are a helpful assistant. You can use the tools provided to you to help the user.",
llm="gemini/gemini-2.5-flash-lite-preview-06-17",
llm="gemini/gemini-2.5-flash",
self_reflect=False,
verbose=False,
tools=[mock_web_search]
+1 -1
View File
@@ -2375,7 +2375,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.127"
version = "0.0.128"
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.53.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.53.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.54.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.2.54.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.53 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.2.54 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.53"
version = "2.2.54"
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.127",
"praisonaiagents>=0.0.128",
"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.53"
version = "2.2.54"
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.127"
praisonaiagents = ">=0.0.128"
python-dotenv = ">=0.19.0"
instructor = ">=1.3.3"
PyYAML = ">=6.0"
@@ -0,0 +1,256 @@
"""Unit tests for validation feedback in workflow retry logic"""
import pytest
from unittest.mock import Mock, patch
from praisonaiagents import Agent, Task, TaskOutput
from praisonaiagents.process import Process
class TestValidationFeedback:
"""Test validation feedback functionality"""
def test_task_has_validation_feedback_field(self):
"""Test that Task class has validation_feedback field"""
task = Task(
name="test_task",
description="Test task",
expected_output="Test output"
)
assert hasattr(task, 'validation_feedback')
assert task.validation_feedback is None
def test_validation_feedback_captured_on_invalid_decision(self):
"""Test that validation feedback is captured when decision is invalid"""
# Create mock agents
agent1 = Mock(spec=Agent)
agent2 = Mock(spec=Agent)
# Create tasks
collect_task = Task(
name="collect_data",
description="Collect data",
expected_output="Data",
agent=agent1,
is_start=True,
next_tasks=["validate_data"]
)
validate_task = Task(
name="validate_data",
description="Validate data",
expected_output="Validation result",
agent=agent2,
task_type="decision",
condition={
"valid": [],
"invalid": ["collect_data"]
}
)
# Set up task relationships
validate_task.previous_tasks = ["collect_data"]
# Create process
process = Process(
agents={"agent1": agent1, "agent2": agent2},
tasks={"collect_data": collect_task, "validate_data": validate_task},
verbose=0
)
# Simulate task execution results
collect_task.result = TaskOutput(
raw="Collected only 3 items",
description="Collected data items",
agent="agent1"
)
collect_task.status = "completed"
validate_task.result = TaskOutput(
raw="Not enough results, need at least 10",
description="Validation result",
agent="agent2"
)
validate_task.status = "completed"
# Simulate workflow routing with invalid decision
# This would normally happen in the workflow execution
decision_str = "invalid"
current_task = validate_task
target_tasks = current_task.condition.get(decision_str, [])
if target_tasks:
task_value = target_tasks[0]
next_task = collect_task # This is the retry task
# The implementation should add validation feedback
if decision_str in Process.VALIDATION_FAILURE_DECISIONS:
if current_task and current_task.result:
validated_task = collect_task # The task that was validated
feedback = {
'validation_response': decision_str,
'validation_details': current_task.result.raw,
'rejected_output': validated_task.result.raw if validated_task and validated_task.result else None,
'validator_task': current_task.name
}
next_task.validation_feedback = feedback
# Verify feedback was captured
assert collect_task.validation_feedback is not None
assert collect_task.validation_feedback['validation_response'] == 'invalid'
assert collect_task.validation_feedback['validation_details'] == "Not enough results, need at least 10"
assert collect_task.validation_feedback['rejected_output'] == "Collected only 3 items"
assert collect_task.validation_feedback['validator_task'] == "validate_data"
def test_validation_feedback_included_in_context(self):
"""Test that validation feedback is included in task context"""
# Create a task with validation feedback
task = Task(
name="retry_task",
description="Original description",
expected_output="Expected output"
)
task.validation_feedback = {
'validation_response': 'invalid',
'validation_details': 'Not enough results',
'rejected_output': 'Previous output',
'validator_task': 'validator'
}
# Create process to test context building
process = Process(
agents={},
tasks={"retry_task": task},
verbose=0
)
# Build context
context = process._build_task_context(task)
# Verify feedback is in context
assert "Previous attempt failed validation with reason: invalid" in context
assert "Validation feedback: Not enough results" in context
assert "Rejected output: Previous output" in context
assert "Please try again with a different approach based on this feedback." in context
# Verify feedback was cleared after use
assert task.validation_feedback is None
def test_validation_feedback_backward_compatibility(self):
"""Test that tasks without validation feedback work normally"""
task = Task(
name="normal_task",
description="Normal task",
expected_output="Normal output"
)
process = Process(
agents={},
tasks={"normal_task": task},
verbose=0
)
# Build context without validation feedback
context = process._build_task_context(task)
# Should return empty string for task with no context
assert context == ""
# Task should still work normally
assert task.validation_feedback is None
def test_multiple_retry_decisions_supported(self):
"""Test that various failure decision strings trigger feedback capture"""
# Import the constant from Process class
from praisonaiagents.process import Process
failure_decisions = Process.VALIDATION_FAILURE_DECISIONS
for decision in failure_decisions:
task = Task(
name="test_task",
description="Test",
expected_output="Test"
)
# Simulate the decision routing logic
if decision in Process.VALIDATION_FAILURE_DECISIONS:
task.validation_feedback = {
'validation_response': decision,
'validation_details': f"Failed with {decision}",
'rejected_output': "Previous output",
'validator_task': "validator"
}
assert task.validation_feedback is not None
assert task.validation_feedback['validation_response'] == decision
def test_validation_feedback_with_context_tasks(self):
"""Test that validation feedback works when validated task is in context"""
# Create mock agents
agent1 = Mock(spec=Agent)
agent2 = Mock(spec=Agent)
# Create tasks
data_task = Task(
name="data_task",
description="Generate data",
expected_output="Data",
agent=agent1
)
validate_task = Task(
name="validate_data",
description="Validate data",
expected_output="Validation result",
agent=agent2,
task_type="decision",
context=[data_task], # Data task is in context, not previous_tasks
condition={
"valid": [],
"invalid": ["data_task"]
}
)
# Create process
process = Process(
agents={"agent1": agent1, "agent2": agent2},
tasks={"data_task": data_task, "validate_data": validate_task},
verbose=0
)
# Simulate task execution results
data_task.result = TaskOutput(
raw="Generated data with errors",
description="Generated data",
agent="agent1"
)
data_task.status = "completed"
validate_task.result = TaskOutput(
raw="Data has errors, please fix",
description="Data validation result",
agent="agent2"
)
validate_task.status = "completed"
# Test the improved validation feedback logic
decision_str = "invalid"
current_task = validate_task
# This simulates the improved logic that checks context when no previous_tasks
validated_task = None
if current_task.previous_tasks:
prev_task_name = current_task.previous_tasks[-1]
validated_task = data_task if data_task.name == prev_task_name else None
elif current_task.context:
# Check context for the validated task
for ctx_task in reversed(current_task.context):
if ctx_task.result and ctx_task.name != current_task.name:
validated_task = ctx_task
break
# Verify the context-based task was found
assert validated_task is not None
assert validated_task.name == "data_task"
assert validated_task.result.raw == "Generated data with errors"
+2584 -2582
View File
File diff suppressed because it is too large. Load diff