mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-12 18:47:44 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5d4db4262 | ||
|
|
87d691c616 |
No files matched your search
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.1.0" \
|
||||
"praisonai>=3.1.2" \
|
||||
"praisonai[chat]" \
|
||||
"embedchain[github,youtube]"
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.1.0" \
|
||||
"praisonai>=3.1.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[chat]" \
|
||||
"praisonai[realtime]" \
|
||||
|
||||
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
|
||||
# Install Python packages (using latest versions)
|
||||
RUN pip install --no-cache-dir \
|
||||
praisonai_tools \
|
||||
"praisonai>=3.1.0" \
|
||||
"praisonai>=3.1.2" \
|
||||
"praisonai[ui]" \
|
||||
"praisonai[crewai]"
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ class Praisonai < Formula
|
||||
|
||||
desc "AI tools for various AI applications"
|
||||
homepage "https://github.com/MervinPraison/PraisonAI"
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.1.0.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.1.0.tar.gz | shasum -a 256`.split.first
|
||||
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.1.2.tar.gz"
|
||||
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v3.1.2.tar.gz | shasum -a 256`.split.first
|
||||
license "MIT"
|
||||
|
||||
depends_on "[email protected]"
|
||||
|
||||
+404
-250
@@ -4857,9 +4857,10 @@ Now, {final_instruction.lower()}:"""
|
||||
# Import message queue components
|
||||
from praisonai.cli.features.message_queue import (
|
||||
MessageQueue, StateManager, QueueDisplay, ProcessingState,
|
||||
AsyncProcessor, LiveStatusDisplay
|
||||
LiveStatusDisplay
|
||||
)
|
||||
import threading
|
||||
import queue as queue_module
|
||||
|
||||
# Create message queue and state manager
|
||||
message_queue = MessageQueue()
|
||||
@@ -4868,6 +4869,30 @@ Now, {final_instruction.lower()}:"""
|
||||
live_status = LiveStatusDisplay()
|
||||
processing_lock = threading.Lock()
|
||||
|
||||
# Create execution queue for worker thread (TRUE ASYNC)
|
||||
execution_queue = queue_module.Queue()
|
||||
approval_request_queue = queue_module.Queue()
|
||||
approval_response_queue = queue_module.Queue()
|
||||
|
||||
# Worker state for cross-thread communication
|
||||
# Enhanced with task-bound context for proper Q/A mapping
|
||||
worker_state = {
|
||||
'running': True,
|
||||
'current_task': None, # Full task context object (FIFO head)
|
||||
'approval_pending': False,
|
||||
'waiting_for_approval_input': False,
|
||||
'completed_tasks': [], # Queue of completed tasks to display (FIFO order)
|
||||
'error_tasks': [], # Queue of error tasks to display
|
||||
'tool_activity': None, # Current tool being used
|
||||
'last_status_line': None, # For transient status updates
|
||||
}
|
||||
|
||||
# Task counter for unique IDs (FIFO position tracking)
|
||||
task_counter = {'value': 0}
|
||||
|
||||
# Check for verbose mode
|
||||
verbose_mode = getattr(args, 'verbose', False) if hasattr(args, 'verbose') else False
|
||||
|
||||
# Initialize persistent session
|
||||
from praisonai.cli.session import get_session_store
|
||||
session_store = get_session_store()
|
||||
@@ -4902,11 +4927,17 @@ Now, {final_instruction.lower()}:"""
|
||||
'queue_display': queue_display,
|
||||
'live_status': live_status,
|
||||
'processing_lock': processing_lock,
|
||||
'async_processor': None, # Will be created per-request
|
||||
'execution_queue': execution_queue, # Queue for worker thread
|
||||
'approval_request_queue': approval_request_queue,
|
||||
'approval_response_queue': approval_response_queue,
|
||||
'worker_state': worker_state,
|
||||
'unified_session': unified_session, # Reference to persistent session
|
||||
'session_store': session_store, # Reference to store for saving
|
||||
}
|
||||
|
||||
# Start the execution worker thread (TRUE ASYNC - runs in background)
|
||||
worker_thread = self._start_execution_worker(tools_list, console, session_state)
|
||||
|
||||
# Try to use prompt_toolkit for autocomplete
|
||||
prompt_session = None
|
||||
try:
|
||||
@@ -4940,10 +4971,155 @@ Now, {final_instruction.lower()}:"""
|
||||
console.print(f"[dim]Resumed session with {unified_session.message_count} messages[/dim]")
|
||||
console.print("[dim]Use @file.txt to include file content | Queue messages while processing[/dim]\n")
|
||||
|
||||
# Start display thread for status updates (non-blocking)
|
||||
import time
|
||||
from rich.panel import Panel
|
||||
|
||||
display_running = {'value': True}
|
||||
|
||||
def display_loop():
|
||||
"""Background thread for status display - NEVER blocks input.
|
||||
|
||||
FIFO-aligned display:
|
||||
- Default mode: Minimal, calm output. Just show response.
|
||||
- Verbose mode: Full task lifecycle with IDs, times, word counts.
|
||||
"""
|
||||
while display_running['value']:
|
||||
try:
|
||||
# Check for completed tasks to display (FIFO order guaranteed)
|
||||
if worker_state['completed_tasks']:
|
||||
task = worker_state['completed_tasks'].pop(0)
|
||||
response = task.get('response', '')
|
||||
|
||||
# Clear any transient status line
|
||||
if worker_state.get('last_status_line'):
|
||||
console.print("\r" + " " * 80 + "\r", end="")
|
||||
worker_state['last_status_line'] = None
|
||||
|
||||
console.print() # New line before response
|
||||
|
||||
if verbose_mode:
|
||||
# VERBOSE: Full task lifecycle with metadata
|
||||
question = task.get('question', '')
|
||||
task_id = task.get('task_id', 0)
|
||||
elapsed = task.get('elapsed', 0)
|
||||
word_count = len(response.split()) if response else 0
|
||||
q_display = question[:80] + "..." if len(question) > 80 else question
|
||||
|
||||
console.print(f"[bold cyan]─── Task #{task_id} completed ({elapsed:.1f}s, {word_count} words) ───[/bold cyan]")
|
||||
console.print(f"[bold green]Q:[/bold green] {q_display}")
|
||||
console.print(f"[bold blue]A:[/bold blue] ", end="")
|
||||
|
||||
# Stream response (both modes)
|
||||
if response:
|
||||
words = response.split()
|
||||
for i, word in enumerate(words):
|
||||
console.print(word + " ", end="")
|
||||
if i % 20 == 19:
|
||||
time.sleep(0.003)
|
||||
console.print()
|
||||
|
||||
if verbose_mode:
|
||||
task_id = task.get('task_id', 0)
|
||||
console.print(f"[dim]─── End Task #{task_id} ───[/dim]")
|
||||
console.print()
|
||||
|
||||
# Check for error tasks to display
|
||||
if worker_state['error_tasks']:
|
||||
task = worker_state['error_tasks'].pop(0)
|
||||
error = task.get('error', '')
|
||||
|
||||
# Clear transient status
|
||||
if worker_state.get('last_status_line'):
|
||||
console.print("\r" + " " * 80 + "\r", end="")
|
||||
worker_state['last_status_line'] = None
|
||||
|
||||
console.print()
|
||||
if verbose_mode:
|
||||
question = task.get('question', '')
|
||||
task_id = task.get('task_id', 0)
|
||||
q_display = question[:80] + "..." if len(question) > 80 else question
|
||||
console.print(f"[bold red]─── Task #{task_id} failed ───[/bold red]")
|
||||
console.print(f"[bold green]Q:[/bold green] {q_display}")
|
||||
console.print(f"[red]Error: {error}[/red]")
|
||||
console.print()
|
||||
|
||||
# Check for approval requests
|
||||
try:
|
||||
approval_info = approval_request_queue.get_nowait()
|
||||
|
||||
function_name = approval_info['function_name']
|
||||
arguments = approval_info['arguments']
|
||||
risk_level = approval_info['risk_level']
|
||||
|
||||
risk_colors = {"critical": "bold red", "high": "red", "medium": "yellow", "low": "blue"}
|
||||
risk_color = risk_colors.get(risk_level, "white")
|
||||
|
||||
# Clear transient status
|
||||
if worker_state.get('last_status_line'):
|
||||
console.print("\r" + " " * 80 + "\r", end="")
|
||||
worker_state['last_status_line'] = None
|
||||
|
||||
console.print()
|
||||
|
||||
# Show approval panel (both modes)
|
||||
tool_info = f"[bold]Function:[/] {function_name}\n"
|
||||
tool_info += f"[bold]Risk Level:[/] [{risk_color}]{risk_level.upper()}[/{risk_color}]\n"
|
||||
tool_info += "[bold]Arguments:[/]\n"
|
||||
for key, value in arguments.items():
|
||||
str_value = str(value)[:100] + "..." if len(str(value)) > 100 else str(value)
|
||||
tool_info += f" {key}: {str_value}\n"
|
||||
|
||||
console.print(Panel(tool_info.strip(), title="🔒 Tool Approval Required", border_style=risk_color))
|
||||
console.print(f"[{risk_color}]Type 'y' to approve, 'n' to reject:[/{risk_color}] ", end="")
|
||||
|
||||
worker_state['waiting_for_approval_input'] = True
|
||||
|
||||
except queue_module.Empty:
|
||||
pass
|
||||
|
||||
# Show FIFO head status (transient, both modes)
|
||||
current_task = worker_state.get('current_task')
|
||||
if current_task and not worker_state.get('waiting_for_approval_input'):
|
||||
queue_size = execution_queue.qsize()
|
||||
prompt_preview = current_task.get('question', '')[:50]
|
||||
if len(current_task.get('question', '')) > 50:
|
||||
prompt_preview += "..."
|
||||
|
||||
# Build status line
|
||||
tool_activity = worker_state.get('tool_activity')
|
||||
if tool_activity and tool_activity.get('status') == 'started':
|
||||
tool_name = tool_activity.get('name', '')
|
||||
status_line = f"⚙️ {tool_name}"
|
||||
else:
|
||||
status_line = f"⏳ Processing: {prompt_preview}"
|
||||
|
||||
if queue_size > 0:
|
||||
status_line += f" | 📋 {queue_size} waiting"
|
||||
|
||||
# Only update if changed (avoid flicker)
|
||||
if status_line != worker_state.get('last_status_line'):
|
||||
console.print(f"\r[dim cyan]{status_line}[/dim cyan]" + " " * 20, end="\r")
|
||||
worker_state['last_status_line'] = status_line
|
||||
elif not current_task and worker_state.get('last_status_line'):
|
||||
# Clear status when idle
|
||||
console.print("\r" + " " * 80 + "\r", end="")
|
||||
worker_state['last_status_line'] = None
|
||||
|
||||
time.sleep(0.1)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Start display thread
|
||||
display_thread = threading.Thread(target=display_loop, daemon=True, name="DisplayLoop")
|
||||
display_thread.start()
|
||||
|
||||
running = True
|
||||
while running:
|
||||
try:
|
||||
# Get user input (with autocomplete if available)
|
||||
# This is NON-BLOCKING relative to LLM execution
|
||||
# Status is shown by the display thread, not here
|
||||
if prompt_session:
|
||||
user_input = prompt_session.prompt().strip()
|
||||
else:
|
||||
@@ -4952,6 +5128,21 @@ Now, {final_instruction.lower()}:"""
|
||||
if not user_input:
|
||||
continue
|
||||
|
||||
# Check if this is an approval response
|
||||
if worker_state.get('waiting_for_approval_input'):
|
||||
try:
|
||||
from praisonaiagents.approval import ApprovalDecision
|
||||
if user_input.lower() in ['y', 'yes', 'approve']:
|
||||
console.print("[green]✅ Approved[/green]")
|
||||
approval_response_queue.put(ApprovalDecision(approved=True, reason="User approved"))
|
||||
else:
|
||||
console.print("[red]❌ Denied[/red]")
|
||||
approval_response_queue.put(ApprovalDecision(approved=False, reason="User denied"))
|
||||
worker_state['waiting_for_approval_input'] = False
|
||||
continue
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Handle slash commands
|
||||
if user_input.startswith("/"):
|
||||
cmd_parts = user_input[1:].split(maxsplit=1)
|
||||
@@ -4961,6 +5152,8 @@ Now, {final_instruction.lower()}:"""
|
||||
if cmd in ["exit", "quit", "q"]:
|
||||
console.print("[dim]Goodbye![/dim]")
|
||||
running = False
|
||||
worker_state['running'] = False
|
||||
display_running['value'] = False
|
||||
continue
|
||||
elif cmd == "help":
|
||||
self._print_interactive_help(console)
|
||||
@@ -5031,6 +5224,17 @@ Now, {final_instruction.lower()}:"""
|
||||
session_state['request_count'] = 0
|
||||
console.print(f"[cyan]Started new session: {new_session.session_id}[/cyan]")
|
||||
continue
|
||||
elif cmd == "status":
|
||||
# Show current processing status
|
||||
current = worker_state.get('current_prompt')
|
||||
queue_size = execution_queue.qsize()
|
||||
if current:
|
||||
console.print(f"[cyan]Processing:[/cyan] {current}")
|
||||
if queue_size > 0:
|
||||
console.print(f"[cyan]Queued:[/cyan] {queue_size} messages")
|
||||
if not current and queue_size == 0:
|
||||
console.print("[dim]Idle - no messages processing[/dim]")
|
||||
continue
|
||||
else:
|
||||
console.print(f"[yellow]Unknown command: /{cmd}. Type /help for available commands.[/yellow]")
|
||||
continue
|
||||
@@ -5038,20 +5242,35 @@ Now, {final_instruction.lower()}:"""
|
||||
# Process @file mentions before sending to LLM
|
||||
processed_input = self._process_at_mentions(user_input, console)
|
||||
|
||||
# Check if already processing - queue if so
|
||||
if state_manager.is_processing:
|
||||
message_queue.add(processed_input)
|
||||
queue_count = message_queue.count
|
||||
console.print(f"[dim]📋 Queued ({queue_count}) - processing will continue in order[/dim]")
|
||||
continue
|
||||
# Create task with unique ID and full context
|
||||
task_counter['value'] += 1
|
||||
task_id = task_counter['value']
|
||||
|
||||
# Start async processing
|
||||
self._process_interactive_prompt_async(
|
||||
processed_input,
|
||||
tools_list,
|
||||
console,
|
||||
session_state=session_state
|
||||
)
|
||||
task = {
|
||||
'task_id': task_id,
|
||||
'prompt': processed_input,
|
||||
'question': processed_input, # Full question for Q/A mapping
|
||||
'status': 'queued',
|
||||
'queued_at': time.time(),
|
||||
}
|
||||
|
||||
# Submit to execution queue - THIS IS NON-BLOCKING (FIFO)
|
||||
queue_size = execution_queue.qsize()
|
||||
execution_queue.put(task)
|
||||
|
||||
# Show queue status (minimal by default, verbose shows task IDs)
|
||||
if verbose_mode:
|
||||
q_preview = processed_input[:60] + "..." if len(processed_input) > 60 else processed_input
|
||||
if queue_size > 0:
|
||||
console.print(f"[dim cyan]📋 Task #{task_id} queued (FIFO position {queue_size + 1})[/dim cyan]")
|
||||
console.print(f"[dim] └─ {q_preview}[/dim]")
|
||||
else:
|
||||
console.print(f"[dim cyan]▶ Task #{task_id} started (FIFO head)[/dim cyan]")
|
||||
console.print(f"[dim] └─ {q_preview}[/dim]")
|
||||
else:
|
||||
# DEFAULT: Minimal, calm output
|
||||
if queue_size > 0:
|
||||
console.print(f"[dim]📋 Queued ({queue_size + 1} in queue)[/dim]")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
console.print("\n[dim]Use /exit to quit[/dim]")
|
||||
@@ -5494,266 +5713,201 @@ Provide a concise summary (max 200 words):"""
|
||||
|
||||
return None
|
||||
|
||||
def _process_interactive_prompt_async(self, prompt, tools_list, console, session_state=None):
|
||||
def _start_execution_worker(self, tools_list, console, session_state):
|
||||
"""
|
||||
Process a prompt asynchronously with non-blocking status display.
|
||||
Start the background execution worker thread.
|
||||
|
||||
Shows status updates without blocking user input.
|
||||
Allows user to queue more messages while processing.
|
||||
Supports interactive approval for tool calls.
|
||||
This worker continuously processes messages from the queue,
|
||||
allowing the main input loop to remain non-blocking.
|
||||
"""
|
||||
import threading
|
||||
import time
|
||||
import sys
|
||||
import queue
|
||||
from rich.live import Live
|
||||
from rich.spinner import Spinner
|
||||
import queue as queue_module
|
||||
from rich.panel import Panel
|
||||
from praisonai.cli.features.message_queue import ProcessingState
|
||||
|
||||
state_manager = session_state['state_manager']
|
||||
message_queue = session_state['message_queue']
|
||||
live_status = session_state['live_status']
|
||||
|
||||
# Set processing state
|
||||
state_manager.set_state(ProcessingState.PROCESSING)
|
||||
live_status.clear()
|
||||
live_status.update_status("Thinking...")
|
||||
|
||||
# Result container for thread communication
|
||||
result_container = {
|
||||
'response': None,
|
||||
'error': None,
|
||||
'done': False,
|
||||
'approval_pending': False,
|
||||
'approval_info': None,
|
||||
'approval_response': None
|
||||
}
|
||||
|
||||
# Approval queue for cross-thread communication
|
||||
approval_request_queue = queue.Queue()
|
||||
approval_response_queue = queue.Queue()
|
||||
|
||||
# Get conversation history for context
|
||||
conversation_history = session_state.get('conversation_history', [])
|
||||
execution_queue = session_state['execution_queue']
|
||||
approval_request_queue = session_state['approval_request_queue']
|
||||
approval_response_queue = session_state['approval_response_queue']
|
||||
worker_state = session_state['worker_state']
|
||||
|
||||
# Check if trust mode is enabled
|
||||
trust_mode = getattr(self.args, 'trust', False) if hasattr(self, 'args') else False
|
||||
|
||||
def process_in_background():
|
||||
"""Run agent processing in background thread."""
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
# Suppress noisy loggers
|
||||
for logger_name in ["httpx", "httpcore", "duckduckgo_search", "crawl4ai"]:
|
||||
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
||||
|
||||
# Set up approval callback
|
||||
def worker_loop():
|
||||
"""Main worker loop - processes execution queue."""
|
||||
while worker_state['running']:
|
||||
try:
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
if trust_mode:
|
||||
# Auto-approve all in trust mode
|
||||
def auto_approve_all(function_name, arguments, risk_level):
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved via --trust flag")
|
||||
set_approval_callback(auto_approve_all)
|
||||
else:
|
||||
# Interactive approval via queue
|
||||
def interactive_approval_callback(function_name, arguments, risk_level):
|
||||
"""Request approval from main thread via queue."""
|
||||
# Send approval request to main thread
|
||||
approval_request_queue.put({
|
||||
'function_name': function_name,
|
||||
'arguments': arguments,
|
||||
'risk_level': risk_level
|
||||
})
|
||||
result_container['approval_pending'] = True
|
||||
|
||||
# Wait for response from main thread (with timeout)
|
||||
try:
|
||||
response = approval_response_queue.get(timeout=120) # 2 minute timeout
|
||||
result_container['approval_pending'] = False
|
||||
return response
|
||||
except queue.Empty:
|
||||
result_container['approval_pending'] = False
|
||||
return ApprovalDecision(approved=False, reason="Approval timeout")
|
||||
|
||||
set_approval_callback(interactive_approval_callback)
|
||||
except ImportError:
|
||||
pass # Approval module not available
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
model = session_state.get('current_model')
|
||||
|
||||
live_status.update_status("Creating agent...")
|
||||
|
||||
# Build system prompt with conversation context
|
||||
backstory = "You are a helpful AI assistant with access to tools for file operations, code intelligence, and shell commands."
|
||||
if conversation_history:
|
||||
# Add recent conversation context to backstory
|
||||
recent = conversation_history[-10:] # Last 10 messages
|
||||
context_lines = []
|
||||
for msg in recent:
|
||||
role = msg.get('role', 'unknown')
|
||||
content = msg.get('content', '')[:200] # Truncate long messages
|
||||
context_lines.append(f"{role}: {content}")
|
||||
if context_lines:
|
||||
backstory += "\n\nRecent conversation context:\n" + "\n".join(context_lines)
|
||||
|
||||
# Create agent with tools
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
role="Helpful AI Assistant",
|
||||
goal="Help the user with their tasks",
|
||||
backstory=backstory,
|
||||
tools=tools_list if tools_list else None,
|
||||
verbose=False,
|
||||
llm=model
|
||||
)
|
||||
|
||||
live_status.update_status("Calling LLM...")
|
||||
|
||||
# Get response
|
||||
response = agent.chat(prompt, stream=False)
|
||||
result_container['response'] = str(response) if response else ""
|
||||
|
||||
except Exception as e:
|
||||
result_container['error'] = e
|
||||
finally:
|
||||
result_container['done'] = True
|
||||
|
||||
# Start background thread
|
||||
bg_thread = threading.Thread(target=process_in_background, daemon=True)
|
||||
bg_thread.start()
|
||||
|
||||
# Main loop: handle spinner display and approval requests
|
||||
start_time = time.time()
|
||||
live_instance = None
|
||||
|
||||
try:
|
||||
while not result_container['done']:
|
||||
# Check for pending approval requests
|
||||
try:
|
||||
approval_info = approval_request_queue.get_nowait()
|
||||
|
||||
# Stop spinner to show approval prompt
|
||||
if live_instance:
|
||||
live_instance.stop()
|
||||
live_instance = None
|
||||
|
||||
# Display approval prompt
|
||||
state_manager.set_state(ProcessingState.WAITING_APPROVAL)
|
||||
|
||||
function_name = approval_info['function_name']
|
||||
arguments = approval_info['arguments']
|
||||
risk_level = approval_info['risk_level']
|
||||
|
||||
risk_colors = {"critical": "bold red", "high": "red", "medium": "yellow", "low": "blue"}
|
||||
risk_color = risk_colors.get(risk_level, "white")
|
||||
|
||||
tool_info = f"[bold]Function:[/] {function_name}\n"
|
||||
tool_info += f"[bold]Risk Level:[/] [{risk_color}]{risk_level.upper()}[/{risk_color}]\n"
|
||||
tool_info += "[bold]Arguments:[/]\n"
|
||||
for key, value in arguments.items():
|
||||
str_value = str(value)[:100] + "..." if len(str(value)) > 100 else str(value)
|
||||
tool_info += f" {key}: {str_value}\n"
|
||||
|
||||
console.print(Panel(tool_info.strip(), title="🔒 Tool Approval Required", border_style=risk_color))
|
||||
console.print(f"[{risk_color}]Type 'y' or 'yes' to approve, 'n' or 'no' to reject:[/{risk_color}] ", end="")
|
||||
|
||||
# Get user input for approval
|
||||
# Wait for a message with timeout (allows checking running flag)
|
||||
try:
|
||||
from praisonaiagents.approval import ApprovalDecision
|
||||
user_response = input().strip().lower()
|
||||
|
||||
if user_response in ['y', 'yes', 'approve']:
|
||||
console.print("[green]✅ Approved[/green]")
|
||||
approval_response_queue.put(ApprovalDecision(approved=True, reason="User approved"))
|
||||
else:
|
||||
console.print("[red]❌ Denied[/red]")
|
||||
approval_response_queue.put(ApprovalDecision(approved=False, reason="User denied"))
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
console.print("\n[red]❌ Cancelled[/red]")
|
||||
from praisonaiagents.approval import ApprovalDecision
|
||||
approval_response_queue.put(ApprovalDecision(approved=False, reason="User cancelled"))
|
||||
task = execution_queue.get(timeout=0.5)
|
||||
except queue_module.Empty:
|
||||
continue
|
||||
|
||||
# Extract task context
|
||||
prompt = task.get('prompt', '')
|
||||
task_id = task.get('task_id', 0)
|
||||
question = task.get('question', prompt)
|
||||
start_time = time.time()
|
||||
|
||||
# Set processing state with full task context
|
||||
state_manager.set_state(ProcessingState.PROCESSING)
|
||||
task['status'] = 'running'
|
||||
task['start_time'] = start_time
|
||||
worker_state['current_task'] = task # Full task context
|
||||
live_status.clear()
|
||||
live_status.update_status(f"Task #{task_id}: Thinking...")
|
||||
|
||||
except queue.Empty:
|
||||
pass # No approval pending
|
||||
|
||||
# Update spinner display if not waiting for approval
|
||||
if not result_container['approval_pending'] and not result_container['done']:
|
||||
elapsed = time.time() - start_time
|
||||
current_status = live_status.current_status
|
||||
queue_count = message_queue.count
|
||||
queue_info = f" | Queued: {queue_count}" if queue_count > 0 else ""
|
||||
spinner_text = f"{current_status} ({elapsed:.1f}s){queue_info}"
|
||||
try:
|
||||
from praisonaiagents import Agent
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
# Suppress noisy loggers
|
||||
for logger_name in ["httpx", "httpcore", "duckduckgo_search", "crawl4ai"]:
|
||||
logging.getLogger(logger_name).setLevel(logging.WARNING)
|
||||
|
||||
# Set up approval callback
|
||||
try:
|
||||
from praisonaiagents.approval import set_approval_callback, ApprovalDecision
|
||||
|
||||
if trust_mode:
|
||||
def auto_approve_all(function_name, arguments, risk_level):
|
||||
return ApprovalDecision(approved=True, reason="Auto-approved via --trust flag")
|
||||
set_approval_callback(auto_approve_all)
|
||||
else:
|
||||
def interactive_approval_callback(function_name, arguments, risk_level):
|
||||
"""Request approval from main thread via queue."""
|
||||
approval_request_queue.put({
|
||||
'function_name': function_name,
|
||||
'arguments': arguments,
|
||||
'risk_level': risk_level
|
||||
})
|
||||
worker_state['approval_pending'] = True
|
||||
|
||||
try:
|
||||
response = approval_response_queue.get(timeout=120)
|
||||
worker_state['approval_pending'] = False
|
||||
return response
|
||||
except queue_module.Empty:
|
||||
worker_state['approval_pending'] = False
|
||||
return ApprovalDecision(approved=False, reason="Approval timeout")
|
||||
|
||||
set_approval_callback(interactive_approval_callback)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
model = session_state.get('current_model')
|
||||
conversation_history = session_state.get('conversation_history', [])
|
||||
|
||||
live_status.update_status("Creating agent...")
|
||||
|
||||
# Build backstory with context
|
||||
backstory = "You are a helpful AI assistant with access to tools for file operations, code intelligence, and shell commands."
|
||||
if conversation_history:
|
||||
recent = conversation_history[-10:]
|
||||
context_lines = []
|
||||
for msg in recent:
|
||||
role = msg.get('role', 'unknown')
|
||||
content = msg.get('content', '')[:200]
|
||||
context_lines.append(f"{role}: {content}")
|
||||
if context_lines:
|
||||
backstory += "\n\nRecent conversation context:\n" + "\n".join(context_lines)
|
||||
|
||||
agent = Agent(
|
||||
name="Assistant",
|
||||
role="Helpful AI Assistant",
|
||||
goal="Help the user with their tasks",
|
||||
backstory=backstory,
|
||||
tools=tools_list if tools_list else None,
|
||||
verbose=False,
|
||||
llm=model
|
||||
)
|
||||
|
||||
live_status.update_status(f"Task #{task_id}: Calling LLM...")
|
||||
|
||||
response = agent.chat(prompt, stream=False)
|
||||
response_str = str(response) if response else ""
|
||||
|
||||
# Calculate elapsed time
|
||||
elapsed = time.time() - start_time
|
||||
|
||||
# Store completed task for display with Q/A mapping
|
||||
completed_task = {
|
||||
'task_id': task_id,
|
||||
'question': task.get('question', prompt),
|
||||
'response': response_str,
|
||||
'elapsed': elapsed,
|
||||
'status': 'completed',
|
||||
}
|
||||
worker_state['completed_tasks'].append(completed_task)
|
||||
|
||||
# Update session state
|
||||
input_tokens = len(prompt) // 4
|
||||
output_tokens = len(response_str) // 4
|
||||
session_state['total_input_tokens'] += input_tokens
|
||||
session_state['total_output_tokens'] += output_tokens
|
||||
session_state['request_count'] += 1
|
||||
session_state['conversation_history'].append({'role': 'user', 'content': prompt})
|
||||
session_state['conversation_history'].append({'role': 'assistant', 'content': response_str})
|
||||
|
||||
# Persist to UnifiedSession
|
||||
if 'unified_session' in session_state and 'session_store' in session_state:
|
||||
unified_session = session_state['unified_session']
|
||||
unified_session.add_user_message(prompt)
|
||||
unified_session.add_assistant_message(response_str)
|
||||
unified_session.update_stats(input_tokens, output_tokens)
|
||||
session_state['session_store'].save(unified_session)
|
||||
|
||||
if live_instance is None:
|
||||
# Start new Live context
|
||||
live_instance = Live(Spinner("dots", text=spinner_text, style="cyan"),
|
||||
console=console, refresh_per_second=10, transient=True)
|
||||
live_instance.start()
|
||||
else:
|
||||
live_instance.update(Spinner("dots", text=spinner_text, style="cyan"))
|
||||
except Exception as e:
|
||||
# Store error task for display with Q/A mapping
|
||||
error_task = {
|
||||
'task_id': task_id,
|
||||
'question': task.get('question', prompt),
|
||||
'error': str(e),
|
||||
'status': 'failed',
|
||||
}
|
||||
worker_state['error_tasks'].append(error_task)
|
||||
|
||||
finally:
|
||||
worker_state['current_task'] = None
|
||||
worker_state['tool_activity'] = None
|
||||
state_manager.set_state(ProcessingState.IDLE)
|
||||
live_status.clear()
|
||||
execution_queue.task_done()
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[red]Display error: {e}[/red]")
|
||||
finally:
|
||||
if live_instance:
|
||||
live_instance.stop()
|
||||
except Exception as e:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Wait for thread to finish
|
||||
bg_thread.join(timeout=2.0)
|
||||
# Start worker thread
|
||||
worker_thread = threading.Thread(target=worker_loop, daemon=True, name="ExecutionWorker")
|
||||
worker_thread.start()
|
||||
return worker_thread
|
||||
|
||||
def _submit_prompt_to_worker(self, prompt, session_state):
|
||||
"""
|
||||
Submit a prompt to the execution worker queue.
|
||||
|
||||
# Handle result
|
||||
if result_container['error']:
|
||||
console.print(f"[red]Error: {result_container['error']}[/red]")
|
||||
elif result_container['response']:
|
||||
# Print response with streaming effect
|
||||
response_str = result_container['response']
|
||||
words = response_str.split()
|
||||
for i, word in enumerate(words):
|
||||
console.print(word + " ", end="")
|
||||
sys.stdout.flush()
|
||||
if i % 15 == 14:
|
||||
time.sleep(0.005)
|
||||
console.print()
|
||||
|
||||
# Update session state
|
||||
input_tokens = len(prompt) // 4
|
||||
output_tokens = len(response_str) // 4
|
||||
session_state['total_input_tokens'] += input_tokens
|
||||
session_state['total_output_tokens'] += output_tokens
|
||||
session_state['request_count'] += 1
|
||||
session_state['conversation_history'].append({'role': 'user', 'content': prompt})
|
||||
session_state['conversation_history'].append({'role': 'assistant', 'content': response_str})
|
||||
|
||||
# Persist to UnifiedSession
|
||||
if 'unified_session' in session_state and 'session_store' in session_state:
|
||||
unified_session = session_state['unified_session']
|
||||
unified_session.add_user_message(prompt)
|
||||
unified_session.add_assistant_message(response_str)
|
||||
unified_session.update_stats(input_tokens, output_tokens)
|
||||
session_state['session_store'].save(unified_session)
|
||||
|
||||
# Set state back to idle
|
||||
state_manager.set_state(ProcessingState.IDLE)
|
||||
live_status.clear()
|
||||
|
||||
# Process next queued message if any
|
||||
next_msg = message_queue.pop()
|
||||
if next_msg:
|
||||
console.print("\n[dim cyan]Processing queued message...[/dim cyan]")
|
||||
self._process_interactive_prompt_async(next_msg, tools_list, console, session_state)
|
||||
This is NON-BLOCKING - returns immediately after queuing.
|
||||
"""
|
||||
execution_queue = session_state['execution_queue']
|
||||
execution_queue.put({'prompt': prompt})
|
||||
|
||||
def _process_interactive_prompt_async(self, prompt, tools_list, console, session_state=None):
|
||||
"""
|
||||
DEPRECATED: This method is kept for backward compatibility.
|
||||
Use _submit_prompt_to_worker instead for true non-blocking behavior.
|
||||
"""
|
||||
# Just submit to worker queue - non-blocking
|
||||
self._submit_prompt_to_worker(prompt, session_state)
|
||||
|
||||
def _process_interactive_prompt(self, prompt, tools_list, console, show_profiling=False, session_state=None):
|
||||
"""Process a prompt in interactive mode with streaming."""
|
||||
|
||||
@@ -57,7 +57,7 @@ class CloudDeployer:
|
||||
file.write("FROM python:3.11-slim\n")
|
||||
file.write("WORKDIR /app\n")
|
||||
file.write("COPY . .\n")
|
||||
file.write("RUN pip install flask praisonai==3.1.0 gunicorn markdown\n")
|
||||
file.write("RUN pip install flask praisonai==3.1.2 gunicorn markdown\n")
|
||||
file.write("EXPOSE 8080\n")
|
||||
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
__version__ = "3.1.0"
|
||||
__version__ = "3.1.2"
|
||||
Reference in new issue
Block a user