mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 01:08:39 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f62e9201a | ||
|
|
1b69360d9f | ||
|
|
9bdc6545ad | ||
|
|
06cd909367 | ||
|
|
67aeece7fc | ||
|
|
ef67d0753c | ||
|
|
f374f9f791 | ||
|
|
492fb29af9 | ||
|
|
cd56ebd5cf | ||
|
|
08e99f6ef7 | ||
|
|
670d653ae9 | ||
|
|
faa014b8b0 | ||
|
|
6d454d0c6e | ||
|
|
0ec3fe3510 | ||
|
|
707888cdf9 |
No files matched your search
+143
-1
@@ -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.
|
||||
@@ -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.")
|
||||
@@ -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()
|
||||
@@ -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={
|
||||
|
||||
@@ -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.
@@ -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()
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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" }
|
||||
|
||||
@@ -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"
|
||||
|
||||
Generated
+1
-1
@@ -1868,7 +1868,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "praisonaiagents"
|
||||
version = "0.0.53"
|
||||
version = "0.0.60"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "openai" },
|
||||
|
||||
@@ -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?"
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
@@ -110,7 +110,7 @@ Loads environment variables from .env file or system and sets them.</p>
|
||||
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')
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -124,6 +124,7 @@
|
||||
"features/langchain",
|
||||
"features/async",
|
||||
"features/mini",
|
||||
"features/generate-reasoning",
|
||||
"features/codeagent",
|
||||
"features/mathagent",
|
||||
"features/structured",
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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]]
|
||||
|
||||
Reference in new issue
Block a user