Compare commits

...
15 Commits
Author SHA1 Message Date
Mervin Praison 9f62e9201a Merge pull request #339 from MervinPraison/develop
2.0.60
2025-01-29 05:59:25 +00:00
MervinPraison 1b69360d9f Bump PraisonAI version to 2.0.60 and update dependencies
- Updated version to 2.0.60 in Homebrew formula, pyproject.toml, and Docker files
- Upgraded praisonaiagents dependency from 0.0.56 to 0.0.60
- Added new documentation for generating reasoning data
- Included example script for chain-of-thought data generation
- Enhanced workflow task management and logging in process modules

The changes improve the framework's version management, documentation, and provide new tools for synthetic reasoning data generation.
2025-01-29 05:58:58 +00:00
MervinPraison 9bdc6545ad Enhance Workflow Task Management with Workflow Finished Flag and Improved Routing
- Added `workflow_finished` flag to `Process` class to enable early workflow termination
- Implemented more robust task routing logic in `process.py` for decision and loop tasks
- Updated task reset mechanism to respect `rerun` parameter
- Improved context clearing and task status management
- Enhanced logging for workflow progression and task transitions

The changes provide more flexible and controlled workflow execution, with better handling of task conditions and early termination scenarios.
2025-01-28 16:40:22 +00:00
MervinPraison 06cd909367 Add max_iter parameter and enhanced workflow task management with fallback mechanism
- Updated `orchestrator-workers.py` to include `max_iter=10` parameter in workflow initialization
- Enhanced `process.py` with a new `_find_next_not_started_task()` method to provide fallback task selection
- Implemented comprehensive logging for workflow cycles, task status, and iteration tracking
- Added retry counter and fallback mechanism to handle scenarios with incomplete or stalled tasks
- Improved workflow termination logic with detailed final summary logging

The changes improve workflow reliability, prevent potential infinite loops, and provide more robust task management in complex multi-agent scenarios.
2025-01-28 11:26:32 +00:00
MervinPraison 67aeece7fc Bump praisonaiagents package version to 0.0.58
- Updated version in `pyproject.toml` and `uv.lock`
- Incremented package version for continued development and improvements
2025-01-27 19:19:40 +00:00
MervinPraison ef67d0753c Enhance Workflow Task Management with Detailed Logging and Loop Task Handling
- Updated `.cursorrules` with comprehensive technical overview of workflow execution challenges
- Refactored `cot-agents.py` to:
  - Add Pydantic model for structured output
  - Improve task description and condition handling
  - Add output validation for chain of thought generation
- Enhanced `process.py` with:
  - Extensive debug logging for task execution
  - Improved loop task and subtask management
  - More detailed logging of task status and progression

The changes improve workflow reliability, provide deeper insights into task execution, and prevent infinite loops in complex task workflows.
2025-01-27 19:18:49 +00:00
MervinPraison f374f9f791 Enhance COT Agent Workflow with Improved Task Management and Logging
- Refactored `cot-agents.py` to include:
  - New `write_csv` and `count_questions` functions
  - Added `TotalQuestionsEvaluator` agent for tracking question count
  - Improved task workflow with decision and loop task types
- Updated `process.py` to support:
  - Enhanced logging for task execution
  - Better handling of loop and decision tasks
  - Improved subtask creation and management
- Modified `csv_tools.py` to support more flexible CSV writing with string input
- Implemented more robust task progression and status tracking

The changes improve the flexibility, logging, and task management of the Chain of Thought agent workflow.
2025-01-27 13:59:59 +00:00
MervinPraison 492fb29af9 Enhance Chain of Thought (COT) agent workflow with batch processing and Hugging Face integration
- Added `cot-agents-batch.py` to support batch processing of Q&A pairs
- Updated `cot-agents.py` with a more comprehensive workflow including:
  - Question generation
  - Chain of Thought solution generation
  - Hugging Face dataset upload
- Refined `generatecot.py` with improvements to solution generation:
  - Reduced max attempts
  - Adjusted scoring thresholds
  - Added direct QA pair tracking

The changes improve the flexibility and automation of Chain of Thought solution generation and dataset management.
2025-01-27 02:52:14 +00:00
MervinPraison cd56ebd5cf Add Chain of Thought (COT) generation tools and update project structure
- Introduced comprehensive `generatecot.py` in `praisonaiagents/tools/train/data/` with advanced Chain of Thought solution generation capabilities
- Added multiple COT-related tools and methods:
  - Solution generation and improvement
  - Answer verification
  - Error detection and correction
  - Export to various formats (JSON, CSV, Alpaca)
  - HuggingFace dataset upload support
- Updated tool mappings in `__init__.py` to include COT-related tools
- Moved existing `generatecot.py` from `train/data/` to `tools/train/data/`
- Bumped project version to 0.0.57 in relevant files

The changes enhance the framework's capabilities for generating and managing step-by-step problem-solving solutions with AI-powered reasoning.
2025-01-26 07:22:32 +00:00
MervinPraison 08e99f6ef7 Enhance GenerateCOT with dictionary-based solution generation and Alpaca format export
- Added new methods to generate solutions in dictionary format:
  - `get_solution_dict()` for step-by-step problem solving
  - `improve_solution_dict()` to refine existing solutions
  - `start_dict()` to generate and validate solutions
- Implemented `export_alpaca_format()` to convert solutions to Alpaca training format
- Updated type hints and added example usage for new methods
- Extended the script with additional QA data processing and export demonstration

The changes improve the flexibility and usability of the GenerateCOT class for solution generation and data preparation.
2025-01-25 08:22:08 +00:00
MervinPraison 670d653ae9 Add GenerateCOT class for step-by-step solution generation and verification
- Created a new Python script `generatecot.py` in the train/data directory
- Implemented a comprehensive solution generation class with features like:
  - Step-by-step problem solving
  - Answer verification
  - Solution improvement
  - Error detection and correction
- Added support for loading and saving solutions with JSON
- Included an example usage demonstrating the class functionality

The GenerateCOT (Chain of Thought) class provides a robust mechanism for generating and refining solutions to various problems using AI-powered reasoning.
2025-01-25 07:47:20 +00:00
Mervin Praison faa014b8b0 Merge pull request #333 from MervinPraison/develop
2.0.59
2025-01-24 17:17:53 +00:00
MervinPraison 6d454d0c6e Update PraisonAI to version 2.0.59 and enhance dependencies
- Bumped PraisonAI version from 2.0.58 to 2.0.59 in `Dockerfile`, `pyproject.toml`, and `uv.lock` to reflect the latest updates.
- Updated the `praisonaiagents` dependency version from `0.0.53` to `0.0.56` across relevant files for improved functionality.
- Modified agent task descriptions in `repetitive-agents.py` to clarify expected outputs.
- Enhanced the `process.py` file to improve task handling and expected output management.

These changes aim to refine the PraisonAI framework's capabilities and ensure compatibility with the latest dependencies.
2025-01-24 17:17:32 +00:00
MervinPraison 0ec3fe3510 Add new agent scripts and update version to 0.0.55
- Introduced three new Python scripts: `deepseek-reasoning-output-tokens.py`, `llm-deepseek-reasoning-steps.py`, and `llm-langchain-toolcall.py` to enhance reasoning capabilities and multi-source information retrieval.
- Updated `pyproject.toml` and `uv.lock` to reflect the new version 0.0.55.
- Enhanced `wikipedia-agent.py` to utilize the new `gpt-4o-mini` model for improved performance.
- Improved `agent.py` to support tool calls in chat completions, refining the overall functionality of the agent framework.

These changes aim to expand the capabilities of the PraisonAI framework by integrating new reasoning features and improving user experience with updated versioning.
2025-01-24 13:04:34 +00:00
MervinPraison 707888cdf9 Remove deprecated agent scripts and update version to 0.0.54
- Deleted `any-llm-agents.py` and `any-llm-agents-advanced.py` scripts to streamline the agent framework.
- Updated `pyproject.toml` and `uv.lock` to reflect the new version 0.0.54.
- Enhanced `agent.py` and `llm.py` to improve handling of reasoning steps by ensuring `reasoning_steps` is properly managed in API calls.

These changes aim to simplify the agent framework and improve the overall functionality of the PraisonAI system.
2025-01-24 10:35:42 +00:00
31 changed files with 2696 additions and 365 deletions

No files matched your search

