Compare commits

...
9 Commits
Author SHA1 Message Date
Mervin Praison 94cf264b4c Merge pull request #266 from MervinPraison/develop
setting openai_model_name env variable to set
2025-01-06 09:52:09 +00:00
MervinPraison ab44fd4b27 setting openai_model_name env variable to set 2025-01-06 09:51:50 +00:00
Mervin Praison 7bad6cf7e9 Merge pull request #265 from MervinPraison/develop
adding mini agents
2025-01-06 09:31:54 +00:00
MervinPraison 4ffbe48c58 adding mini agents 2025-01-06 09:31:39 +00:00
Mervin Praison 476eeb1184 Merge pull request #264 from MervinPraison/develop
bug fix ui load
2025-01-06 01:07:03 +00:00
MervinPraison 550769017a bug fix ui load 2025-01-06 01:06:24 +00:00
Mervin Praison cccd75ca75 Merge pull request #263 from MervinPraison/develop
Develop
2025-01-06 00:18:12 +00:00
MervinPraison 395f8fb5ef praisonai agents ui working with tools 2025-01-06 00:17:54 +00:00
MervinPraison e4893ad948 working with other callbacks 2025-01-05 23:08:20 +00:00
21 changed files with 661 additions and 880 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install flask praisonai==2.0.26 gunicorn markdown
RUN pip install flask praisonai==2.0.30 gunicorn markdown
EXPOSE 8080
CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]
View File
Whitespace-only changes.
+6
View File
@@ -0,0 +1,6 @@
from praisonaiagents import Agent, Agents, Tools
research_agent = Agent(instructions="You are a research agent to search internet about AI 2024", tools=[Tools.internet_search])
summarise_agent = Agent(instructions="You are a summarize agent to summarise in points")
agents = Agents(agents=[research_agent, summarise_agent])
agents.start()
+6
View File
@@ -5,6 +5,7 @@ Praison AI Agents - A package for hierarchical AI agent task execution
from .agent.agent import Agent
from .agents.agents import PraisonAIAgents
from .task.task import Task
from .tools.tools import Tools
from .main import (
TaskOutput,
ReflectionOutput,
@@ -21,9 +22,14 @@ from .main import (
async_display_callbacks,
)
# Add Agents as an alias for PraisonAIAgents
Agents = PraisonAIAgents
__all__ = [
'Agent',
'PraisonAIAgents',
'Agents',
'Tools',
'Task',
'TaskOutput',
'ReflectionOutput',
+52 -14
View File
@@ -3,7 +3,7 @@ import time
import json
import logging
import asyncio
from typing import List, Optional, Any, Dict, Union, Literal
from typing import List, Optional, Any, Dict, Union, Literal, TYPE_CHECKING
from rich.console import Console
from rich.live import Live
from openai import AsyncOpenAI
@@ -19,6 +19,9 @@ from ..main import (
error_logs
)
if TYPE_CHECKING:
from ..task.task import Task
class Agent:
def _generate_tool_definition(self, function_name):
"""
@@ -132,11 +135,12 @@ class Agent:
def __init__(
self,
name: str,
role: str,
goal: str,
backstory: str,
llm: Optional[Union[str, Any]] = "gpt-4o",
name: Optional[str] = None,
role: Optional[str] = None,
goal: Optional[str] = None,
backstory: Optional[str] = None,
instructions: Optional[str] = None,
llm: Optional[Union[str, Any]] = None,
tools: Optional[List[Any]] = None,
function_calling_llm: Optional[Any] = None,
max_iter: int = 20,
@@ -158,16 +162,35 @@ class Agent:
knowledge_sources: Optional[List[Any]] = None,
use_system_prompt: Optional[bool] = True,
markdown: bool = True,
self_reflect: bool = True,
self_reflect: Optional[bool] = None,
max_reflect: int = 3,
min_reflect: int = 1,
reflect_llm: Optional[str] = None
):
self.name = name
self.role = role
self.goal = goal
self.backstory = backstory
self.llm = llm
# Handle backward compatibility for required fields
if all(x is None for x in [name, role, goal, backstory, instructions]):
raise ValueError("At least one of name, role, goal, backstory, or instructions must be provided")
# If instructions are provided, use them to set role, goal, and backstory
if instructions:
self.name = name or "Agent"
self.role = role or "Assistant"
self.goal = goal or instructions
self.backstory = backstory or instructions
# Set self_reflect to False by default for instruction-based agents
self.self_reflect = False if self_reflect is None else self_reflect
else:
# Use provided values or defaults
self.name = name or "Agent"
self.role = role or "Assistant"
self.goal = goal or "Help the user with their tasks"
self.backstory = backstory or "I am an AI assistant"
# Default to True for traditional agents if not specified
self.self_reflect = True if self_reflect is None else self_reflect
self.instructions = instructions
# Check for model name in environment variable if not provided
self.llm = llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
self.tools = tools if tools else [] # Store original tools
self.function_calling_llm = function_calling_llm
self.max_iter = max_iter
@@ -190,10 +213,10 @@ class Agent:
self.use_system_prompt = use_system_prompt
self.chat_history = []
self.markdown = markdown
self.self_reflect = self_reflect
self.max_reflect = max_reflect
self.min_reflect = min_reflect
self.reflect_llm = reflect_llm
# Use the same model selection logic for reflect_llm
self.reflect_llm = reflect_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
self.console = Console() # Create a single console instance for the agent
# Initialize system prompt
@@ -202,6 +225,21 @@ Your Role: {self.role}\n
Your Goal: {self.goal}
"""
def generate_task(self) -> 'Task':
"""Generate a Task object from the agent's instructions"""
from ..task.task import Task
description = self.instructions if self.instructions else f"Execute task as {self.role} with goal: {self.goal}"
expected_output = "Complete the assigned task successfully"
return Task(
name=self.name,
description=description,
expected_output=expected_output,
agent=self,
tools=self.tools
)
def execute_tool(self, function_name, arguments):
"""
Execute a tool dynamically based on the function name and arguments.
+43 -11
View File
@@ -41,7 +41,10 @@ def process_video(video_path: str, seconds_per_frame=2):
return base64_frames
class PraisonAIAgents:
def __init__(self, agents, tasks, verbose=0, completion_checker=None, max_retries=5, process="sequential", manager_llm=None):
def __init__(self, agents, tasks=None, verbose=0, completion_checker=None, max_retries=5, process="sequential", manager_llm=None):
if not agents:
raise ValueError("At least one agent must be provided")
self.agents = agents
self.tasks = {}
if max_retries < 3:
@@ -51,12 +54,41 @@ class PraisonAIAgents:
self.verbose = verbose
self.max_retries = max_retries
self.process = process
if not manager_llm:
logging.debug("No manager_llm provided. Using OPENAI_MODEL_NAME environment variable or defaulting to 'gpt-4o'")
self.manager_llm = manager_llm if manager_llm else os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
# Check for manager_llm in environment variable if not provided
self.manager_llm = manager_llm or os.getenv('OPENAI_MODEL_NAME', 'gpt-4o')
if self.verbose:
logging.info(f"Using model {self.manager_llm} for manager")
# If no tasks provided, generate them from agents
if tasks is None:
tasks = []
for agent in self.agents:
task = agent.generate_task()
tasks.append(task)
logging.info(f"Auto-generated {len(tasks)} tasks from agents")
else:
# Validate tasks for backward compatibility
if not tasks:
raise ValueError("If tasks are provided, at least one task must be present")
logging.info(f"Using {len(tasks)} provided tasks")
# Add tasks and set their status
for task in tasks:
self.add_task(task)
task.status = "not started"
# If tasks were auto-generated from agents or process is sequential, set up sequential flow
if len(tasks) > 1 and (process == "sequential" or all(task.next_tasks == [] for task in tasks)):
for i in range(len(tasks) - 1):
# Set up next task relationship
tasks[i].next_tasks = [tasks[i + 1].name]
# Set up context for the next task to include the current task
if tasks[i + 1].context is None:
tasks[i + 1].context = []
tasks[i + 1].context.append(tasks[i])
logging.info("Set up sequential flow with automatic context passing")
self._state = {} # Add state storage at PraisonAIAgents level
def add_task(self, task):
@@ -119,9 +151,9 @@ Expected Output: {task.expected_output}.
else:
context_results += f"Previous task {context_task.name if context_task.name else context_task.description} had no result.\n"
task_prompt += f"""
Here are the results of previous tasks that might be useful:\n
{context_results}
"""
Here are the results of previous tasks that might be useful:\n
{context_results}
"""
task_prompt += "Please provide only the final result of your work. Do not add any conversation or extra explanation."
if self.verbose >= 2:
@@ -320,7 +352,7 @@ Expected Output: {task.expected_output}.
task_prompt = f"""
You need to do the following task: {task.description}.
Expected Output: {task.expected_output}.
"""
"""
if task.context:
context_results = ""
for context_task in task.context:
@@ -329,9 +361,9 @@ Expected Output: {task.expected_output}.
else:
context_results += f"Previous task {context_task.name if context_task.name else context_task.description} had no result.\n"
task_prompt += f"""
Here are the results of previous tasks that might be useful:\n
{context_results}
"""
Here are the results of previous tasks that might be useful:\n
{context_results}
"""
task_prompt += "Please provide only the final result of your work. Do not add any conversation or extra explanation."
if self.verbose >= 2:
+4
View File
@@ -0,0 +1,4 @@
"""Tools package for PraisonAI Agents"""
from .tools import Tools
__all__ = ['Tools']
+40
View File
@@ -0,0 +1,40 @@
"""Tools module for PraisonAI Agents"""
from typing import List, Dict
import logging
import importlib
class Tools:
@staticmethod
def internet_search(query: str) -> List[Dict]:
"""
Perform a search using DuckDuckGo.
Args:
query (str): The search query.
Returns:
list: A list of search result titles, URLs, and snippets.
"""
# Check if duckduckgo_search is installed
if importlib.util.find_spec("duckduckgo_search") is None:
error_msg = "DuckDuckGo search is not available. Please install duckduckgo_search package using: pip install duckduckgo_search"
logging.error(error_msg)
return [{"error": error_msg}]
try:
# Import only when needed
from duckduckgo_search import DDGS
results = []
ddgs = DDGS()
for result in ddgs.text(keywords=query, max_results=5):
results.append({
"title": result.get("title", ""),
"url": result.get("href", ""),
"snippet": result.get("body", "")
})
return results
except Exception as e:
error_msg = f"Error during DuckDuckGo search: {e}"
logging.error(error_msg)
return [{"error": error_msg}]
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "praisonaiagents"
version = "0.0.20"
version = "0.0.22"
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
authors = [
{ name="Mervin Praison" }
+1 -1
View File
@@ -186,7 +186,7 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.20"
version = "0.0.22"
source = { editable = "." }
dependencies = [
{ name = "openai" },
+1 -1
View File
@@ -110,7 +110,7 @@ Loads environment variables from .env file or system and sets them.</p>
file.write(&#34;FROM python:3.11-slim\n&#34;)
file.write(&#34;WORKDIR /app\n&#34;)
file.write(&#34;COPY . .\n&#34;)
file.write(&#34;RUN pip install flask praisonai==2.0.26 gunicorn markdown\n&#34;)
file.write(&#34;RUN pip install flask praisonai==2.0.30 gunicorn markdown\n&#34;)
file.write(&#34;EXPOSE 8080\n&#34;)
file.write(&#39;CMD [&#34;gunicorn&#34;, &#34;-b&#34;, &#34;0.0.0.0:8080&#34;, &#34;api:app&#34;]\n&#39;)
+3 -2
View File
@@ -1,12 +1,13 @@
---
title: "Callbacks"
title: "AI Agents with Callbacks"
sidebarTitle: "Callbacks"
description: "Learn how to implement callbacks to monitor and log AI agent interactions, errors, and task completions."
icon: "bell"
---
<Note>
**Prerequisites**
- Python 3.8 or higher
- Python 3.10 or higher
- PraisonAI Agents package installed
- Basic understanding of Python functions
</Note>
+248
View File
@@ -0,0 +1,248 @@
---
title: "Mini AI Agents"
description: "Learn how to create simple yet powerful AI agents in just a few lines of code."
icon: "robot"
---
<Note>
**Prerequisites**
- Python 3.10 or higher
- PraisonAI Agents package installed
- Basic understanding of Python
</Note>
## Quick Start
Create multiple AI agents that can work together in just a few lines of code!
<Steps>
<Step title="Install Package">
First, install the PraisonAI Agents package:
```bash
pip install praisonaiagents
```
</Step>
<Step title="Import Required Components">
Import the necessary components:
```python
from praisonaiagents import Agent, Agents
```
</Step>
<Step title="Create Your Agents">
Create specialized agents with specific roles:
```python
summarise_agent = Agent(instructions="Summarise what is Photosynthesis")
```
</Step>
<Step title="Start Your Agents">
Initialize and run your agents:
```python
agents = Agents(agents=[summarise_agent])
agents.start()
```
</Step>
</Steps>
## Understanding Mini AI Agents
<Card title="What are Mini AI Agents?" icon="question">
Mini AI Agents are simplified yet powerful AI agents that can:
- Work together to accomplish tasks
- Use tools like internet search
- Process and summarize information
- Execute tasks sequentially
</Card>
## Key Components
<CardGroup cols={2}>
<Card title="Agent" icon="user-robot">
Individual AI agents with specific roles and capabilities
```python
Agent(instructions="Your agent's role description")
```
</Card>
<Card title="Tools" icon="wrench">
Built-in tools that agents can use
```python
tools=[Tools.internet_search]
```
</Card>
<Card title="Agents Manager" icon="users">
Coordinates multiple agents
```python
Agents(agents=[agent1, agent2])
```
</Card>
<Card title="Sequential Flow" icon="arrow-right">
Agents work in sequence, passing results to each other
</Card>
</CardGroup>
## Available Tools
<CardGroup cols={2}>
<Card title="Internet Search" icon="search">
```python
Tools.internet_search
```
Search the internet using DuckDuckGo
<Note>
Requires: `pip install duckduckgo_search`
</Note>
</Card>
</CardGroup>
## Examples
### Basic Research and Summary
```terminal
pip install praisonaiagents duckduckgo_search
```
```python
from praisonaiagents import Agent, Agents, Tools
# Create a research agent
research_agent = Agent(
instructions="You are a research agent to search internet about AI 2024",
tools=[Tools.internet_search]
)
# Create a summarize agent
summarise_agent = Agent(
instructions="You are a summarize agent to summarise research agent's findings"
)
# Initialize and run agents
agents = Agents(agents=[research_agent, summarise_agent])
agents.start()
```
### Custom Instructions
<CodeGroup>
```python Basic
# Simple instructions
research_agent = Agent(
instructions="Research about climate change"
)
```
```python Advanced
# Detailed instructions
research_agent = Agent(
instructions="""
You are a research agent focused on gathering information about:
1. Latest AI developments in 2024
2. Major breakthroughs in machine learning
3. New AI applications in industry
Provide detailed and accurate information from reliable sources.
"""
)
```
</CodeGroup>
## Best Practices
<AccordionGroup>
<Accordion title="Agent Instructions">
Write clear and specific instructions:
```python
# Good
Agent(instructions="Research and analyze the latest AI developments in 2024")
# Too vague
Agent(instructions="Research AI")
```
</Accordion>
<Accordion title="Tool Usage">
Provide tools only to agents that need them:
```python
# Research agent needs search
research_agent = Agent(
instructions="Research AI",
tools=[Tools.internet_search]
)
# Summary agent doesn't need search
summary_agent = Agent(
instructions="Summarize findings"
)
```
</Accordion>
</AccordionGroup>
## Common Patterns
### Research and Analysis
```python
# Research agent
researcher = Agent(
instructions="Research latest developments in quantum computing",
tools=[Tools.internet_search]
)
# Analysis agent
analyst = Agent(
instructions="Analyze and explain the research findings in simple terms"
)
agents = Agents(agents=[researcher, analyst])
```
### Information Processing
```python
# Data collector
collector = Agent(
instructions="Collect information about renewable energy",
tools=[Tools.internet_search]
)
# Summarizer
summarizer = Agent(
instructions="Create a concise summary of the collected information"
)
# Report writer
writer = Agent(
instructions="Write a detailed report based on the summary"
)
agents = Agents(agents=[collector, summarizer, writer])
```
## Troubleshooting
<CardGroup cols={2}>
<Card title="Tool Not Available" icon="triangle-exclamation">
If using `Tools.internet_search`, install required package:
```bash
pip install duckduckgo_search
```
</Card>
<Card title="Agent Communication" icon="comments">
Ensure agent instructions are clear and complementary
</Card>
</CardGroup>
## Next Steps
<CardGroup cols={2}>
<Card title="Advanced Agents" icon="graduation-cap" href="./advanced">
Learn about advanced agent configurations
</Card>
<Card title="Custom Tools" icon="screwdriver-wrench" href="./tools">
Create your own agent tools
</Card>
</CardGroup>
<Note>
Mini AI Agents are designed to be simple yet powerful. They're perfect for quick prototypes and straightforward automation tasks.
</Note>
+1
View File
@@ -103,6 +103,7 @@
"features/reasoning",
"framework/multimodalagents",
"features/async",
"features/mini",
"features/callbacks",
"train",
"framework/praisonaiagents",
+1 -1
View File
@@ -3,7 +3,7 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/2.0.26.tar.gz"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/2.0.30.tar.gz"
sha256 "1828fb9227d10f991522c3f24f061943a254b667196b40b1a3e4a54a8d30ce32" # Replace with actual SHA256 checksum
license "MIT"
+1 -1
View File
@@ -56,7 +56,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.0.26 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.0.30 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+110 -63
View File
@@ -5,6 +5,8 @@ import yaml
import logging
import inspect
import chainlit as cl
from praisonaiagents import Agent, Task, PraisonAIAgents, register_display_callback
framework = "praisonai"
config_list = [
{
@@ -14,12 +16,19 @@ config_list = [
}
]
agent_file = "test.yaml"
actions=[
actions = [
cl.Action(name="run", value="run", label="✅ Run"),
cl.Action(name="modify", value="modify", label="🔧 Modify"),
]
@cl.action_callback("run")
async def on_run(action):
await main(cl.Message(content=""))
@cl.action_callback("modify")
async def on_modify(action):
await cl.Message(content="Modify the agents and tools from below settings").send()
import os
import sys
import yaml
@@ -37,9 +46,6 @@ import chainlit as cl
from chainlit.types import ThreadDict
import chainlit.data as cl_data
# External imports from your local packages (adjust if needed)
from praisonaiagents import Agent, Task, PraisonAIAgents
# -----------------------------------------------------------------------------
# Global Setup
# -----------------------------------------------------------------------------
@@ -53,14 +59,12 @@ message_queue = Queue() # Queue to handle messages sent to Chainlit UI
agent_file = "agents.yaml"
# -----------------------------------------------------------------------------
# Database and Settings Logic (untouched logic with minimal additions for Chainlit)
# Database and Settings Logic
# -----------------------------------------------------------------------------
MAX_RETRIES = 3
RETRY_DELAY = 1 # seconds
# Original DatabaseManager from chainlit_ui.py, unmodified logic, plus minimal
# placeholder methods for Chainlit's data layer calls (create_user, get_user, etc.)
from db import DatabaseManager
async def init_database_with_retry():
@@ -118,7 +122,7 @@ async def update_thread_metadata(thread_id: str, metadata: dict):
raise
# -----------------------------------------------------------------------------
# Callback Manager (untouched from colab_combined.py)
# Callback Manager
# -----------------------------------------------------------------------------
class CallbackManager:
@@ -161,7 +165,50 @@ def callback(name: str, is_async: bool = False):
return decorator
# -----------------------------------------------------------------------------
# Tools Loader (untouched logic but returning dict for tools)
# ADDITIONAL CALLBACKS
# -----------------------------------------------------------------------------
def interaction_callback(message=None, response=None, **kwargs):
logger.debug(f"[CALLBACK: interaction] Message: {message} | Response: {response}")
message_queue.put({
"content": f"[CALLBACK: interaction] Message: {message} | Response: {response}",
"author": "Callback"
})
def error_callback(message=None, **kwargs):
logger.error(f"[CALLBACK: error] Message: {message}")
message_queue.put({
"content": f"[CALLBACK: error] Message: {message}",
"author": "Callback"
})
def tool_call_callback(message=None, **kwargs):
logger.debug(f"[CALLBACK: tool_call] Tool used: {message}")
message_queue.put({
"content": f"[CALLBACK: tool_call] Tool used: {message}",
"author": "Callback"
})
def instruction_callback(message=None, **kwargs):
logger.debug(f"[CALLBACK: instruction] Instruction: {message}")
message_queue.put({
"content": f"[CALLBACK: instruction] Instruction: {message}",
"author": "Callback"
})
def self_reflection_callback(message=None, **kwargs):
logger.debug(f"[CALLBACK: self_reflection] Reflection: {message}")
message_queue.put({
"content": f"[CALLBACK: self_reflection] Reflection: {message}",
"author": "Callback"
})
register_display_callback('error', error_callback)
register_display_callback('tool_call', tool_call_callback)
register_display_callback('instruction', instruction_callback)
register_display_callback('self_reflection', self_reflection_callback)
# -----------------------------------------------------------------------------
# Tools Loader
# -----------------------------------------------------------------------------
def load_tools_from_tools_py():
@@ -172,7 +219,6 @@ def load_tools_from_tools_py():
tools_dict = {}
try:
spec = importlib.util.spec_from_file_location("tools", "tools.py")
logger.info(f"Spec: {spec}")
if spec is None:
logger.info("tools.py not found in current directory")
return tools_dict
@@ -182,7 +228,10 @@ def load_tools_from_tools_py():
for name, obj in inspect.getmembers(module):
if not name.startswith('_') and callable(obj) and not inspect.isclass(obj):
# Store the function in globals
globals()[name] = obj
# Build the function definition
tool_def = {
"type": "function",
"function": {
@@ -190,16 +239,14 @@ def load_tools_from_tools_py():
"description": obj.__doc__ or f"Function to {name.replace('_', ' ')}",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to look up information about"
}
},
"required": ["query"]
"properties": {},
"required": []
}
}
},
# Keep the actual callable as well
"callable": obj,
}
tools_dict[name] = tool_def
logger.info(f"Loaded and globalized tool function: {name}")
@@ -209,7 +256,7 @@ def load_tools_from_tools_py():
return tools_dict
# -----------------------------------------------------------------------------
# Async Queue Processor (untouched logic)
# Async Queue Processor
# -----------------------------------------------------------------------------
async def process_message_queue():
@@ -223,7 +270,7 @@ async def process_message_queue():
logger.error(f"Error processing message queue: {e}")
# -----------------------------------------------------------------------------
# Step & Task Callbacks (Async & Sync) (untouched logic)
# Step & Task Callbacks
# -----------------------------------------------------------------------------
async def step_callback(step_details):
@@ -351,9 +398,8 @@ def sync_step_callback_wrapper(step_details):
logger.error(f"Error in sync_step_callback_wrapper: {e}", exc_info=True)
# -----------------------------------------------------------------------------
# Main PraisonAI Runner (untouched logic)
# Main PraisonAI Runner
# -----------------------------------------------------------------------------
async def ui_run_praisonai(config, topic, tools_dict):
logger.info("Starting ui_run_praisonai")
agents_map = {}
@@ -370,8 +416,6 @@ async def ui_run_praisonai(config, topic, tools_dict):
goal_filled = details['goal'].format(topic=topic)
backstory_filled = details['backstory'].format(topic=topic)
await cl.Message(content=f"[DEBUG] Creating agent: {role_name}", author="System").send()
def step_callback_sync(step_details):
step_details["agent_name"] = role_name
try:
@@ -394,7 +438,8 @@ async def ui_run_praisonai(config, topic, tools_dict):
max_rpm=details.get('max_rpm'),
max_execution_time=details.get('max_execution_time'),
cache=details.get('cache', True),
step_callback=step_callback_sync
step_callback=step_callback_sync,
self_reflect=details.get('self_reflect', False)
)
agents_map[role] = agent
@@ -402,17 +447,35 @@ async def ui_run_praisonai(config, topic, tools_dict):
for role, details in config['roles'].items():
agent = agents_map[role]
role_name = agent.name
# -------------------------------------------------------------
# FIX: Skip empty or invalid tool names to avoid null tool objects
# -------------------------------------------------------------
role_tools = []
task_tools = [] # Initialize task_tools outside the loop
for tool_name in details.get('tools', []):
if not tool_name or not tool_name.strip():
logger.warning("Skipping empty tool name.")
continue
if tool_name in tools_dict:
role_tools.append(tools_dict[tool_name])
# Create a copy of the tool definition
tool_def = tools_dict[tool_name].copy()
# Store the callable separately and remove from definition
callable_func = tool_def.pop("callable")
# Add callable to role_tools for task execution
role_tools.append(callable_func)
# Add API tool definition to task's tools
task_tools.append(tool_def)
# Also set the agent's tools to include both
agent.tools = role_tools
else:
logger.warning(f"Tool '{tool_name}' not found. Skipping.")
for tname, tdetails in details.get('tasks', {}).items():
description_filled = tdetails['description'].format(topic=topic)
expected_output_filled = tdetails['expected_output'].format(topic=topic)
await cl.Message(content=f"[DEBUG] Created task: {tname} for {role_name}", author="System").send()
def task_callback_sync(task_output):
try:
loop_ = asyncio.new_event_loop()
@@ -426,7 +489,7 @@ async def ui_run_praisonai(config, topic, tools_dict):
description=description_filled,
expected_output=expected_output_filled,
agent=agent,
tools=role_tools,
tools=task_tools, # Pass API tool definitions
async_execution=True,
context=[],
config=tdetails.get('config', {}),
@@ -454,6 +517,7 @@ async def ui_run_praisonai(config, topic, tools_dict):
await cl.Message(content="Starting PraisonAI agents execution...", author="System").send()
# Decide how to process tasks
if config.get('process') == 'hierarchical':
prai_agents = PraisonAIAgents(
agents=list(agents_map.values()),
@@ -493,10 +557,11 @@ async def ui_run_praisonai(config, topic, tools_dict):
raise
# -----------------------------------------------------------------------------
# Chainlit Handlers + logic from chainlit_ui.py (no changes in logic)
# Chainlit Handlers + logic
# -----------------------------------------------------------------------------
tools_dict = load_tools_from_tools_py()
print(f"[DEBUG] tools_dict: {tools_dict}")
# Load agent config (default) from 'agents.yaml'
with open(agent_file, 'r') as f:
@@ -520,9 +585,6 @@ if AUTH_PASSWORD_ENABLED:
@cl.set_chat_profiles
async def set_profiles(current_user: cl.User):
"""
Keep all the same starter logic from chainlit_ui.py.
"""
return [
cl.ChatProfile(
name="Auto",
@@ -577,7 +639,6 @@ async def set_profiles(current_user: cl.User):
@cl.on_chat_start
async def start_chat():
try:
# Load model name from database
model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-4o-mini")
cl.user_session.set("model_name", model_name)
logger.debug(f"Model name: {model_name}")
@@ -587,16 +648,19 @@ async def start_chat():
[{"role": "system", "content": "You are a helpful assistant."}],
)
# Create tools.py if it doesn't exist
if not os.path.exists("tools.py"):
with open("tools.py", "w") as f:
f.write("# Add your custom tools here\n")
if not os.path.exists("agents.yaml"):
with open("agents.yaml", "w") as f:
f.write("# Add your custom agents here\n")
settings = await cl.ChatSettings(
[
TextInput(id="Model", label="OpenAI - Model", initial=model_name),
TextInput(id="BaseUrl", label="OpenAI - Base URL", initial=config_list[0]['base_url']),
TextInput(id="ApiKey", label="OpenAI - API Key", initial=config_list[0]['api_key']),
TextInput(id="ApiKey", label="OpenAI - API Key", initial=config_list[0]['api_key']),
Select(
id="Framework",
label="Framework",
@@ -608,7 +672,7 @@ async def start_chat():
cl.user_session.set("settings", settings)
chat_profile = cl.user_session.get("chat_profile")
if chat_profile=="Manual":
if chat_profile == "Manual":
agent_file = "agents.yaml"
full_agent_file_path = os.path.abspath(agent_file)
if os.path.exists(full_agent_file_path):
@@ -628,7 +692,7 @@ async def start_chat():
[
TextInput(id="Model", label="OpenAI - Model", initial=model_name),
TextInput(id="BaseUrl", label="OpenAI - Base URL", initial=config_list[0]['base_url']),
TextInput(id="ApiKey", label="OpenAI - API Key", initial=config_list[0]['api_key']),
TextInput(id="ApiKey", label="OpenAI - API Key", initial=config_list[0]['api_key']),
Select(
id="Framework",
label="Framework",
@@ -655,13 +719,8 @@ async def start_chat():
logger.error(f"Error in start_chat: {str(e)}")
await cl.Message(content=f"An error occurred while starting the chat: {str(e)}").send()
@cl.on_chat_resume
async def on_chat_resume(thread: ThreadDict):
"""
Logic from chainlit_ui.py for chat resume:
- Restore message history from thread steps
"""
try:
message_history = cl.user_session.get("message_history", [])
root_messages = [m for m in thread["steps"] if m["parentId"] is None]
@@ -676,27 +735,19 @@ async def on_chat_resume(thread: ThreadDict):
@cl.on_message
async def main(message: cl.Message):
"""
Merged logic from colab_combined.py and chainlit_ui.py for main message:
- Use existing ui_run_praisonai to process user message with PraisonAI
- Keep message history, no changes to logic
"""
try:
logger.info(f"User message: {message.content}")
await cl.Message(
content=f"🔄 Processing your request: {message.content}...",
author="System"
).send()
msg = cl.Message(content="")
await msg.stream_token(f"🔄 Processing your request: {message.content}...")
# Run PraisonAI
result = await ui_run_praisonai(config, message.content, tools_dict)
# Update message history
message_history = cl.user_session.get("message_history", [])
message_history.append({"role": "user", "content": message.content})
message_history.append({"role": "assistant", "content": str(result)})
cl.user_session.set("message_history", message_history)
await msg.send()
except Exception as e:
error_msg = f"Error running PraisonAI agents: {str(e)}"
logger.error(error_msg, exc_info=True)
@@ -704,14 +755,12 @@ async def main(message: cl.Message):
@cl.on_settings_update
async def on_settings_update(settings):
"""Handle updates to the ChatSettings form."""
try:
global config_list, framework
config_list[0]['model'] = settings["Model"]
config_list[0]['base_url'] = settings["BaseUrl"]
config_list[0]['api_key'] = settings["ApiKey"]
# Save settings to database with retry
for attempt in range(MAX_RETRIES):
try:
await save_setting_with_retry("model_name", config_list[0]['model'])
@@ -724,7 +773,6 @@ async def on_settings_update(settings):
continue
raise
# Save to environment variables for compatibility
os.environ["OPENAI_API_KEY"] = config_list[0]['api_key']
os.environ["OPENAI_MODEL_NAME"] = config_list[0]['model']
os.environ["OPENAI_API_BASE"] = config_list[0]['base_url']
@@ -739,7 +787,6 @@ async def on_settings_update(settings):
with open("tools.py", "w") as f:
f.write(settings["tools"])
# Update thread metadata if exists with retry
thread_id = cl.user_session.get("thread_id")
if thread_id:
for attempt in range(MAX_RETRIES):
@@ -749,6 +796,7 @@ async def on_settings_update(settings):
metadata = thread.get("metadata", {})
if isinstance(metadata, str):
try:
import json
metadata = json.loads(metadata)
except json.JSONDecodeError:
metadata = {}
@@ -766,10 +814,9 @@ async def on_settings_update(settings):
except Exception as e:
logger.error(f"Error updating settings: {str(e)}")
await cl.Message(content=f"An error occurred while updating settings: {str(e)}. Retrying...").send()
# One final retry after a longer delay
try:
await asyncio.sleep(RETRY_DELAY * 2)
await on_settings_update(settings)
except Exception as e:
logger.error(f"Final retry failed: {str(e)}")
await cl.Message(content=f"Failed to update settings after retries: {str(e)}").send()
await cl.Message(content=f"Failed to update settings after retries: {str(e)}").send()
-775
View File
@@ -1,775 +0,0 @@
from chainlit.input_widget import Select, TextInput
import os
import sys
import yaml
import logging
import inspect
import chainlit as cl
framework = "praisonai"
config_list = [
{
'model': os.environ.get("OPENAI_MODEL_NAME", "gpt-4o"),
'base_url': os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1"),
'api_key': os.environ.get("OPENAI_API_KEY", "")
}
]
agent_file = "test.yaml"
actions=[
cl.Action(name="run", value="run", label="✅ Run"),
cl.Action(name="modify", value="modify", label="🔧 Modify"),
]
import os
import sys
import yaml
import logging
import inspect
import asyncio
import importlib.util
import sqlite3
from queue import Queue
from datetime import datetime
from dotenv import load_dotenv
# Chainlit imports
import chainlit as cl
from chainlit.types import ThreadDict
import chainlit.data as cl_data
# External imports from your local packages (adjust if needed)
from praisonaiagents import Agent, Task, PraisonAIAgents
# -----------------------------------------------------------------------------
# Global Setup
# -----------------------------------------------------------------------------
load_dotenv()
log_level = os.getenv("LOGLEVEL", "INFO").upper()
logger = logging.getLogger(__name__)
logger.setLevel(log_level)
message_queue = Queue() # Queue to handle messages sent to Chainlit UI
agent_file = "agents.yaml"
# -----------------------------------------------------------------------------
# Database and Settings Logic (untouched logic with minimal additions for Chainlit)
# -----------------------------------------------------------------------------
MAX_RETRIES = 3
RETRY_DELAY = 1 # seconds
# Original DatabaseManager from chainlit_ui.py, unmodified logic, plus minimal
# placeholder methods for Chainlit's data layer calls (create_user, get_user, etc.)
from db import DatabaseManager
async def init_database_with_retry():
db = DatabaseManager()
for attempt in range(MAX_RETRIES):
try:
db.initialize()
return db
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < MAX_RETRIES - 1:
await asyncio.sleep(RETRY_DELAY)
continue
raise
db_manager = asyncio.run(init_database_with_retry())
cl_data._data_layer = db_manager
async def save_setting_with_retry(key: str, value: str):
for attempt in range(MAX_RETRIES):
try:
await db_manager.save_setting(key, value)
return
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < MAX_RETRIES - 1:
await asyncio.sleep(RETRY_DELAY)
continue
raise
async def load_setting_with_retry(key: str) -> str:
for attempt in range(MAX_RETRIES):
try:
return await db_manager.load_setting(key)
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < MAX_RETRIES - 1:
await asyncio.sleep(RETRY_DELAY)
continue
raise
return ""
def save_setting(key: str, value: str):
asyncio.run(save_setting_with_retry(key, value))
def load_setting(key: str) -> str:
return asyncio.run(load_setting_with_retry(key))
async def update_thread_metadata(thread_id: str, metadata: dict):
for attempt in range(MAX_RETRIES):
try:
await cl_data.update_thread(thread_id, metadata=metadata)
return
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < MAX_RETRIES - 1:
await asyncio.sleep(RETRY_DELAY)
continue
raise
# -----------------------------------------------------------------------------
# Callback Manager (untouched from colab_combined.py)
# -----------------------------------------------------------------------------
class CallbackManager:
def __init__(self):
self._callbacks = {}
def register(self, name: str, callback, is_async: bool = False) -> None:
self._callbacks[name] = {'func': callback, 'is_async': is_async}
async def call(self, name: str, **kwargs) -> None:
if name not in self._callbacks:
logger.warning(f"No callback registered for {name}")
return
callback_info = self._callbacks[name]
func = callback_info['func']
is_async = callback_info['is_async']
try:
if is_async:
await func(**kwargs)
else:
if asyncio.iscoroutinefunction(func):
await func(**kwargs)
else:
await asyncio.get_event_loop().run_in_executor(None, lambda: func(**kwargs))
except Exception as e:
logger.error(f"Error in callback {name}: {str(e)}")
callback_manager = CallbackManager()
def register_callback(name: str, callback, is_async: bool = False) -> None:
callback_manager.register(name, callback, is_async)
async def trigger_callback(name: str, **kwargs) -> None:
await callback_manager.call(name, **kwargs)
def callback(name: str, is_async: bool = False):
def decorator(func):
register_callback(name, func, is_async)
return func
return decorator
# -----------------------------------------------------------------------------
# Tools Loader (untouched logic but returning dict for tools)
# -----------------------------------------------------------------------------
def load_tools_from_tools_py():
"""
Imports and returns all contents from tools.py file.
Also adds the tools to the global namespace.
"""
tools_dict = {}
try:
spec = importlib.util.spec_from_file_location("tools", "tools.py")
logger.info(f"Spec: {spec}")
if spec is None:
logger.info("tools.py not found in current directory")
return tools_dict
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
for name, obj in inspect.getmembers(module):
if not name.startswith('_') and callable(obj) and not inspect.isclass(obj):
globals()[name] = obj
tool_def = {
"type": "function",
"function": {
"name": name,
"description": obj.__doc__ or f"Function to {name.replace('_', ' ')}",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query to look up information about"
}
},
"required": ["query"]
}
}
}
tools_dict[name] = tool_def
logger.info(f"Loaded and globalized tool function: {name}")
logger.info(f"Loaded {len(tools_dict)} tool functions from tools.py")
except Exception as e:
logger.warning(f"Error loading tools from tools.py: {e}")
return tools_dict
# -----------------------------------------------------------------------------
# Async Queue Processor (untouched logic)
# -----------------------------------------------------------------------------
async def process_message_queue():
while True:
try:
if not message_queue.empty():
msg_data = message_queue.get()
await cl.Message(**msg_data).send()
await asyncio.sleep(0.1)
except Exception as e:
logger.error(f"Error processing message queue: {e}")
# -----------------------------------------------------------------------------
# Step & Task Callbacks (Async & Sync) (untouched logic)
# -----------------------------------------------------------------------------
async def step_callback(step_details):
logger.info(f"[CALLBACK DEBUG] step_callback: {step_details}")
agent_name = step_details.get("agent_name", "Agent")
try:
if step_details.get("response"):
message_queue.put({
"content": f"Agent Response: {step_details['response']}",
"author": agent_name
})
if step_details.get("tool_name"):
message_queue.put({
"content": f"🛠️ Using tool: {step_details['tool_name']}",
"author": "System"
})
except Exception as e:
logger.error(f"Error in step_callback: {e}", exc_info=True)
async def task_callback(task_output):
logger.info(f"[CALLBACK DEBUG] task_callback: type={type(task_output)}")
try:
if hasattr(task_output, 'raw'):
content = task_output.raw
elif hasattr(task_output, 'content'):
content = task_output.content
else:
content = str(task_output)
message_queue.put({
"content": f"Task Output: {content}",
"author": "Task"
})
except Exception as e:
logger.error(f"Error in task_callback: {e}", exc_info=True)
async def step_callback_wrapper(step_details):
logger.info(f"[CALLBACK DEBUG] step_callback_wrapper: {step_details}")
agent_name = step_details.get("agent_name", "Agent")
try:
if not cl.context.context_var.get():
logger.warning("[CALLBACK DEBUG] No Chainlit context in wrapper.")
return
if step_details.get("response"):
await cl.Message(
content=f"{agent_name}: {step_details['response']}",
author=agent_name,
).send()
if step_details.get("tool_name"):
await cl.Message(
content=f"🛠️ {agent_name} is using tool: {step_details['tool_name']}",
author="System",
).send()
if step_details.get("thought"):
await cl.Message(
content=f"💭 {agent_name}'s thought: {step_details['thought']}",
author=agent_name,
).send()
except Exception as e:
logger.error(f"Error in step_callback_wrapper: {e}", exc_info=True)
try:
await cl.Message(content=f"Error in step callback: {e}", author="System").send()
except Exception as send_error:
logger.error(f"Error sending error message: {send_error}")
async def task_callback_wrapper(task_output):
logger.info("[CALLBACK DEBUG] task_callback_wrapper triggered")
try:
if not cl.context.context_var.get():
logger.warning("[CALLBACK DEBUG] No Chainlit context in task wrapper.")
return
if hasattr(task_output, 'raw'):
content = task_output.raw
elif hasattr(task_output, 'content'):
content = task_output.content
else:
content = str(task_output)
await cl.Message(
content=f"✅ Agent completed task:\n{content}",
author="Agent",
).send()
if hasattr(task_output, 'details'):
await cl.Message(
content=f"📝 Additional details:\n{task_output.details}",
author="Agent",
).send()
except Exception as e:
logger.error(f"Error in task_callback_wrapper: {e}", exc_info=True)
try:
await cl.Message(content=f"Error in task callback: {e}", author="System").send()
except Exception as send_error:
logger.error(f"Error sending error message: {send_error}")
def sync_task_callback_wrapper(task_output):
logger.info("[CALLBACK DEBUG] sync_task_callback_wrapper")
try:
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if loop.is_running():
asyncio.run_coroutine_threadsafe(task_callback_wrapper(task_output), loop)
else:
loop.run_until_complete(task_callback_wrapper(task_output))
except Exception as e:
logger.error(f"Error in sync_task_callback_wrapper: {e}", exc_info=True)
def sync_step_callback_wrapper(step_details):
logger.info("[CALLBACK DEBUG] sync_step_callback_wrapper")
try:
try:
loop = asyncio.get_event_loop()
except RuntimeError:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
if loop.is_running():
asyncio.run_coroutine_threadsafe(step_callback_wrapper(step_details), loop)
else:
loop.run_until_complete(step_callback_wrapper(step_details))
except Exception as e:
logger.error(f"Error in sync_step_callback_wrapper: {e}", exc_info=True)
# -----------------------------------------------------------------------------
# Main PraisonAI Runner (untouched logic)
# -----------------------------------------------------------------------------
async def ui_run_praisonai(config, topic, tools_dict):
logger.info("Starting ui_run_praisonai")
agents_map = {}
tasks = []
tasks_dict = {}
try:
queue_processor = asyncio.create_task(process_message_queue())
# Create agents
for role, details in config['roles'].items():
role_name = details.get('name', role).format(topic=topic)
role_filled = details.get('role', role).format(topic=topic)
goal_filled = details['goal'].format(topic=topic)
backstory_filled = details['backstory'].format(topic=topic)
await cl.Message(content=f"[DEBUG] Creating agent: {role_name}", author="System").send()
def step_callback_sync(step_details):
step_details["agent_name"] = role_name
try:
loop_ = asyncio.new_event_loop()
asyncio.set_event_loop(loop_)
loop_.run_until_complete(step_callback(step_details))
loop_.close()
except Exception as e:
logger.error(f"Error in step_callback_sync: {e}", exc_info=True)
agent = Agent(
name=role_name,
role=role_filled,
goal=goal_filled,
backstory=backstory_filled,
llm=details.get('llm', 'gpt-4o'),
verbose=True,
allow_delegation=details.get('allow_delegation', False),
max_iter=details.get('max_iter', 15),
max_rpm=details.get('max_rpm'),
max_execution_time=details.get('max_execution_time'),
cache=details.get('cache', True),
step_callback=step_callback_sync
)
agents_map[role] = agent
# Create tasks
for role, details in config['roles'].items():
agent = agents_map[role]
role_name = agent.name
role_tools = []
for tool_name in details.get('tools', []):
if tool_name in tools_dict:
role_tools.append(tools_dict[tool_name])
for tname, tdetails in details.get('tasks', {}).items():
description_filled = tdetails['description'].format(topic=topic)
expected_output_filled = tdetails['expected_output'].format(topic=topic)
await cl.Message(content=f"[DEBUG] Created task: {tname} for {role_name}", author="System").send()
def task_callback_sync(task_output):
try:
loop_ = asyncio.new_event_loop()
asyncio.set_event_loop(loop_)
loop_.run_until_complete(task_callback(task_output))
loop_.close()
except Exception as e:
logger.error(f"Error in task_callback_sync: {e}", exc_info=True)
task = Task(
description=description_filled,
expected_output=expected_output_filled,
agent=agent,
tools=role_tools,
async_execution=True,
context=[],
config=tdetails.get('config', {}),
output_json=tdetails.get('output_json'),
output_pydantic=tdetails.get('output_pydantic'),
output_file=tdetails.get('output_file', ""),
callback=task_callback_sync,
create_directory=tdetails.get('create_directory', False)
)
tasks.append(task)
tasks_dict[tname] = task
# Build context links
for role, details in config['roles'].items():
for tname, tdetails in details.get('tasks', {}).items():
if tname not in tasks_dict:
continue
task = tasks_dict[tname]
context_tasks = [
tasks_dict[ctx]
for ctx in tdetails.get('context', [])
if ctx in tasks_dict
]
task.context = context_tasks
await cl.Message(content="Starting PraisonAI agents execution...", author="System").send()
if config.get('process') == 'hierarchical':
prai_agents = PraisonAIAgents(
agents=list(agents_map.values()),
tasks=tasks,
verbose=True,
process="hierarchical",
manager_llm=config.get('manager_llm', 'gpt-4o')
)
else:
prai_agents = PraisonAIAgents(
agents=list(agents_map.values()),
tasks=tasks,
verbose=2
)
cl.user_session.set("agents", prai_agents)
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(None, prai_agents.start)
if hasattr(response, 'raw'):
result = response.raw
elif hasattr(response, 'content'):
result = response.content
else:
result = str(response)
await cl.Message(content="PraisonAI agents execution completed.", author="System").send()
await asyncio.sleep(1)
queue_processor.cancel()
return result
except Exception as e:
error_msg = f"Error in ui_run_praisonai: {str(e)}"
logger.error(error_msg, exc_info=True)
await cl.Message(content=error_msg, author="System").send()
raise
# -----------------------------------------------------------------------------
# Chainlit Handlers + logic from chainlit_ui.py (no changes in logic)
# -----------------------------------------------------------------------------
tools_dict = load_tools_from_tools_py()
# Load agent config (default) from 'agents.yaml'
with open(agent_file, 'r') as f:
config = yaml.safe_load(f)
AUTH_PASSWORD_ENABLED = os.getenv("AUTH_PASSWORD_ENABLED", "true").lower() == "true"
CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET")
if not CHAINLIT_AUTH_SECRET:
os.environ["CHAINLIT_AUTH_SECRET"] = "p8BPhQChpg@J>jBz$wGxqLX2V>yTVgP*7Ky9H$aV:axW~ANNX-7_T:o@lnyCBu^U"
username_env = os.getenv("CHAINLIT_USERNAME", "admin")
password_env = os.getenv("CHAINLIT_PASSWORD", "admin")
def simple_auth_callback(u: str, p: str):
if (u, p) == (username_env, password_env):
return cl.User(identifier=u, metadata={"role": "ADMIN", "provider": "credentials"})
return None
if AUTH_PASSWORD_ENABLED:
auth_callback = cl.password_auth_callback(simple_auth_callback)
@cl.set_chat_profiles
async def set_profiles(current_user: cl.User):
"""
Keep all the same starter logic from chainlit_ui.py.
"""
return [
cl.ChatProfile(
name="Auto",
markdown_description=(
"Automatically generate agents and tasks based on your input."
),
starters=[
cl.Starter(
label="Create a movie script",
message=(
"Create a movie script about a futuristic society where AI "
"and humans coexist, focusing on the conflict and resolution "
"between them. Start with an intriguing opening scene."
),
icon="/public/movie.svg",
),
cl.Starter(
label="Design a fantasy world",
message=(
"Design a detailed fantasy world with unique geography, "
"cultures, and magical systems. Start by describing the main "
"continent and its inhabitants."
),
icon="/public/fantasy.svg",
),
cl.Starter(
label="Write a futuristic political thriller",
message=(
"Write a futuristic political thriller involving a conspiracy "
"within a global government. Start with a high-stakes meeting "
"that sets the plot in motion."
),
icon="/public/thriller.svg",
),
cl.Starter(
label="Develop a new board game",
message=(
"Develop a new, innovative board game. Describe the game's "
"objective, rules, and unique mechanics. Create a scenario to "
"illustrate gameplay."
),
icon="/public/game.svg",
),
],
),
cl.ChatProfile(
name="Manual",
markdown_description="Manually define your agents and tasks using a YAML file.",
),
]
@cl.on_chat_start
async def start_chat():
try:
# Load model name from database
model_name = load_setting("model_name") or os.getenv("MODEL_NAME", "gpt-4o-mini")
cl.user_session.set("model_name", model_name)
logger.debug(f"Model name: {model_name}")
cl.user_session.set(
"message_history",
[{"role": "system", "content": "You are a helpful assistant."}],
)
# Create tools.py if it doesn't exist
if not os.path.exists("tools.py"):
with open("tools.py", "w") as f:
f.write("# Add your custom tools here\n")
settings = await cl.ChatSettings(
[
TextInput(id="Model", label="OpenAI - Model", initial=model_name),
TextInput(id="BaseUrl", label="OpenAI - Base URL", initial=config_list[0]['base_url']),
TextInput(id="ApiKey", label="OpenAI - API Key", initial=config_list[0]['api_key']),
Select(
id="Framework",
label="Framework",
values=["praisonai", "crewai", "autogen"],
initial_index=0,
),
]
).send()
cl.user_session.set("settings", settings)
chat_profile = cl.user_session.get("chat_profile")
if chat_profile=="Manual":
agent_file = "agents.yaml"
full_agent_file_path = os.path.abspath(agent_file)
if os.path.exists(full_agent_file_path):
with open(full_agent_file_path, 'r') as f:
yaml_content = f.read()
msg = cl.Message(content=yaml_content, language="yaml")
await msg.send()
full_tools_file_path = os.path.abspath("tools.py")
if os.path.exists(full_tools_file_path):
with open(full_tools_file_path, 'r') as f:
tools_content = f.read()
msg = cl.Message(content=tools_content, language="python")
await msg.send()
settings = await cl.ChatSettings(
[
TextInput(id="Model", label="OpenAI - Model", initial=model_name),
TextInput(id="BaseUrl", label="OpenAI - Base URL", initial=config_list[0]['base_url']),
TextInput(id="ApiKey", label="OpenAI - API Key", initial=config_list[0]['api_key']),
Select(
id="Framework",
label="Framework",
values=["praisonai", "crewai", "autogen"],
initial_index=0,
),
TextInput(id="agents", label="agents.yaml", initial=yaml_content, multiline=True),
TextInput(id="tools", label="tools.py", initial=tools_content, multiline=True),
]
).send()
cl.user_session.set("settings", settings)
res = await cl.AskActionMessage(
content="Pick an action!",
actions=actions,
).send()
if res and res.get("value") == "modify":
await cl.Message(content="Modify the agents and tools from below settings", actions=actions).send()
elif res and res.get("value") == "run":
await main(cl.Message(content="", actions=actions))
await on_settings_update(settings)
except Exception as e:
logger.error(f"Error in start_chat: {str(e)}")
await cl.Message(content=f"An error occurred while starting the chat: {str(e)}").send()
@cl.on_chat_resume
async def on_chat_resume(thread: ThreadDict):
"""
Logic from chainlit_ui.py for chat resume:
- Restore message history from thread steps
"""
try:
message_history = cl.user_session.get("message_history", [])
root_messages = [m for m in thread["steps"] if m["parentId"] is None]
for message in root_messages:
if message["type"] == "user_message":
message_history.append({"role": "user", "content": message["output"]})
elif message["type"] == "ai_message":
message_history.append({"role": "assistant", "content": message["content"]})
cl.user_session.set("message_history", message_history)
except Exception as e:
logger.error(f"Error in on_chat_resume: {str(e)}")
@cl.on_message
async def main(message: cl.Message):
"""
Merged logic from colab_combined.py and chainlit_ui.py for main message:
- Use existing ui_run_praisonai to process user message with PraisonAI
- Keep message history, no changes to logic
"""
try:
logger.info(f"User message: {message.content}")
await cl.Message(
content=f"🔄 Processing your request: {message.content}...",
author="System"
).send()
# Run PraisonAI
result = await ui_run_praisonai(config, message.content, tools_dict)
# Update message history
message_history = cl.user_session.get("message_history", [])
message_history.append({"role": "user", "content": message.content})
message_history.append({"role": "assistant", "content": str(result)})
cl.user_session.set("message_history", message_history)
except Exception as e:
error_msg = f"Error running PraisonAI agents: {str(e)}"
logger.error(error_msg, exc_info=True)
await cl.Message(content=error_msg, author="System").send()
@cl.on_settings_update
async def on_settings_update(settings):
"""Handle updates to the ChatSettings form."""
try:
global config_list, framework
config_list[0]['model'] = settings["Model"]
config_list[0]['base_url'] = settings["BaseUrl"]
config_list[0]['api_key'] = settings["ApiKey"]
# Save settings to database with retry
for attempt in range(MAX_RETRIES):
try:
await save_setting_with_retry("model_name", config_list[0]['model'])
await save_setting_with_retry("base_url", config_list[0]['base_url'])
await save_setting_with_retry("api_key", config_list[0]['api_key'])
break
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < MAX_RETRIES - 1:
await asyncio.sleep(RETRY_DELAY)
continue
raise
# Save to environment variables for compatibility
os.environ["OPENAI_API_KEY"] = config_list[0]['api_key']
os.environ["OPENAI_MODEL_NAME"] = config_list[0]['model']
os.environ["OPENAI_API_BASE"] = config_list[0]['base_url']
os.environ["MODEL_NAME"] = config_list[0]['model']
framework = settings["Framework"]
os.environ["FRAMEWORK"] = framework
if "agents" in settings:
with open("agents.yaml", "w") as f:
f.write(settings["agents"])
if "tools" in settings:
with open("tools.py", "w") as f:
f.write(settings["tools"])
# Update thread metadata if exists with retry
thread_id = cl.user_session.get("thread_id")
if thread_id:
for attempt in range(MAX_RETRIES):
try:
thread = await cl_data.get_thread(thread_id)
if thread:
metadata = thread.get("metadata", {})
if isinstance(metadata, str):
try:
metadata = json.loads(metadata)
except json.JSONDecodeError:
metadata = {}
metadata["model_name"] = config_list[0]['model']
await cl_data.update_thread(thread_id, metadata=metadata)
cl.user_session.set("metadata", metadata)
break
except sqlite3.OperationalError as e:
if "database is locked" in str(e) and attempt < MAX_RETRIES - 1:
await asyncio.sleep(RETRY_DELAY)
continue
raise
logger.info("Settings updated successfully")
except Exception as e:
logger.error(f"Error updating settings: {str(e)}")
await cl.Message(content=f"An error occurred while updating settings: {str(e)}. Retrying...").send()
# One final retry after a longer delay
try:
await asyncio.sleep(RETRY_DELAY * 2)
await on_settings_update(settings)
except Exception as e:
logger.error(f"Final retry failed: {str(e)}")
await cl.Message(content=f"Failed to update settings after retries: {str(e)}").send()
+133
View File
@@ -0,0 +1,133 @@
# Understanding Tool Integration in AI Agents - A Beginner's Guide
## Overview
This guide explains how to properly integrate tools (functions) that an AI agent can use, making them both understandable to the OpenAI API and executable by your code.
## Key Components
### 1. Tool Definition Structure
```python
# Example tool definition in tools.py
def search_tool(query: str) -> list:
"""
Perform a web search using DuckDuckGo.
Args:
query (str): The search query string.
Returns:
list: Search results with title, url, and snippet.
"""
# Function implementation...
```
### 2. Tool Dictionary Format
```python
tools_dict = {
'search_tool': {
'type': 'function',
'function': {
'name': 'search_tool',
'description': '...',
'parameters': {
'type': 'object',
'properties': {
'query': {'type': 'string'}
}
}
},
'callable': search_tool # The actual Python function
}
}
```
## The Two-Part System
### Part 1: OpenAI API Communication
```python
# task_tools: What OpenAI understands
task_tools = []
tool_def = tools_dict[tool_name].copy()
callable_func = tool_def.pop("callable") # Remove the Python function
task_tools.append(tool_def) # Add clean JSON-serializable definition
```
### Part 2: Function Execution
```python
# role_tools: What your code executes
role_tools = []
role_tools.append(callable_func) # Store the actual function
agent.tools = role_tools # Give agent access to executable functions
```
## Putting It All Together
```python
# Initialize empty lists
role_tools = [] # For executable functions
task_tools = [] # For OpenAI API definitions
# Process each tool
for tool_name in tools_list:
if tool_name in tools_dict:
# 1. Get the tool definition
tool_def = tools_dict[tool_name].copy()
# 2. Separate the callable function
callable_func = tool_def.pop("callable")
# 3. Store the function for execution
role_tools.append(callable_func)
# 4. Store the API definition
task_tools.append(tool_def)
# 5. Give agent access to functions
agent.tools = role_tools
# Create task with API definitions
task = Task(
description="...",
tools=task_tools, # OpenAI API will use these
agent=agent, # Agent has access to callable functions
# ... other parameters ...
)
```
## Why This Works
1. **API Communication**
- OpenAI API receives clean JSON tool definitions
- No Python functions that would cause serialization errors
2. **Function Execution**
- Agent has access to actual Python functions
- Can execute tools when OpenAI decides to use them
3. **Separation of Concerns**
- `task_tools`: Describes what tools can do (for OpenAI)
- `role_tools`: Actually does the work (for Python)
## Common Errors and Solutions
1. **"Invalid type for 'tools[0]'"**
- Cause: Sending null or invalid tool definition to OpenAI
- Solution: Use proper tool definition format in `task_tools`
2. **"Object of type function is not JSON serializable"**
- Cause: Trying to send Python function to OpenAI API
- Solution: Remove callable function from API definition
3. **"Tool is not callable"**
- Cause: Agent doesn't have access to executable functions
- Solution: Set `agent.tools = role_tools`
## Best Practices
1. Always initialize both `task_tools` and `role_tools` lists
2. Make clean copies of tool definitions to avoid modifying originals
3. Keep tool definitions JSON-serializable for API communication
4. Ensure agents have access to callable functions
5. Document tool parameters and return values clearly
This structure maintains clean separation between API communication and actual function execution, making your AI agent system both reliable and maintainable.
+4 -4
View File
@@ -1,6 +1,6 @@
[project]
name = "PraisonAI"
version = "2.0.26"
version = "2.0.30"
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.20",
"praisonaiagents>=0.0.21",
"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.26"
version = "2.0.30"
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.20"
praisonaiagents = ">=0.0.21"
python-dotenv = ">=0.19.0"
instructor = ">=1.3.3"
PyYAML = ">=6.0"
Generated
+5 -5
View File
@@ -3061,7 +3061,7 @@ wheels = [
[[package]]
name = "praisonai"
version = "2.0.26"
version = "2.0.30"
source = { editable = "." }
dependencies = [
{ name = "instructor" },
@@ -3198,7 +3198,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.20" },
{ name = "praisonaiagents", specifier = ">=0.0.21" },
{ 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" },
@@ -3251,16 +3251,16 @@ wheels = [
[[package]]
name = "praisonaiagents"
version = "0.0.20"
version = "0.0.21"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "openai" },
{ name = "pydantic" },
{ name = "rich" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/89/0e9d17b7113772ab469f3d113af51fb3ff5c2f16eb0a1b70667a12478358/praisonaiagents-0.0.20.tar.gz", hash = "sha256:15e08f2c8c06643e091870ba0f15c1614f6052dea45986c877dad52e8fe7f06e", size = 23885 }
sdist = { url = "https://files.pythonhosted.org/packages/ff/2a/9da2df9f6fc96e5c3192005f4bad3422fa7675fab1f07bfad9fcff93cd91/praisonaiagents-0.0.21.tar.gz", hash = "sha256:f6072220114ecb937b50ac675ace9f8b335667231637eed796e16ae1fc0656b1", size = 25460 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/94/5f62ded3aad4ee5d879e8b9712be4a3268abc2d89f71b10999109cef147c/praisonaiagents-0.0.20-py3-none-any.whl", hash = "sha256:3e9c62f3c4d8978bea8e0c95359a49e20db833511e17e122ccc06eb77996a618", size = 29883 },
{ url = "https://files.pythonhosted.org/packages/bc/61/3904cd43f66320d8b7ce407fc18c143842d3779716f51dc10b0faa9757e9/praisonaiagents-0.0.21-py3-none-any.whl", hash = "sha256:16f18d4e553471a740a03921e49d961c53ca881b287f422d65e77ae2c4c88275", size = 31742 },
]
[[package]]