+143 -1
View File
@@ -5,4 +5,146 @@
5. Make it minimal change as possible
6. Firstly try not to make any modification to the existing code as possible
7. Only modify the existing code if its highly required, without that if it cant be done, then add new code section.
8. If you are adding new code, make sure to add it in a way that it can be easily integrated with the existing codebase.
8. If you are adding new code, make sure to add it in a way that it can be easily integrated with the existing codebase.
Below is a **detailed technical overview** of the issues that have been coming up in the workflow execution (specifically around loops, resetting tasks, and continuing on to subsequent tasks). It includes:
1. **How the workflow currently flows**
2. **What logic marks a task as “completed”**
3. **What issues arose in `loop` tasks**
4. **Why the workflow can end up “stuck” or “looping indefinitely”**
---
## 1. Overall Workflow Flow
### a. Building relationships among tasks
- At startup, the code iterates through all `tasks`.
- For each `task`:
- It looks at `task.next_tasks`, and for each `next_task_name`:
- Finds the corresponding `Task` object
- Appends the current task’s name to the found `next_task`'s `previous_tasks` list.
- This means if Task A has `next_tasks=["B"]`, then Task B’s `previous_tasks` will include `"A"`.
### b. Finding and starting with a “start task”
- The workflow code tries to locate a task with `is_start=True`.
- If no such task is found, it uses the first item in the tasks dictionary instead.
- That “start task” is what the workflow tries to run first.
### c. Execution loop in the method (e.g., `workflow()` or `aworkflow()`)
- There is a `while current_task:` loop that processes tasks in sequence, or conditionally, based on `task.condition`.
- Each time a task runs (if non-loop), it yields the `task_id` or triggers an agent to run. Once the agent finishes (with or without a final result), the workflow picks up again to see what to do next:
- If the `task` is a `loop` type, the code tries to create or manage sub-tasks for each row/line in an “input_file.”
- If the `task` is a normal (“decision” or “task” or “some-other-type”), it just executes once, sets `status="completed"`, and the code moves on.
### d. Condition-based branching
- If a task has a result that includes a “decision” (like `{"decision":"more"}` or `"done"`), the code checks `task.condition`. For example:
```python
condition = {
"more": "generate_task",
"done": "evaluate_total_questions"
}
```
- If the result’s decision is `"done"`, it jumps to the task named `"evaluate_total_questions"`.
- If the result’s decision is `"more"`, it jumps right back to `"generate_task"`.
- If no condition matches, it can fallback to the first item in `task.next_tasks`.
### e. Marking tasks as “completed”
- After a task’s execution (like a typical “non-loop” task), the code sets `task.status = "completed"`.
- Then the code has a snippet that says:
```python
if self.tasks[task_id].status == "completed":
# Possibly reset to "not started" so we can re-run if needed
```
- By default, the system tries to “reset” tasks to `"not started"`, **unless** it is a loop task or a subtask of a loop.
---
## 2. How a “completed” task is marked
Generally, tasks are marked `status="completed"` in two primary ways:
1. **Non-Loop Execution**
A normal task (like a “decision” or “task”) is executed once the code calls the agent, the agent returns a final result, and the system sets `task.status = "completed"`.
2. **Loop Execution**
A loop-type task is *programmatically* set to `status="completed"` when all of its sub-tasks have finished. That is:
- The code checks: “Have all child tasks of this loop finished?”
- If `True`, the loop task is set to `completed`.
---
## 3. Issues Specifically in `loop` Tasks
### a. Re-Entering the Loop
- Before, the same snippet that “resets completed tasks to ‘not started’ so they can re-run if needed” **also** tried to reset loop tasks or their subtasks.
- If a loop task got reset to `"not started"`, the code would eventually pick it back up again, leading to repeated creation of sub-tasks (or repeated attempts to re-run them).
- This caused an **infinite loop** or repeating the same steps in the workflow, never truly exiting the loop stage.
### b. Subtasks Not Marked or Overwritten
- Another tricky scenario: If sub-tasks themselves got reset, the parent loop would see them as “not started” again, and might wait for them to “complete,” or might re-run them. That can lead to indefinite re-running of sub-tasks.
### c. Not Proceeding to Next (e.g., “upload_to_huggingface”)
- If the loop kept “restarting,” the workflow never ended up hitting the next tasks. For example, if your workflow is:
1. `generate_task`
2. `evaluate_total_questions`
3. `generate_cot` (loop)
4. `upload_to_huggingface`
- The system might get stuck in step #3 indefinitely (the sub-tasks keep getting reset, so it never actually transitions to step #4).
### d. Condition Logic vs. Next Tasks
- Another subtlety: If the loop tasks had a `condition` that pointed them back to a prior step, it might cause unintentional re-entry. Typically, you only want loop tasks to proceed once, **unless** you explicitly want to re-visit. But if it’s a data ingestion process, you usually want to do it once, then move on to the next step.
---
## 4. Why the System Can End Up Stuck or “Looping Indefinitely”
1. **Reset Mechanism**
- The code tries to “reset tasks to ‘not started’ once they complete,” so they can be re-run in some dynamic multi-run scenario.
- But that same logic can cause loop tasks to revert back to “not started” the moment they end. The system sees “Oh, a task is ‘not started’? Let’s run it!” and you’re in a cycle.
2. **No Condition for Exit**
- If the loop has a condition that leads back to a prior step (like `"more" -> generate_cot`), it can keep re-running.
3. **Subtasks Not Marked**
- If the subtask or the loop tries to “reactivate” each other, it never exits.
---
## Summary of the “Core Problem”
1. We want to keep the resetting mechanism for **non-loop tasks** – because in some advanced workflows, we like re-running them from a different path or after some condition.
2. But we want **loop tasks** to remain `"completed"` once all sub-tasks are done, so the code can seamlessly proceed to the next major step.
3. Before the fix, loop tasks or their sub-tasks got reset. This triggered the system to re-enter the loop, re-run the sub-tasks, etc., causing an infinite loop and preventing the workflow from reaching tasks like “upload_to_huggingface.”
---
## Technical Highlights to Pass On
- **In the reset snippet**:
```python
if self.tasks[task_id].status == "completed":
# never reset if loop or subtask-of-loop
# else reset to "not started"
```
This is crucial to skipping re-runs on loop tasks.
- **Ensure** that once a loop’s sub-tasks are all “completed,” the loop’s status is set to “completed,” and it transitions to the next major tasks (like `upload_to_huggingface`).
- **Check** if the loop’s condition is correct. If you want a single pass, do not implement a condition that leads back to the same loop.
- Also, you can check you do not have “overlapping conditions” that cause re-entry.
---
### Conclusion
**Hence,** the main challenge is that the reset logic (meant to let normal tasks be re-run) conflicts with a loop task’s one-pass usage. Once you avoid resetting the loop tasks or sub-tasks, you can finish them once, mark them “completed,” and properly proceed to the next stage.
Dont remove any logging or debug statements, as it will help you to understand the flow of the code.
+73
View File
@@ -0,0 +1,73 @@
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.tools import cot_save, cot_upload_to_huggingface
# Create a COT agent for generating and managing chain of thought solutions
cot_agent = Agent(
name="COTGenerator",
role="Chain of Thought Specialist",
goal="Generate and manage chain of thought solutions for Q&A pairs",
backstory="Expert in breaking down problems and generating detailed solution steps",
tools=[cot_save],
llm="gpt-4o-mini", # Using recommended model
)
save_to_huggingface_agent = Agent(
name="SaveToHuggingface",
role="Save to Huggingface",
goal="Save the generated chain of thought solutions to a Huggingface dataset",
backstory="Expert in saving data to Huggingface",
tools=[cot_upload_to_huggingface],
llm="gpt-4o-mini", # Using recommended model
)
# Define question-answer pairs to process in batches
qa_pairs = [
{
"question": "What is the sum of numbers from 1 to 10?",
"answer": "55"
},
{
"question": "Calculate the area of a circle with radius 5",
"answer": "78.54"
}
]
# Process qa_pairs in batches
batch_size = 2
for i in range(0, len(qa_pairs), batch_size):
batch_qa = qa_pairs[i:i + batch_size]
# Create task for current batch
qa_text = "\n".join(
f"- Q: {qa['question']}\n A: {qa['answer']}"
for qa in batch_qa
)
cot_task = Task(
description=(
"For each Q&A pair:\n"
"1. Use the tool cot_save to generate a chain of thought solution\n"
"2. Save to CSV file with filename 'cot_dataset.csv'\n\n"
f"Q&A Pairs:\n{qa_text}"
),
expected_output="Chain of thought solutions saved to CSV",
agent=cot_agent,
name=f"generate_cot_solutions_batch_{i//batch_size + 1}"
)
save_to_huggingface_task = Task(
description="Save to Huggingface dataset at mervinpraison/cot-dataset , cot_dataset.csv",
expected_output="Chain of thought solutions saved to Huggingface",
agent=save_to_huggingface_agent,
name=f"save_to_huggingface_batch_{i//batch_size + 1}"
)
# Initialize and run the agent
agents = PraisonAIAgents(
agents=[cot_agent, save_to_huggingface_agent],
tasks=[cot_task, save_to_huggingface_task]
)
# Execute current batch
result = agents.start()
print(f"\nBatch {i//batch_size + 1} completed. Solutions appended to CSV file.")
+142
View File
@@ -0,0 +1,142 @@
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.tools import cot_save, cot_upload_to_huggingface
from pydantic import BaseModel
import os
# Define Pydantic model for structured output
class DecisionModel(BaseModel):
response: str
decision: str
def write_csv(file_path, data):
"""Write data to CSV file."""
if not os.path.exists(file_path):
with open(file_path, 'w') as file:
file.write(data + '\n')
else:
with open(file_path, 'a') as file:
file.write(data + '\n')
return f"Data appended to {file_path}"
def count_questions(file_path):
"""Count lines in file."""
with open(file_path, 'r') as file:
return sum(1 for _ in file)
# Keep existing agents with minimal changes
qa_generator = Agent(
name="Generator",
role="Question Creator",
goal="Create challenging math and logic questions",
backstory="Expert in educational content creation",
llm="gpt-4o-mini",
tools=[write_csv, count_questions]
)
total_questions_evaluator = Agent(
name="TotalQuestionsEvaluator",
role="Total Questions Evaluator",
goal="Evaluate the total number of questions in qa_pairs.csv file",
backstory="Expert in evaluating the total number of questions in a file",
llm="gpt-4o-mini",
tools=[count_questions],
verbose=False
)
cot_generator = Agent(
name="COTGenerator",
role="Chain of Thought Specialist",
goal="Generate and manage chain of thought solutions for Q&A pairs",
backstory="Expert in breaking down problems and generating detailed solution steps",
tools=[cot_save],
llm="gpt-4o-mini",
verbose=False
)
upload_to_huggingface = Agent(
name="UploadToHuggingface",
role="Upload to Huggingface",
goal="Upload the generated chain of thought solutions to a Huggingface dataset",
backstory="Expert in saving data to Huggingface",
tools=[cot_upload_to_huggingface],
llm="gpt-4o-mini",
verbose=False
)
# Define tasks with workflow improvements
generate_task = Task(
description="""Generate question and answer in csv format without headers: question, answer and append to qa_pairs.csv file
generate 10 unique questions and answers and don't repeat on the same question and answer. Reponse with 'done' when done
with append mode as 'a'
Example question and answer:
question, answer
What is the sum of numbers from 1 to 10?, 55
Number of r's in the word strawberry, 3
""",
expected_output="append to qa_pairs.csv file with questions and answers and move to next task",
agent=qa_generator,
name="generate_task",
is_start=True,
next_tasks=["evaluate_total_questions"],
task_type="decision",
condition={
"more": "generate_task",
"done": "evaluate_total_questions"
}
)
evaluate_total_questions_task = Task(
description="Evaluate the total number of questions in qa_pairs.csv file is 1",
expected_output="Total number of questions in qa_pairs.csv file",
agent=total_questions_evaluator,
task_type="decision",
name="evaluate_total_questions",
condition={
"more": "generate_task",
"done": "generate_cot"
}
)
generate_cot_task = Task(
name="generate_cot",
description="""Generate chain of thought solutions for each question in the input file.
Save to cot_solutions.csv file
Don't generate chain of thought solutions again after receiving the response from Tool Call
After calling the tool, respond with a JSON object:
{
"response": "done",
"decision": "done"
}
""",
expected_output="done",
agent=cot_generator,
input_file="qa_pairs.csv",
task_type="loop",
next_tasks=["upload_to_huggingface"],
condition={
"done": ["upload_to_huggingface"],
"exit": [],
},
output_pydantic=DecisionModel # Use Pydantic model for output validation
)
upload_to_huggingface_task = Task(
name="upload_to_huggingface",
description="""Upload to Huggingface:
1. Save to cot_solutions.csv
2. Upload to mervinpraison/cot-dataset""",
expected_output="Dataset published successfully",
agent=upload_to_huggingface,
tools=[cot_upload_to_huggingface]
)
# Initialize workflow
agents = PraisonAIAgents(
agents=[qa_generator, total_questions_evaluator, cot_generator, upload_to_huggingface],
tasks=[generate_task, evaluate_total_questions_task, generate_cot_task, upload_to_huggingface_task],
process="workflow",
max_iter=30,
verbose=False
)
agents.start()
@@ -0,0 +1,19 @@
from praisonaiagents import Agent, Task, PraisonAIAgents
llm_config = {
"model": "deepseek/deepseek-reasoner",
"max_tokens": 1
}
reasoning_agent = Agent(role="Helpful Assistant", llm=llm_config, reasoning_steps=True)
small_agent = Agent(role="Helpful Assistant", llm="openai/gpt-3.5-turbo")
reasoning_task = Task(description="How many r's in the word 'Strawberry'?", agent=reasoning_agent)
small_task = Task(description="With the provided reasoning tell me how many r's in the word 'Strawberry'?", agent=small_agent)
agents = PraisonAIAgents(
agents=[reasoning_agent, small_agent],
tasks=[reasoning_task, small_task]
)
agents.start()
+5 -6
View File
@@ -7,8 +7,8 @@ generator = Agent(
goal="Generate initial solutions and incorporate feedback",
instructions=(
"1. Look at the context from previous tasks.\n"
"2. If you see that you have already produced 2 points, then add another 2 new points "
" so that the total becomes 10.\n"
"2. If you see that you have already produced 2 points, then add another 2 new points starting with 3. "
" so that the total becomes 4.\n"
"3. Otherwise, just produce the first 2 points.\n"
"4. Return only the final list of points, with no extra explanation."
)
@@ -17,10 +17,10 @@ generator = Agent(
evaluator = Agent(
name="Evaluator",
role="Solution evaluator",
goal="Evaluate solutions and provide improvement feedback",
goal="Evaluate solutions and respond with more or done",
instructions=(
"1. Count how many lines in the response start with a number and a period (like '1. ' or '2. ').\n"
"2. If there are 10 or more, respond with 'done'.\n"
"2. If there are 4 or more, respond with 'done'.\n"
"3. Otherwise, respond with 'more'.\n"
"4. Return only the single word: 'done' or 'more'."
)
@@ -40,10 +40,9 @@ generate_task = Task(
evaluate_task = Task(
name="evaluate",
description="Check if there are 10 points about AI",
description="Check if there are 4 points about AI. ONLY RESPOND WITH 'more' or 'done' in your response field",
expected_output="more or done",
agent=evaluator,
next_tasks=["generate"],
context=[generate_task],
task_type="decision",
condition={
+14
View File
@@ -0,0 +1,14 @@
from praisonaiagents import Agent, Task, PraisonAIAgents
reasoning_agent = Agent(role="Helpful Assistant", reasoning_steps=True, llm="deepseek/deepseek-reasoner")
small_agent = Agent(role="Helpful Assistant", llm="openai/gpt-3.5-turbo")
reasoning_task = Task(description="How many r's in the word 'Strawberry'?", agent=reasoning_agent)
small_task = Task(description="With the provided reasoning tell me how many r's in the word 'Strawberry'?", agent=small_agent)
agents = PraisonAIAgents(
agents=[reasoning_agent, small_agent],
tasks=[reasoning_task, small_task]
)
agents.start()
File renamed without changes.
File renamed without changes.
+30
View File
@@ -0,0 +1,30 @@
from praisonaiagents import Agent, Task, PraisonAIAgents
from langchain_community.tools import YouTubeSearchTool
from langchain_community.utilities import WikipediaAPIWrapper
# Create an agent with both tools
agent = Agent(
name="SearchAgent",
role="Research Assistant",
goal="Search for information from multiple sources",
backstory="I am an AI assistant that can search YouTube and Wikipedia.",
tools=[YouTubeSearchTool, WikipediaAPIWrapper],
llm="openai/gpt-4o-mini"
)
# Create tasks to demonstrate both tools
task = Task(
name="search_task",
description="Search for information about 'AI advancements' on both YouTube and Wikipedia",
expected_output="Combined information from YouTube videos and Wikipedia articles",
agent=agent
)
# Create and start the workflow
agents = PraisonAIAgents(
agents=[agent],
tasks=[task],
verbose=True
)
agents.start()
+2 -1
View File
@@ -97,7 +97,8 @@ workflow = PraisonAIAgents(
agents=[router, worker1, worker2, worker3, synthesizer],
tasks=[router_task, worker1_task, worker2_task, worker3_task, synthesize_task],
process="workflow",
verbose=True
verbose=True,
max_iter=10
)
# Run orchestrated workflow
+172 -103
View File
@@ -97,36 +97,79 @@ def process_stream_chunks(chunks):
content_list = []
reasoning_list = []
tool_calls = []
current_tool_call = None
# First pass: Get initial tool call data
for chunk in chunks:
if not hasattr(chunk, "choices") or not chunk.choices:
continue
# Track usage from each chunk
if hasattr(chunk, "usage"):
completion_tokens += getattr(chunk.usage, "completion_tokens", 0)
prompt_tokens += getattr(chunk.usage, "prompt_tokens", 0)
delta = getattr(chunk.choices[0], "delta", None)
if not delta:
continue
# Handle content and reasoning
if hasattr(delta, "content") and delta.content:
content_list.append(delta.content)
if hasattr(delta, "reasoning_content") and delta.reasoning_content:
reasoning_list.append(delta.reasoning_content)
# Handle tool calls
if hasattr(delta, "tool_calls") and delta.tool_calls:
for tool_call_delta in delta.tool_calls:
if tool_call_delta.index is not None and tool_call_delta.id:
# Found the initial tool call
current_tool_call = {
"id": tool_call_delta.id,
"type": "function",
"function": {
"name": tool_call_delta.function.name,
"arguments": ""
}
}
while len(tool_calls) <= tool_call_delta.index:
tool_calls.append(None)
tool_calls[tool_call_delta.index] = current_tool_call
current_tool_call = tool_calls[tool_call_delta.index]
elif current_tool_call is not None and hasattr(tool_call_delta.function, "arguments"):
if tool_call_delta.function.arguments:
current_tool_call["function"]["arguments"] += tool_call_delta.function.arguments
# Remove any None values and empty tool calls
tool_calls = [tc for tc in tool_calls if tc and tc["id"] and tc["function"]["name"]]
combined_content = "".join(content_list) if content_list else ""
combined_reasoning = "".join(reasoning_list) if reasoning_list else None
finish_reason = getattr(last_chunk.choices[0], "finish_reason", None) if hasattr(last_chunk, "choices") and last_chunk.choices else None
# Create ToolCall objects
processed_tool_calls = []
if tool_calls:
try:
from openai.types.chat import ChatCompletionMessageToolCall
for tc in tool_calls:
tool_call = ChatCompletionMessageToolCall(
id=tc["id"],
type=tc["type"],
function={
"name": tc["function"]["name"],
"arguments": tc["function"]["arguments"]
}
)
processed_tool_calls.append(tool_call)
except Exception as e:
print(f"Error processing tool call: {e}")
message = ChatCompletionMessage(
content=combined_content,
reasoning_content=combined_reasoning
role="assistant",
reasoning_content=combined_reasoning,
tool_calls=processed_tool_calls if processed_tool_calls else None
)
choice = Choice(
finish_reason=finish_reason,
finish_reason=finish_reason or "tool_calls" if processed_tool_calls else None,
index=0,
message=message
)
@@ -528,6 +571,53 @@ Your Goal: {self.goal}
def __str__(self):
return f"Agent(name='{self.name}', role='{self.role}', goal='{self.goal}')"
def _process_stream_response(self, messages, temperature, start_time, formatted_tools=None, reasoning_steps=False):
"""Process streaming response and return final response"""
try:
# Create the response stream
response_stream = client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature,
tools=formatted_tools if formatted_tools else None,
stream=True
)
full_response_text = ""
reasoning_content = ""
chunks = []
# Create Live display with proper configuration
with Live(
display_generating("", start_time),
console=self.console,
refresh_per_second=4,
transient=True,
vertical_overflow="ellipsis",
auto_refresh=True
) as live:
for chunk in response_stream:
chunks.append(chunk)
if chunk.choices[0].delta.content:
full_response_text += chunk.choices[0].delta.content
live.update(display_generating(full_response_text, start_time))
# Update live display with reasoning content if enabled
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
rc = chunk.choices[0].delta.reasoning_content
if rc:
reasoning_content += rc
live.update(display_generating(f"{full_response_text}\n[Reasoning: {reasoning_content}]", start_time))
# Clear the last generating display with a blank line
self.console.print()
final_response = process_stream_chunks(chunks)
return final_response
except Exception as e:
display_error(f"Error in stream processing: {e}")
return None
def _chat_completion(self, messages, temperature=0.2, tools=None, stream=True, reasoning_steps=False):
start_time = time.time()
logging.debug(f"{self.name} sending messages to LLM: {messages}")
@@ -554,20 +644,31 @@ Your Goal: {self.goal}
logging.warning(f"Tool {tool} not recognized")
try:
initial_response = client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature,
tools=formatted_tools if formatted_tools else None,
stream=False
)
if stream:
# Process as streaming response with formatted tools
final_response = self._process_stream_response(
messages,
temperature,
start_time,
formatted_tools=formatted_tools if formatted_tools else None,
reasoning_steps=reasoning_steps
)
else:
# Process as regular non-streaming response
final_response = client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature,
tools=formatted_tools if formatted_tools else None,
stream=False
)
tool_calls = getattr(initial_response.choices[0].message, 'tool_calls', None)
tool_calls = getattr(final_response.choices[0].message, 'tool_calls', None)
if tool_calls:
messages.append({
"role": "assistant",
"content": initial_response.choices[0].message.content,
"role": "assistant",
"content": final_response.choices[0].message.content,
"tool_calls": tool_calls
})
@@ -590,55 +691,24 @@ Your Goal: {self.goal}
"content": results_str
})
if stream:
response_stream = client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature,
stream=True
)
full_response_text = ""
reasoning_content = ""
chunks = []
# Create Live display with proper configuration
with Live(
display_generating("", start_time),
console=self.console,
refresh_per_second=4,
transient=True,
vertical_overflow="ellipsis",
auto_refresh=True
) as live:
for chunk in response_stream:
chunks.append(chunk)
if chunk.choices[0].delta.content:
full_response_text += chunk.choices[0].delta.content
live.update(display_generating(full_response_text, start_time))
# Update live display with reasoning content if enabled
if reasoning_steps and hasattr(chunk.choices[0].delta, "reasoning_content"):
rc = chunk.choices[0].delta.reasoning_content
if rc:
reasoning_content += rc
live.update(display_generating(f"{full_response_text}\n[Reasoning: {reasoning_content}]", start_time))
# Clear the last generating display with a blank line
self.console.print()
final_response = process_stream_chunks(chunks)
return final_response
else:
if tool_calls:
# Get final response after tool calls
if stream:
final_response = self._process_stream_response(
messages,
temperature,
start_time,
formatted_tools=formatted_tools if formatted_tools else None,
reasoning_steps=reasoning_steps
)
else:
final_response = client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature,
stream=False
)
return final_response
else:
return initial_response
return final_response
except Exception as e:
display_error(f"Error in chat completion: {e}")
@@ -758,8 +828,7 @@ Your Goal: {self.goal}
tool_calls = getattr(response.choices[0].message, 'tool_calls', None)
response_text = response.choices[0].message.content.strip()
if tool_calls:
if tool_calls: ## TODO: Most likely this tool call is already called in _chat_completion, so maybe we can remove this.
messages.append({
"role": "assistant",
"content": response_text,
@@ -988,48 +1057,48 @@ Your Goal: {self.goal}
)
# Format tools if provided
formatted_tools = []
if tools:
for tool in tools:
if isinstance(tool, str):
tool_def = self._generate_tool_definition(tool)
if tool_def:
formatted_tools.append(tool_def)
elif isinstance(tool, dict):
formatted_tools.append(tool)
elif hasattr(tool, "to_openai_tool"):
formatted_tools.append(tool.to_openai_tool())
elif callable(tool):
formatted_tools.append(self._generate_tool_definition(tool.__name__))
formatted_tools = []
if tools:
for tool in tools:
if isinstance(tool, str):
tool_def = self._generate_tool_definition(tool)
if tool_def:
formatted_tools.append(tool_def)
elif isinstance(tool, dict):
formatted_tools.append(tool)
elif hasattr(tool, "to_openai_tool"):
formatted_tools.append(tool.to_openai_tool())
elif callable(tool):
formatted_tools.append(self._generate_tool_definition(tool.__name__))
# Create async OpenAI client
async_client = AsyncOpenAI()
# Create async OpenAI client
async_client = AsyncOpenAI()
# Make the API call based on the type of request
if tools:
response = await async_client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature,
tools=formatted_tools
)
return await self._achat_completion(response, tools)
elif output_json or output_pydantic:
response = await async_client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature,
response_format={"type": "json_object"}
)
# Return the raw response
return response.choices[0].message.content
else:
response = await async_client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature
)
return response.choices[0].message.content
# Make the API call based on the type of request
if tools:
response = await async_client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature,
tools=formatted_tools
)
return await self._achat_completion(response, tools)
elif output_json or output_pydantic:
response = await async_client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature,
response_format={"type": "json_object"}
)
# Return the raw response
return response.choices[0].message.content
else:
response = await async_client.chat.completions.create(
model=self.llm,
messages=messages,
temperature=temperature
)
return response.choices[0].message.content
except Exception as e:
display_error(f"Error in chat completion: {e}")
return None
+212 -22
View File
@@ -17,6 +17,8 @@ from ..main import (
from rich.console import Console
from rich.live import Live
# TODO: Include in-build tool calling in LLM class
# TODO: Restructure so that duplicate calls are not made (Sync with agent.py)
class LLMContextLengthExceededException(Exception):
"""Raised when LLM context length is exceeded"""
def __init__(self, message: str):
@@ -111,11 +113,28 @@ class LLM:
litellm.success_callback = []
litellm._async_success_callback = []
litellm.callbacks = []
# Additional logging suppression
litellm.suppress_debug_messages = True
litellm._logging._disable_debugging()
logging.getLogger("litellm.utils").setLevel(logging.WARNING)
logging.getLogger("litellm.main").setLevel(logging.WARNING)
verbose = extra_settings.get('verbose', True)
# Only suppress logs if not in debug mode
if not isinstance(verbose, bool) and verbose >= 10:
# Enable detailed debug logging
logging.getLogger("asyncio").setLevel(logging.DEBUG)
logging.getLogger("selector_events").setLevel(logging.DEBUG)
logging.getLogger("litellm.utils").setLevel(logging.DEBUG)
logging.getLogger("litellm.main").setLevel(logging.DEBUG)
litellm.suppress_debug_messages = False
litellm.set_verbose = True
else:
# Suppress debug logging for normal operation
logging.getLogger("asyncio").setLevel(logging.WARNING)
logging.getLogger("selector_events").setLevel(logging.WARNING)
logging.getLogger("litellm.utils").setLevel(logging.WARNING)
logging.getLogger("litellm.main").setLevel(logging.WARNING)
litellm.suppress_debug_messages = True
litellm._logging._disable_debugging()
warnings.filterwarnings("ignore", category=RuntimeWarning)
except ImportError:
raise ImportError(
"LiteLLM is required but not installed. "
@@ -143,7 +162,7 @@ class LLM:
self.extra_settings = extra_settings
self.console = Console()
self.chat_history = []
self.verbose = extra_settings.get('verbose', True)
self.verbose = verbose
self.markdown = extra_settings.get('markdown', True)
self.self_reflect = extra_settings.get('self_reflect', False)
self.max_reflect = extra_settings.get('max_reflect', 3)
@@ -178,7 +197,8 @@ class LLM:
"""Enhanced get_response with all OpenAI-like features"""
try:
import litellm
reasoning_steps = kwargs.get('reasoning_steps', self.reasoning_steps)
# This below **kwargs** is passed to .completion() directly. so reasoning_steps has to be popped. OR find alternate best way of handling this.
reasoning_steps = kwargs.pop('reasoning_steps', self.reasoning_steps)
# Disable litellm debug messages
litellm.set_verbose = False
@@ -240,7 +260,7 @@ class LLM:
messages=messages,
temperature=temperature,
stream=False, # force non-streaming
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
reasoning_content = resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
response_text = resp["choices"][0]["message"]["content"]
@@ -344,7 +364,7 @@ class LLM:
messages=messages,
temperature=temperature,
stream=False, # force non-streaming
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
reasoning_content = resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
response_text = resp["choices"][0]["message"]["content"]
@@ -435,7 +455,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
temperature=temperature,
stream=False, # Force non-streaming
response_format={"type": "json_object"},
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
# Grab reflection text and optional reasoning
reasoning_content = reflection_resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
@@ -469,7 +489,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
temperature=temperature,
stream=True,
response_format={"type": "json_object"},
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
@@ -483,7 +503,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
temperature=temperature,
stream=True,
response_format={"type": "json_object"},
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
reflection_text += chunk.choices[0].delta.content
@@ -557,7 +577,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
"""Async version of get_response with identical functionality."""
try:
import litellm
reasoning_steps = kwargs.get('reasoning_steps', self.reasoning_steps)
reasoning_steps = kwargs.pop('reasoning_steps', self.reasoning_steps)
litellm.set_verbose = False
# Build messages list
@@ -669,7 +689,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
messages=messages,
temperature=temperature,
stream=False, # force non-streaming
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
reasoning_content = resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
response_text = resp["choices"][0]["message"]["content"]
@@ -731,7 +751,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
temperature=temperature,
stream=False,
tools=formatted_tools, # We safely pass tools here
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
# handle tool_calls from tool_response as usual...
tool_calls = tool_response.choices[0].message.get("tool_calls")
@@ -777,7 +797,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
temperature=temperature,
stream=False, # force non-streaming
tools=formatted_tools, # Include tools
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
reasoning_content = resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
response_text = resp["choices"][0]["message"]["content"]
@@ -807,7 +827,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
temperature=temperature,
stream=True,
tools=formatted_tools,
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
@@ -821,7 +841,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
messages=messages,
temperature=temperature,
stream=True,
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
response_text += chunk.choices[0].delta.content
@@ -867,7 +887,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
temperature=temperature,
stream=False, # Force non-streaming
response_format={"type": "json_object"},
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
)
# Grab reflection text and optional reasoning
reasoning_content = reflection_resp["choices"][0]["message"].get("provider_specific_fields", {}).get("reasoning_content")
@@ -901,7 +921,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
temperature=temperature,
stream=True,
response_format={"type": "json_object"},
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
content = chunk.choices[0].delta.content
@@ -915,7 +935,7 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
temperature=temperature,
stream=True,
response_format={"type": "json_object"},
**kwargs
**{k:v for k,v in kwargs.items() if k != 'reasoning_steps'}
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
reflection_text += chunk.choices[0].delta.content
@@ -1020,4 +1040,174 @@ Output MUST be JSON with 'reflection' and 'satisfactory'.
if type(event) in event_types:
litellm._async_success_callback.remove(event)
litellm.callbacks = events
litellm.callbacks = events
# Response without tool calls
def response(
self,
prompt: Union[str, List[Dict]],
system_prompt: Optional[str] = None,
temperature: float = 0.2,
stream: bool = True,
verbose: bool = True,
markdown: bool = True,
console: Optional[Console] = None,
**kwargs
) -> str:
"""Simple function to get model response without tool calls or complex features"""
try:
import litellm
import logging
logger = logging.getLogger(__name__)
litellm.set_verbose = False
start_time = time.time()
logger.debug("Using synchronous response function")
# Build messages list
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
# Add prompt to messages
if isinstance(prompt, list):
messages.append({"role": "user", "content": prompt})
else:
messages.append({"role": "user", "content": prompt})
# Get response from LiteLLM
if stream:
response_text = ""
if verbose:
with Live(display_generating("", start_time), console=console or self.console, refresh_per_second=4) as live:
for chunk in litellm.completion(
model=self.model,
messages=messages,
temperature=temperature,
stream=True,
**kwargs
):
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:
for chunk in litellm.completion(
model=self.model,
messages=messages,
temperature=temperature,
stream=True,
**kwargs
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
response_text += chunk.choices[0].delta.content
else:
response = litellm.completion(
model=self.model,
messages=messages,
temperature=temperature,
stream=False,
**kwargs
)
response_text = response.choices[0].message.content.strip()
if verbose:
display_interaction(
prompt if isinstance(prompt, str) else prompt[0].get("text", ""),
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console or self.console
)
return response_text.strip()
except Exception as error:
display_error(f"Error in response: {str(error)}")
raise
# Async version of response function. Response without tool calls
async def aresponse(
self,
prompt: Union[str, List[Dict]],
system_prompt: Optional[str] = None,
temperature: float = 0.2,
stream: bool = True,
verbose: bool = True,
markdown: bool = True,
console: Optional[Console] = None,
**kwargs
) -> str:
"""Async version of response function"""
try:
import litellm
import logging
logger = logging.getLogger(__name__)
litellm.set_verbose = False
start_time = time.time()
logger.debug("Using asynchronous response function")
# Build messages list
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
# Add prompt to messages
if isinstance(prompt, list):
messages.append({"role": "user", "content": prompt})
else:
messages.append({"role": "user", "content": prompt})
# Get response from LiteLLM
if stream:
response_text = ""
if verbose:
with Live(display_generating("", start_time), console=console or self.console, refresh_per_second=4) as live:
async for chunk in await litellm.acompletion(
model=self.model,
messages=messages,
temperature=temperature,
stream=True,
**kwargs
):
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:
async for chunk in await litellm.acompletion(
model=self.model,
messages=messages,
temperature=temperature,
stream=True,
**kwargs
):
if chunk and chunk.choices and chunk.choices[0].delta.content:
response_text += chunk.choices[0].delta.content
else:
response = await litellm.acompletion(
model=self.model,
messages=messages,
temperature=temperature,
stream=False,
**kwargs
)
response_text = response.choices[0].message.content.strip()
if verbose:
display_interaction(
prompt if isinstance(prompt, str) else prompt[0].get("text", ""),
response_text,
markdown=markdown,
generation_time=time.time() - start_time,
console=console or self.console
)
return response_text.strip()
except Exception as error:
display_error(f"Error in response_async: {str(error)}")
raise
File diff suppressed because it is too large. Load diff
+18 -16
View File
@@ -38,7 +38,8 @@ class Task:
loop_state: Optional[Dict[str, Union[str, int]]] = None,
memory=None,
quality_check=True,
input_file: Optional[str] = None
input_file: Optional[str] = None,
rerun: bool = False # Renamed from can_rerun and logic inverted, default True for backward compatibility
):
# Add check if memory config is provided
if memory is not None or (config and config.get('memory_config')):
@@ -76,6 +77,7 @@ class Task:
self.loop_state = loop_state if loop_state else {}
self.memory = memory
self.quality_check = quality_check
self.rerun = rerun # Assigning the rerun parameter
# Set logger level based on config verbose level
verbose = self.config.get("verbose", 0)
@@ -83,7 +85,7 @@ class Task:
logger.setLevel(logging.INFO)
else:
logger.setLevel(logging.WARNING)
# Also set third-party loggers to WARNING
logging.getLogger('chromadb').setLevel(logging.WARNING)
logging.getLogger('openai').setLevel(logging.WARNING)
@@ -148,7 +150,7 @@ class Task:
logger.info(f"Task {self.id}: Initializing memory from config: {self.config['memory_config']}")
self.memory = Memory(config=self.config['memory_config'])
logger.info(f"Task {self.id}: Memory initialized successfully")
# Verify database was created
if os.path.exists(self.config['memory_config']['storage']['path']):
logger.info(f"Task {self.id}: Memory database exists after initialization")
@@ -182,11 +184,11 @@ class Task:
"""Execute callback and store quality metrics if enabled"""
logger.info(f"Task {self.id}: execute_callback called")
logger.info(f"Quality check enabled: {self.quality_check}")
# Initialize memory if not already initialized
if not self.memory:
self.memory = self.initialize_memory()
logger.info(f"Memory object exists: {self.memory is not None}")
if self.memory:
logger.info(f"Memory config: {self.memory.cfg}")
@@ -202,24 +204,24 @@ class Task:
except Exception as e:
logger.error(f"Task {self.id}: Failed to store task output in memory: {e}")
logger.exception(e)
logger.info(f"Task output: {task_output.raw[:100]}...")
if self.quality_check and self.memory:
try:
logger.info(f"Task {self.id}: Starting memory operations")
logger.info(f"Task {self.id}: Calculating quality metrics for output: {task_output.raw[:100]}...")
# Get quality metrics from LLM
metrics = self.memory.calculate_quality_metrics(
task_output.raw,
self.expected_output
)
logger.info(f"Task {self.id}: Quality metrics calculated: {metrics}")
quality_score = metrics.get("accuracy", 0.0)
logger.info(f"Task {self.id}: Quality score: {quality_score}")
# Store in both short and long-term memory with higher threshold
logger.info(f"Task {self.id}: Finalizing task output in memory...")
self.memory.finalize_task_output(
@@ -231,7 +233,7 @@ class Task:
task_id=self.id
)
logger.info(f"Task {self.id}: Finalized task output in memory")
# Store quality metrics separately
logger.info(f"Task {self.id}: Storing quality metrics...")
self.memory.store_quality(
@@ -240,7 +242,7 @@ class Task:
task_id=self.id,
metrics=metrics
)
# Store in both short and long-term memory with higher threshold
self.memory.finalize_task_output(
content=task_output.raw,
@@ -248,7 +250,7 @@ class Task:
quality_score=quality_score,
threshold=0.7 # Only high quality outputs in long-term memory
)
# Build context for next tasks
if self.next_tasks:
logger.info(f"Task {self.id}: Building context for next tasks...")
@@ -257,7 +259,7 @@ class Task:
max_items=5
)
logger.info(f"Task {self.id}: Built context for next tasks: {len(context)} items")
logger.info(f"Task {self.id}: Memory operations complete")
except Exception as e:
logger.error(f"Task {self.id}: Failed to process memory operations: {e}")
@@ -295,7 +297,7 @@ Expected Output: {self.expected_output}.
context_results.append(
f"Previous task {context_item.name if context_item.name else context_item.description} has no result yet."
)
# Join unique context results
unique_contexts = list(dict.fromkeys(context_results)) # Remove duplicates
task_prompt += f"""
@@ -307,7 +309,7 @@ Context:
def execute_callback_sync(self, task_output: TaskOutput) -> None:
"""
Synchronous wrapper to ensure that execute_callback is awaited,
preventing 'Task was destroyed but pending!' warnings if called
preventing 'Task was destroyed but pending!' warnings if called
from non-async code.
"""
import asyncio
+22 -1
View File
@@ -135,6 +135,27 @@ TOOL_MAPPINGS = {
'group_by': ('.pandas_tools', 'PandasTools'),
'pivot_table': ('.pandas_tools', 'PandasTools'),
'pandas_tools': ('.pandas_tools', 'PandasTools'),
# Chain of Thought Training Tools
'cot_run': ('.train.data.generatecot', 'GenerateCOT'), # Orchestrates text solution
'cot_run_dict': ('.train.data.generatecot', 'GenerateCOT'), # Orchestrates dict-based solution
'cot_generate': ('.train.data.generatecot', 'GenerateCOT'), # Generate text solution
'cot_generate_dict': ('.train.data.generatecot', 'GenerateCOT'), # Generate structured solution
'cot_improve': ('.train.data.generatecot', 'GenerateCOT'), # Improve text solution
'cot_improve_dict': ('.train.data.generatecot', 'GenerateCOT'), # Improve dict-based solution
'cot_check': ('.train.data.generatecot', 'GenerateCOT'), # Check correctness
'cot_find_error': ('.train.data.generatecot', 'GenerateCOT'), # Locate error in solution
'cot_load_answers': ('.train.data.generatecot', 'GenerateCOT'), # Load QA pairs
# COT Save/Export with QA Pairs
'cot_save_solutions_with_qa_pairs': ('.train.data.generatecot', 'GenerateCOT'), # Save with QA pairs
'cot_append_solutions_with_qa_pairs': ('.train.data.generatecot', 'GenerateCOT'), # Append with QA pairs
'cot_export_json_with_qa_pairs': ('.train.data.generatecot', 'GenerateCOT'), # Export JSON with QA pairs
'cot_export_csv_with_qa_pairs': ('.train.data.generatecot', 'GenerateCOT'), # Export CSV with QA pairs
'cot_append_csv_with_qa_pairs': ('.train.data.generatecot', 'GenerateCOT'), # Append CSV with QA pairs
'cot_save': ('.train.data.generatecot', 'GenerateCOT'), # Save single QA to file
'cot_upload_to_huggingface': ('.train.data.generatecot', 'GenerateCOT'), # Upload dataset to HuggingFace
'cot_tools': ('.train.data.generatecot', 'GenerateCOT'), # Full toolkit access
}
_instances = {} # Cache for class instances
@@ -161,7 +182,7 @@ def __getattr__(name: str) -> Any:
]:
return getattr(module, name)
if name in ['file_tools', 'pandas_tools', 'wikipedia_tools',
'newspaper_tools', 'arxiv_tools', 'spider_tools', 'duckdb_tools', 'csv_tools', 'json_tools', 'excel_tools', 'xml_tools', 'yaml_tools', 'calculator_tools', 'python_tools', 'shell_tools']:
'newspaper_tools', 'arxiv_tools', 'spider_tools', 'duckdb_tools', 'csv_tools', 'json_tools', 'excel_tools', 'xml_tools', 'yaml_tools', 'calculator_tools', 'python_tools', 'shell_tools', 'cot_tools']:
return module # Returns the callable module
return getattr(module, name)
else:
+54 -23
View File
@@ -89,7 +89,7 @@ class CSVTools:
def write_csv(
self,
filepath: str,
data: List[Dict[str, Any]],
data: Union[List[Dict[str, Any]], str],
encoding: str = 'utf-8',
delimiter: str = ',',
index: bool = False,
@@ -102,35 +102,66 @@ class CSVTools:
Args:
filepath: Path to CSV file
data: List of row dicts to write
encoding: File encoding
delimiter: Column delimiter
index: Whether to write row indices
header: Whether to write column headers
float_format: Format string for float values
date_format: Format string for date values
mode: Write mode ('w' for write, 'a' for append)
data: Either a list of dictionaries or a string containing CSV data
If string, each line should be comma-separated values
encoding: File encoding (default: 'utf-8')
delimiter: Column delimiter (default: ',')
index: Whether to write row indices (default: False)
header: Whether to write column headers (default: True)
float_format: Format string for float values (default: None)
date_format: Format string for date values (default: None)
mode: Write mode - 'w' for write, 'a' for append (default: 'w')
Returns:
bool: Success status
bool: True if successful, False otherwise
"""
try:
pd = self._get_pandas()
if pd is None:
return False
df = pd.DataFrame(data)
df.to_csv(
filepath,
encoding=encoding,
sep=delimiter,
index=index,
header=header,
float_format=float_format,
date_format=date_format,
mode=mode
)
return True
# Handle string input
if isinstance(data, str):
# Convert string to list of dicts
rows = []
if delimiter in data:
# Get existing columns if file exists and in append mode
existing_cols = []
if mode == 'a' and Path(filepath).exists():
try:
existing_df = pd.read_csv(filepath, nrows=1)
existing_cols = existing_df.columns.tolist()
except:
pass
values = [v.strip() for v in data.split(delimiter)]
if existing_cols:
# Use existing column names
row_dict = dict(zip(existing_cols, values))
else:
# Create generic column names
row_dict = {f'col{i}': val for i, val in enumerate(values)}
rows.append(row_dict)
data = rows
df = pd.DataFrame(data)
# Handle append mode properly
write_header = header if mode == 'w' else (header and not Path(filepath).exists())
df.to_csv(
filepath,
encoding=encoding,
sep=delimiter,
index=index,
header=write_header,
float_format=float_format,
date_format=date_format,
mode=mode
)
return True
except Exception as e:
error_msg = f"Error writing CSV file {filepath}: {str(e)}"
@@ -0,0 +1,531 @@
from typing import Dict, Optional, Union, Any
import json
from datetime import datetime
import os
import logging
# Setup logging based on environment variable
log_level = os.getenv('LOGLEVEL', 'INFO').upper()
logging.basicConfig(
level=log_level,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
# Lazy loader for LLM
def get_llm():
try:
from praisonaiagents.llm.llm import LLM
return LLM
except ImportError:
raise ImportError(
"LLM is required for this toolbut not installed. "
"Please install with: pip install 'praisonaiagents[llm]' datasets huggingface-hub pandas"
)
class GenerateCOT:
def __init__(
self,
qa_pairs: Optional[Dict[str, str]] = None,
model: str = "gpt-4o-mini",
api_key: Optional[str] = None,
max_attempts: int = 3,
verbose: bool = True,
temperature: float = 0.5
):
self.qa_pairs = qa_pairs or {}
self.max_attempts = max_attempts
self.solutions = {}
self.llm = get_llm()(model=model) # Get LLM class and instantiate
self.model = model
self.temperature = temperature
self.verbose = verbose
logger.debug(f"Initialized GenerateCOT with model={model}, max_attempts={max_attempts}")
def _ask_ai(self, prompt: str) -> str:
logger.debug(f"Sending prompt to LLM: {prompt[:100]}...")
response = self.llm.response(prompt, temperature=self.temperature)
logger.debug(f"Received response: {response[:100]}...")
return response
def _build_solution_prompt(self, question: str, context: str) -> str:
return f"""
Solve this problem step by step: {question}
Context: {context}
Steps needed:
1. Break down the problem
2. Show your work
3. Explain each step
4. Give final answer
"""
def cot_generate(self, question: str, context: str = "") -> str:
prompt = self._build_solution_prompt(question, context)
return self._ask_ai(prompt)
def cot_check(self, question: str, answer: str) -> bool:
if question not in self.qa_pairs:
raise ValueError(f"No correct answer found for: {question}")
prompt = f"""
Question: {question}
Given Answer: {answer}
Correct Answer: {self.qa_pairs[question]}
Is the given answer correct? Reply only with 'true' or 'false'.
"""
return self._ask_ai(prompt).lower().strip() == "true"
def cot_find_error(self, question: str, solution: str) -> int:
if self.cot_check(question, solution):
return -1
sentences = [s.strip() for s in solution.replace('。', '.').split('.') if s.strip()]
left, right = 0, len(sentences)
while left < right:
mid = (left + right) // 2
partial = '. '.join(sentences[:mid]) + '.'
if self.cot_check(question, partial):
left = mid + 1
else:
right = mid
return left
def cot_improve(self, question: str, current: str) -> str:
best_solution = current
best_score = self._rate_solution(question, current)
attempts = 0
while attempts < self.max_attempts:
attempts += 1
new_solution = self.cot_generate(question, current)
new_score = self._rate_solution(question, new_solution)
if new_score > best_score:
best_solution = new_solution
best_score = new_score
if best_score > 0.8:
break
return best_solution
def _rate_solution(self, question: str, solution: str) -> float:
prompt = f"""
Rate this solution from 0 to 1:
Question: {question}
Solution: {solution}
Correct Answer: {self.qa_pairs.get(question, '')}
Return only a number between 0 and 1.
"""
try:
score = float(self._ask_ai(prompt))
return min(max(score, 0), 1)
except ValueError:
return 0.0
def cot_run(self, question: str) -> str:
"""Run COT generation for a single question."""
solution = self.cot_generate(question)
if self.cot_check(question, solution):
return solution
solution = self.cot_improve(question, solution)
error_pos = self.cot_find_error(question, solution)
if error_pos != -1:
correct_part = '. '.join(solution.split('. ')[:error_pos]) + '.'
solution = self.cot_generate(question, correct_part)
self.solutions[question] = {
"solution": solution,
"error_position": error_pos,
}
return solution
def cot_load_answers(self, filepath: str) -> bool:
try:
with open(filepath, 'r', encoding='utf-8') as f:
data = json.load(f)
self.qa_pairs.update(data)
return True
except Exception as e:
print(f"Error loading answers: {e}")
return False
def _is_qa_pairs(self, qa_pairs: Any) -> bool:
"""Validate if input is a proper QA pairs dictionary."""
if not qa_pairs:
return True # None or empty is valid
if not isinstance(qa_pairs, dict):
raise ValueError("qa_pairs must be a dictionary with questions as keys and answers as values")
return True
def cot_append_solutions_with_qa_pairs(
self,
filepath: str = 'solutions.json',
qa_pairs: Optional[Dict[str, str]] = None
) -> None:
"""Appends current solutions to existing file or creates a new one."""
try:
self._is_qa_pairs(qa_pairs) # Validate format
if qa_pairs:
self.qa_pairs.update(qa_pairs)
data = {
"solutions": self.solutions,
"qa_pairs": self.qa_pairs,
"saved_at": datetime.now().isoformat()
}
existing_data = {}
try:
with open(filepath, 'r', encoding='utf-8') as f:
existing_data = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
pass
if existing_data:
existing_data["solutions"].update(data["solutions"])
existing_data["qa_pairs"].update(data["qa_pairs"])
existing_data["saved_at"] = data["saved_at"]
data = existing_data
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"Error appending solutions: {e}")
def cot_save_solutions_with_qa_pairs(
self,
filepath: str = 'solutions.json',
append: bool = False,
qa_pairs: Optional[Dict[str, str]] = None
) -> None:
try:
self._is_qa_pairs(qa_pairs) # Validate format
if qa_pairs:
self.qa_pairs.update(qa_pairs)
if append:
self.cot_append_solutions_with_qa_pairs(filepath)
return
data = {
"solutions": self.solutions,
"qa_pairs": self.qa_pairs,
"saved_at": datetime.now().isoformat()
}
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except Exception as e:
print(f"Error saving solutions: {e}")
def cot_generate_dict(self, question: str, context: str = "") -> dict:
prompt = self._build_solution_prompt(question, context)
thought_process = self._ask_ai(prompt)
final_answer_prompt = f"""
Based on this solution, what is the final answer only:
{thought_process}
Give only the final answer, no explanation.
"""
final_answer = self._ask_ai(final_answer_prompt)
return {
"thought_process": thought_process,
"final_answer": final_answer
}
def cot_improve_dict(self, question: str, current_solution: str) -> dict:
"""
Improves the existing solution (text form), returning the best dictionary-based version.
"""
best_solution = {
"thought_process": current_solution,
"final_answer": current_solution
}
best_score = self._rate_solution(question, current_solution)
attempts = 0
while attempts < self.max_attempts:
attempts += 1
new_solution = self.cot_generate_dict(question, current_solution)
new_score = self._rate_solution(question, new_solution["thought_process"])
if new_score > best_score:
best_solution = new_solution
best_score = new_score
if best_score > 0.8:
break
return best_solution
def cot_run_dict(self, question: str) -> dict:
"""Uses the dictionary-based solution approach, storing the final solution in self.solutions."""
logger.debug(f"Starting cot_run_dict for question: {question}")
solution = self.cot_generate_dict(question)
logger.debug(f"Initial solution generated: {str(solution)[:100]}...")
if self.cot_check(question, solution["final_answer"]):
logger.debug("Initial solution passed verification")
self.solutions[question] = solution
return solution
logger.debug("Initial solution failed verification, attempting improvement")
improved = self.cot_improve_dict(question, solution["thought_process"])
if self.cot_check(question, improved["final_answer"]):
logger.debug("Improved solution passed verification")
self.solutions[question] = improved
return improved
logger.debug("Checking for errors in improved solution")
error_pos = self.cot_find_error(question, improved["thought_process"])
if error_pos != -1:
logger.debug(f"Found error at position {error_pos}, generating final solution")
partial_solution = '. '.join(improved["thought_process"].split('. ')[:error_pos]) + '.'
final = self.cot_generate_dict(question, partial_solution)
self.solutions[question] = final
return final
logger.debug("Using improved solution as final result")
self.solutions[question] = improved
return improved
def cot_export_json_with_qa_pairs(
self,
filepath: str = 'dataset.json',
save_to_file: bool = True,
qa_pairs: Optional[Dict[str, str]] = None
) -> Union[str, list]:
"""
Export solutions in Alpaca training format with their full thought process.
"""
try:
self._is_qa_pairs(qa_pairs) # Validate format
if qa_pairs:
self.qa_pairs.update(qa_pairs)
# Generate solutions if empty
if not self.solutions:
for question in qa_pairs:
self.cot_run_dict(question)
alpaca_data = []
for question, sol in self.solutions.items():
alpaca_data.append({
"instruction": question,
"input": "",
"output": sol.get("thought_process", "")
})
if not save_to_file:
return alpaca_data
with open(filepath, 'w', encoding='utf-8') as f:
json.dump(alpaca_data, f, ensure_ascii=False, indent=2)
return filepath
except Exception as e:
print(f"Error exporting to Alpaca format: {e}")
return None
def cot_export_csv_with_qa_pairs(
self,
filepath: str = 'dataset.csv',
qa_pairs: Optional[Dict[str, str]] = None
) -> Optional[str]:
"""Export solutions in CSV format."""
try:
import csv
self._is_qa_pairs(qa_pairs) # Validate format
if qa_pairs:
self.qa_pairs.update(qa_pairs)
# Generate solutions if empty
if not self.solutions:
for question in qa_pairs:
self.cot_run_dict(question)
with open(filepath, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['instruction', 'input', 'output'])
for question, sol in self.solutions.items():
writer.writerow([question, '', sol.get("thought_process", "")])
return filepath
except Exception as e:
print(f"Error exporting to CSV format: {e}")
return None
def cot_save(
self,
question: str,
answer: str,
filepath: str = 'dataset.csv'
) -> Optional[str]:
"""Save a single question-answer pair with chain of thought to CSV file."""
logger.debug(f"Saving QA pair to {filepath}")
try:
self.qa_pairs[question] = answer
logger.debug("Added QA pair to internal dictionary")
solution = self.cot_run_dict(question)
logger.debug("Generated solution for question")
import csv
import os
file_exists = os.path.exists(filepath)
logger.debug(f"File exists: {file_exists}")
with open(filepath, 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
if not file_exists:
logger.debug("Creating new file with headers")
writer.writerow(['instruction', 'input', 'output'])
writer.writerow([question, '', solution.get("thought_process", "")])
logger.debug("Wrote solution to file")
return filepath
except Exception as e:
logger.error(f"Error saving to CSV: {str(e)}")
return None
# Rename existing function to indicate it handles qa_pairs dictionary
def cot_append_csv_with_qa_pairs(
self,
filepath: str = 'dataset.csv',
qa_pairs: Optional[Dict[str, str]] = None
) -> Optional[str]:
"""Append solutions to CSV file using qa_pairs dictionary."""
try:
self._is_qa_pairs(qa_pairs) # Validate format
if qa_pairs:
self.qa_pairs.update(qa_pairs)
import csv
import os
file_exists = os.path.exists(filepath)
with open(filepath, 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
if not file_exists:
writer.writerow(['instruction', 'input', 'output'])
for question, sol in self.solutions.items():
writer.writerow([question, '', sol.get("thought_process", "")])
return filepath
except Exception as e:
print(f"Error appending to CSV: {e}")
return None
def cot_upload_to_huggingface(
self,
huggingface_username: str,
dataset_name: str,
filepath: str,
private: bool = False
) -> str:
"""Upload generated solutions to HuggingFace datasets."""
logger.debug(f"Attempting to upload {filepath} to HuggingFace as {dataset_name}")
try:
from datasets import Dataset
import pandas as pd
logger.debug(f"Loading data from {filepath}")
if filepath.endswith('.csv'):
data = pd.read_csv(filepath)
logger.debug(f"Loaded CSV with {len(data)} rows")
elif filepath.endswith('.json'):
data = pd.read_json(filepath)
logger.debug(f"Loaded JSON with {len(data)} records")
else:
raise ValueError("Only CSV and JSON files are supported")
logger.debug("Converting to HuggingFace dataset")
dataset = Dataset.from_pandas(data)
repo_id = f"{huggingface_username}/{dataset_name}"
logger.debug(f"Pushing to hub: {repo_id}")
dataset.push_to_hub(
repo_id,
private=private
)
logger.debug("Upload completed successfully")
return f"Dataset uploaded successfully to {repo_id}"
except Exception as e:
logger.error(f"Error uploading to HuggingFace: {str(e)}")
return None
# Usage example:
if __name__ == "__main__":
# Direct QA Pairs Export Example
print("\n=== Direct QA Pairs Export Example ===")
direct_qa_data = {
"Number of r's in the word strawberry": "3"
}
direct_generator = GenerateCOT()
# Export with qa_pairs passed directly to functions
direct_generator.cot_export_csv_with_qa_pairs(
filepath='direct_solutions.csv',
qa_pairs=direct_qa_data
)
# Example of using cot_save for a single QA pair
direct_generator.cot_save(
question="What is the capital of France?",
answer="Paris",
filepath="single_qa.csv"
)
# Upload to HuggingFace
direct_generator.cot_upload_to_huggingface(
huggingface_username="mervinpraison",
dataset_name="cot-test",
filepath="single_qa.csv"
)
# direct_generator.cot_export_json_with_qa_pairs(
# filepath='direct_solutions.json',
# qa_pairs=direct_qa_data
# )
# # Rest of the original examples...
# qa_data = {
# "What is 2+2?": "4",
# "How many letters in 'hello'?": "5"
# }
# generator = GenerateCOT(qa_pairs=qa_data)
# for question in qa_data:
# solution = generator.cot_run(question)
# print(f"Question: {question}")
# print(f"Solution: {solution}\n")
# answer = generator.cot_run("What is 2+2?")
# print(answer)
# # Additional QA data processing example
# print("\n=== Processing Additional QA Data ===")
# extra_qa_data = {
# "What is 5 * 3?": "15"
# }
# # Create separate generator for additional data
# extra_generator = GenerateCOT(qa_pairs=extra_qa_data)
# # Process and save solutions
# for question in extra_qa_data:
# solution = extra_generator.cot_run_dict(question)
# print(f"Processing extra question: {question}")
# # Save solutions separately
# extra_generator.cot_save_solutions_with_qa_pairs('extra_qa_solutions.json')
# # Export in Alpaca format
# extra_generator.cot_export_json_with_qa_pairs(filepath='extra_qa_alpaca.json', save_to_file=True)
# # Demonstrate loading saved data
# loaded_generator = GenerateCOT(qa_pairs={})
# loaded_generator.cot_load_answers('extra_qa_solutions.json')
# print("\nLoaded extra QA pairs:", loaded_generator.qa_pairs)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.53"
version = "0.0.60"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
authors = [
{ name="Mervin Praison" }
+2 -1
View File
@@ -6,7 +6,8 @@ agent = Agent(
)
task = Task(
description="Create the list of tasks to be looped through.",
description="complete the task",
expected_output="task completed",
agent=agent,
task_type="loop",
input_file="tasks.csv"
+1 -1
View File
@@ -1868,7 +1868,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.53"
version = "0.0.60"
source = { editable = "." }
dependencies = [
{ name = "openai" },
+1
View File
@@ -7,6 +7,7 @@ agent = Agent(
self_reflect=True,
min_reflect=3,
max_reflect=5,
llm="gpt-4o-mini"
)
agent.start(
"What is the history of AI?"
+124
View File
@@ -0,0 +1,124 @@
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.tools import cot_save, cot_upload_to_huggingface
from pydantic import BaseModel
import os
def print_data(data):
print(data)
return "done"
def upload_to_huggingface(data):
print(data)
return "done"
# Define Pydantic model for structured output
class DecisionModel(BaseModel):
response: str
decision: str
def write_csv(file_path, data):
"""Write data to CSV file."""
# delete file if exists
if os.path.exists(file_path):
os.remove(file_path)
with open(file_path, 'w') as file:
file.write(data + '\n')
return f"Data appended to {file_path}"
def count_questions(file_path):
"""Count lines in file."""
with open(file_path, 'r') as file:
return sum(1 for _ in file)
# Keep existing agents with minimal changes
qa_generator = Agent(
name="Generator",
role="Question Creator",
goal="Create challenging math and logic questions",
backstory="Expert in educational content creation",
llm="gpt-4o-mini",
tools=[write_csv, count_questions]
)
cot_generator = Agent(
name="COTGenerator",
role="Chain of Thought Specialist",
goal="Generate and manage chain of thought solutions for Q&A pairs",
backstory="Expert in breaking down problems and generating detailed solution steps",
tools=[print_data],
llm="gpt-4o-mini",
verbose=False
)
upload_to_huggingface = Agent(
name="UploadToHuggingface",
role="Upload to Huggingface",
goal="Upload the generated chain of thought solutions to a Huggingface dataset",
backstory="Expert in saving data to Huggingface",
tools=[upload_to_huggingface],
llm="gpt-4o-mini",
verbose=False
)
# Define tasks with workflow improvements
generate_task = Task(
description="""Generate question and answer in csv format without headers: question, answer and append to qa_pairs.csv file
generate 1 unique questions and answers and don't repeat on the same question and answer. Reponse with 'done' when done
with append mode as 'a'
Example question and answer:
question, answer
What is the sum of numbers from 1 to 10?, 55
Number of r's in the word strawberry, 3
""",
expected_output="append to qa_pairs.csv file with questions and answers and move to next task",
agent=qa_generator,
name="generate_task",
is_start=True,
next_tasks=["evaluate_total_questions"],
task_type="decision",
condition={
"more": "generate_task",
"done": "generate_cot"
}
)
generate_cot_task = Task(
name="generate_cot",
description="""Generate chain of thought solutions for each question in the input file.
After processing all questions, respond with a JSON object:
{
"response": "done",
"decision": "done"
}
""",
expected_output="done",
agent=cot_generator,
input_file="qa_pairs.csv",
task_type="loop",
next_tasks=["upload_to_huggingface"],
condition={
"done": ["upload_to_huggingface"],
"exit": [],
},
output_pydantic=DecisionModel # Use Pydantic model for output validation
)
upload_to_huggingface_task = Task(
name="upload_to_huggingface",
description="""Upload to Huggingface:
1. Save to cot_solutions.csv
2. Upload to mervinpraison/cot-dataset""",
expected_output="Dataset published successfully",
agent=upload_to_huggingface
)
# Initialize workflow
agents = PraisonAIAgents(
agents=[qa_generator, cot_generator, upload_to_huggingface],
tasks=[generate_task, generate_cot_task, upload_to_huggingface_task],
process="workflow",
max_iter=10,
verbose=False
)
agents.start()
+1 -1
View File
@@ -1,6 +1,6 @@
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install flask praisonai==2.0.58 gunicorn markdown
RUN pip install flask praisonai==2.0.60 gunicorn markdown
EXPOSE 8080
CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]
+1 -1
View File
@@ -110,7 +110,7 @@ Loads environment variables from .env file or system and sets them.</p>
file.write(&#34;FROM python:3.11-slim\n&#34;)
file.write(&#34;WORKDIR /app\n&#34;)
file.write(&#34;COPY . .\n&#34;)
file.write(&#34;RUN pip install flask praisonai==2.0.58 gunicorn markdown\n&#34;)
file.write(&#34;RUN pip install flask praisonai==2.0.60 gunicorn markdown\n&#34;)
file.write(&#34;EXPOSE 8080\n&#34;)
file.write(&#39;CMD [&#34;gunicorn&#34;, &#34;-b&#34;, &#34;0.0.0.0:8080&#34;, &#34;api:app&#34;]\n&#39;)
+289
View File
@@ -0,0 +1,289 @@
---
title: "Generate Synthetic Reasoning Data Agents"
sidebarTitle: "Generate Reasoning Data"
description: "Learn how to generate chain-of-thought reasoning data using PraisonAI Agents."
icon: "brain"
---
```mermaid
flowchart TD
Start[Start] --> Generator[Question Answer Generator Agent]
Generator --> Evaluator[Evaluator Agent]
Evaluator --> COT[Reasoning Steps / COT Generator Agent]
COT --> COT
COT --> Upload[HuggingFace Uploader Agent]
Upload --> End[End]
style Start fill:#8B0000,color:#fff
style Generator fill:#2E8B57,color:#fff
style Evaluator fill:#2E8B57,color:#fff
style COT fill:#2E8B57,color:#fff
style Upload fill:#2E8B57,color:#fff
style End fill:#8B0000,color:#fff
```
## What is Chain-of-Thought Generation?
Chain-of-Thought (CoT) Generation is a process where AI agents create detailed, step-by-step reasoning paths for solving problems. This involves generating questions, evaluating them, producing detailed solution steps, and making the data available for training and analysis.
## Quick Start
<Steps>
<Step title="Install Package">
First, install the PraisonAI Agents package:
```bash
pip install "praisonaiagents[llm]" datasets huggingface-hub pandas
```
</Step>
<Step title="Set API Key">
Set your OpenAI API key as an environment variable in your terminal:
```bash
export OPENAI_API_KEY=your_api_key_here
```
</Step>
<Step title="Create a file">
Create a new file `app.py` with the basic setup:
```python
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.tools import cot_save, cot_upload_to_huggingface
from pydantic import BaseModel
import os
# Define Pydantic model for structured output
class DecisionModel(BaseModel):
response: str
decision: str
def write_csv(file_path, data):
"""Write data to CSV file."""
if not os.path.exists(file_path):
with open(file_path, 'w') as file:
file.write(data + '\n')
else:
with open(file_path, 'a') as file:
file.write(data + '\n')
return f"Data appended to {file_path}"
def count_questions(file_path):
"""Count lines in file."""
with open(file_path, 'r') as file:
return sum(1 for _ in file)
# Create specialized agents
qa_generator = Agent(
name="Generator",
role="Question Creator",
goal="Create challenging math and logic questions",
backstory="Expert in educational content creation",
llm="gpt-4o-mini",
tools=[write_csv, count_questions]
)
total_questions_evaluator = Agent(
name="TotalQuestionsEvaluator",
role="Total Questions Evaluator",
goal="Evaluate the total number of questions in qa_pairs.csv file",
backstory="Expert in evaluating the total number of questions in a file",
llm="gpt-4o-mini",
tools=[count_questions],
verbose=False
)
cot_generator = Agent(
name="COTGenerator",
role="Chain of Thought Specialist",
goal="Generate and manage chain of thought solutions for Q&A pairs",
backstory="Expert in breaking down problems and generating detailed solution steps",
tools=[cot_save],
llm="gpt-4o-mini",
verbose=False
)
upload_to_huggingface = Agent(
name="UploadToHuggingface",
role="Upload to Huggingface",
goal="Upload the generated chain of thought solutions to a Huggingface dataset",
backstory="Expert in saving data to Huggingface",
tools=[cot_upload_to_huggingface],
llm="gpt-4o-mini",
verbose=False
)
# Define tasks with workflow
generate_task = Task(
description="""Generate question and answer in csv format without headers: question, answer and append to qa_pairs.csv file
generate 10 unique questions and answers and don't repeat on the same question and answer. Reponse with 'done' when done
with append mode as 'a'
Example question and answer:
question, answer
What is the sum of numbers from 1 to 10?, 55
Number of r's in the word strawberry, 3
""",
expected_output="append to qa_pairs.csv file with questions and answers and move to next task",
agent=qa_generator,
name="generate_task",
is_start=True,
next_tasks=["evaluate_total_questions"],
task_type="decision",
condition={
"more": "generate_task",
"done": "evaluate_total_questions"
}
)
evaluate_total_questions_task = Task(
description="Evaluate the total number of questions in qa_pairs.csv file is 1",
expected_output="Total number of questions in qa_pairs.csv file",
agent=total_questions_evaluator,
task_type="decision",
name="evaluate_total_questions",
condition={
"more": "generate_task",
"done": "generate_cot"
}
)
generate_cot_task = Task(
name="generate_cot",
description="""Generate chain of thought solutions for each question in the input file.
Save to cot_solutions.csv file
Don't generate chain of thought solutions again after receiving the response from Tool Call
After calling the tool, respond with a JSON object:
{
"response": "done",
"decision": "done"
}
""",
expected_output="done",
agent=cot_generator,
input_file="qa_pairs.csv",
task_type="loop",
next_tasks=["upload_to_huggingface"],
condition={
"done": ["upload_to_huggingface"],
"exit": [],
},
output_pydantic=DecisionModel # Use Pydantic model for output validation
)
upload_to_huggingface_task = Task(
name="upload_to_huggingface",
description="""Upload to Huggingface:
1. Save to cot_solutions.csv
2. Upload to mervinpraison/cot-dataset""",
expected_output="Dataset published successfully",
agent=upload_to_huggingface,
tools=[cot_upload_to_huggingface]
)
# Initialize workflow
agents = PraisonAIAgents(
agents=[qa_generator, total_questions_evaluator, cot_generator, upload_to_huggingface],
tasks=[generate_task, evaluate_total_questions_task, generate_cot_task, upload_to_huggingface_task],
process="workflow",
max_iter=30,
verbose=False
)
agents.start()
```
</Step>
</Steps>
## Features
<CardGroup cols={2}>
<Card title="Question Generation" icon="question">
Create challenging math and logic questions with answers.
</Card>
<Card title="Question Evaluation" icon="check-double">
Evaluate and validate generated questions for quality.
</Card>
<Card title="CoT Solutions" icon="diagram-project">
Generate detailed chain-of-thought solutions for each question.
</Card>
<Card title="Data Management" icon="database">
Save and manage generated data in structured formats.
</Card>
<Card title="HuggingFace Integration" icon="cloud-arrow-up">
Upload datasets directly to HuggingFace for sharing.
</Card>
</CardGroup>
## Understanding the Workflow
<AccordionGroup>
<Accordion title="Key Components">
<CardGroup cols={2}>
<Card title="Question Generator" icon="robot">
Creates unique math and logic questions with answers. Uses `write_csv` and `count_questions` tools.
</Card>
<Card title="Questions Evaluator" icon="magnifying-glass-chart">
Validates the total number of generated questions. Uses `count_questions` tool.
</Card>
<Card title="CoT Generator" icon="diagram-project">
Produces detailed step-by-step solutions. Uses `cot_save` tool for solution management.
</Card>
<Card title="HuggingFace Uploader" icon="cloud-arrow-up">
Publishes datasets to HuggingFace. Uses `cot_upload_to_huggingface` tool.
</Card>
</CardGroup>
</Accordion>
<Accordion title="Task Types and Flow Control">
<Tabs>
<Tab title="Decision Tasks">
Used in question generation and evaluation phases.
```python Decision Task Example
generate_task = Task(
task_type="decision",
condition={
"more": "generate_task",
"done": "evaluate_total_questions"
}
)
```
<Note>
Conditions determine whether to continue generating or move forward. The task can loop back to itself or proceed to the next task.
</Note>
</Tab>
<Tab title="Loop Tasks">
Used in Chain-of-Thought generation phase.
```python Loop Task Example
generate_cot_task = Task(
task_type="loop",
input_file="qa_pairs.csv",
output_pydantic=DecisionModel
)
```
<Warning>
Always use Pydantic models for output validation in loop tasks to ensure data consistency.
</Warning>
</Tab>
</Tabs>
<Info>
Each task type serves a specific purpose in the workflow:
- **Decision Tasks**: Control flow and branching logic
- **Loop Tasks**: Process data iteratively with validation
</Info>
</Accordion>
</AccordionGroup>
## Next Steps
<CardGroup>
<Card title="Introduction" icon="book" href="/introduction">
Learn more about PraisonAI and its core concepts
</Card>
<Card title="Quick Start" icon="bolt" href="/quickstart">
Get started with the basics of PraisonAI
</Card>
</CardGroup>
+1
View File
@@ -124,6 +124,7 @@
"features/langchain",
"features/async",
"features/mini",
"features/generate-reasoning",
"features/codeagent",
"features/mathagent",
"features/structured",
+142
View File
@@ -0,0 +1,142 @@
from praisonaiagents import Agent, Task, PraisonAIAgents
from praisonaiagents.tools import cot_save, cot_upload_to_huggingface
from pydantic import BaseModel
import os
# Define Pydantic model for structured output
class DecisionModel(BaseModel):
response: str
decision: str
def write_csv(file_path, data):
"""Write data to CSV file."""
if not os.path.exists(file_path):
with open(file_path, 'w') as file:
file.write(data + '\n')
else:
with open(file_path, 'a') as file:
file.write(data + '\n')
return f"Data appended to {file_path}"
def count_questions(file_path):
"""Count lines in file."""
with open(file_path, 'r') as file:
return sum(1 for _ in file)
# Keep existing agents with minimal changes
qa_generator = Agent(
name="Generator",
role="Question Creator",
goal="Create challenging math and logic questions",
backstory="Expert in educational content creation",
llm="gpt-4o-mini",
tools=[write_csv, count_questions]
)
total_questions_evaluator = Agent(
name="TotalQuestionsEvaluator",
role="Total Questions Evaluator",
goal="Evaluate the total number of questions in qa_pairs.csv file",
backstory="Expert in evaluating the total number of questions in a file",
llm="gpt-4o-mini",
tools=[count_questions],
verbose=False
)
cot_generator = Agent(
name="COTGenerator",
role="Chain of Thought Specialist",
goal="Generate and manage chain of thought solutions for Q&A pairs",
backstory="Expert in breaking down problems and generating detailed solution steps",
tools=[cot_save],
llm="gpt-4o-mini",
verbose=False
)
upload_to_huggingface = Agent(
name="UploadToHuggingface",
role="Upload to Huggingface",
goal="Upload the generated chain of thought solutions to a Huggingface dataset",
backstory="Expert in saving data to Huggingface",
tools=[cot_upload_to_huggingface],
llm="gpt-4o-mini",
verbose=False
)
# Define tasks with workflow improvements
generate_task = Task(
description="""Generate question and answer in csv format without headers: question, answer and append to qa_pairs.csv file
generate 10 unique questions and answers and don't repeat on the same question and answer. Reponse with 'done' when done
with append mode as 'a'
Example question and answer:
question, answer
What is the sum of numbers from 1 to 10?, 55
Number of r's in the word strawberry, 3
""",
expected_output="append to qa_pairs.csv file with questions and answers and move to next task",
agent=qa_generator,
name="generate_task",
is_start=True,
next_tasks=["evaluate_total_questions"],
task_type="decision",
condition={
"more": "generate_task",
"done": "evaluate_total_questions"
}
)
evaluate_total_questions_task = Task(
description="Evaluate the total number of questions in qa_pairs.csv file is 1",
expected_output="Total number of questions in qa_pairs.csv file",
agent=total_questions_evaluator,
task_type="decision",
name="evaluate_total_questions",
condition={
"more": "generate_task",
"done": "generate_cot"
}
)
generate_cot_task = Task(
name="generate_cot",
description="""Generate chain of thought solutions for each question in the input file.
Save to cot_solutions.csv file
Don't generate chain of thought solutions again after receiving the response from Tool Call
After calling the tool, respond with a JSON object:
{
"response": "done",
"decision": "done"
}
""",
expected_output="done",
agent=cot_generator,
input_file="qa_pairs.csv",
task_type="loop",
next_tasks=["upload_to_huggingface"],
condition={
"done": ["upload_to_huggingface"],
"exit": [],
},
output_pydantic=DecisionModel # Use Pydantic model for output validation
)
upload_to_huggingface_task = Task(
name="upload_to_huggingface",
description="""Upload to Huggingface:
1. Save to cot_solutions.csv
2. Upload to mervinpraison/cot-dataset""",
expected_output="Dataset published successfully",
agent=upload_to_huggingface,
tools=[cot_upload_to_huggingface]
)
# Initialize workflow
agents = PraisonAIAgents(
agents=[qa_generator, total_questions_evaluator, cot_generator, upload_to_huggingface],
tasks=[generate_task, evaluate_total_questions_task, generate_cot_task, upload_to_huggingface_task],
process="workflow",
max_iter=30,
verbose=False
)
agents.start()
+1 -1
View File
@@ -3,7 +3,7 @@ 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/2.0.58.tar.gz"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/2.0.60.tar.gz"
sha256 "1828fb9227d10f991522c3f24f061943a254b667196b40b1a3e4a54a8d30ce32" # Replace with actual SHA256 checksum
license "MIT"
+1 -1
View File
@@ -56,7 +56,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.0.58 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.0.60 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.0.58"
version = "2.0.60"
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.53",
"praisonaiagents>=0.0.60",
"python-dotenv>=0.19.0",
"instructor>=1.3.3",
"PyYAML>=6.0",
@@ -84,7 +84,7 @@ autogen = ["pyautogen>=0.2.19", "praisonai-tools>=0.0.7", "crewai"]
[tool.poetry]
name = "PraisonAI"
version = "2.0.58"
version = "2.0.60"
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 = ""
@@ -102,7 +102,7 @@ python = ">=3.10,<3.13"
rich = ">=13.7"
markdown = ">=3.5"
pyparsing = ">=3.0.0"
praisonaiagents = ">=0.0.53"
praisonaiagents = ">=0.0.60"
python-dotenv = ">=0.19.0"
instructor = ">=1.3.3"
PyYAML = ">=6.0"
Generated
+5 -5
View File
@@ -3060,7 +3060,7 @@ wheels = [
[[package]]
name = "praisonai"
version = "2.0.58"
version = "2.0.60"
source = { editable = "." }
dependencies = [
{ name = "instructor" },
@@ -3197,7 +3197,7 @@ requires-dist = [
{ name = "plotly", marker = "extra == 'realtime'", specifier = ">=5.24.0" },
{ name = "praisonai-tools", marker = "extra == 'autogen'", specifier = ">=0.0.7" },
{ name = "praisonai-tools", marker = "extra == 'crewai'", specifier = ">=0.0.7" },
{ name = "praisonaiagents", specifier = ">=0.0.53" },
{ name = "praisonaiagents", specifier = ">=0.0.60" },
{ 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" },
@@ -3250,16 +3250,16 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.53"
version = "0.0.60"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "openai" },
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/48/7c/c8070c6eb7fcabe22aac9a6b92f72cfcd5a04d02e8f7bcff0e42f62eb31f/praisonaiagents-0.0.53.tar.gz", hash = "sha256:02a8220977776c8a1574b160f3e12b90ac98adc9b3925773e9867c117e10e6f9", size = 87386 }
sdist = { url = "https://files.pythonhosted.org/packages/ba/31/19a0226dda6c6a367294b613296852e89294f5b839482157e8e7b8b6985a/praisonaiagents-0.0.60.tar.gz", hash = "sha256:38404fc976c086bde71ad51fff7b417a105574ba7877f71403625cef90494fc0", size = 100110 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/9b/0f9f2b146877d5b830173981030c1acb82d10ce719b27af085900c2c4763/praisonaiagents-0.0.53-py3-none-any.whl", hash = "sha256:dd7a08cba8dc283136e7f94111f0ced95f172a8a3d9fcdf64dda6f4f1521d7ff", size = 105546 },
{ url = "https://files.pythonhosted.org/packages/13/b0/ae8e2b0b3d0dc1e259e670506530db46cc18ab8300e98323788299e62a95/praisonaiagents-0.0.60-py3-none-any.whl", hash = "sha256:a3a73b60bb8fd760bdeeeed3a7dded49223dc3a29044e73d2af174a0179f710d", size = 118558 },
]
[[package]]