Compare commits

...
2 Commits
Author SHA1 Message Date
Mervin Praison 6dbf194312 Release v2.3.78 2025-12-23 06:46:15 +00:00
Mervin Praison 335e8d7e47 Release v2.3.77 2025-12-23 06:28:26 +00:00
13 changed files with 432 additions and 95 deletions

No files matched your search

+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.76" \
"praisonai>=2.3.78" \
"praisonai[chat]" \
"embedchain[github,youtube]"
+1 -1
View File
@@ -20,7 +20,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.76" \
"praisonai>=2.3.78" \
"praisonai[ui]" \
"praisonai[chat]" \
"praisonai[realtime]" \
+1 -1
View File
@@ -16,7 +16,7 @@ RUN mkdir -p /root/.praison
# Install Python packages (using latest versions)
RUN pip install --no-cache-dir \
praisonai_tools \
"praisonai>=2.3.76" \
"praisonai>=2.3.78" \
"praisonai[ui]" \
"praisonai[crewai]"
+2 -2
View File
@@ -3,8 +3,8 @@ class Praisonai < Formula
desc "AI tools for various AI applications"
homepage "https://github.com/MervinPraison/PraisonAI"
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.76.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.76.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.78.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.78.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
@@ -105,6 +105,7 @@ class FileSearchService:
- Fuzzy match file paths
- Cache results with TTL
- Respect .gitignore (basic)
- Support absolute paths (@/Users/...) and home paths (@~/...)
"""
def __init__(
@@ -125,9 +126,10 @@ class FileSearchService:
# Cache: query -> (results, timestamp)
self._cache: Dict[str, Tuple[List[FileSuggestion], float]] = {}
# File list cache
# File list cache per directory
self._file_list: Optional[List[str]] = None
self._file_list_time: float = 0
self._file_list_dir: Optional[str] = None
def _should_ignore(self, name: str) -> bool:
"""Check if file/dir should be ignored."""
@@ -136,20 +138,23 @@ class FileSearchService:
return True
return False
def _get_file_list(self) -> List[str]:
def _get_file_list(self, search_dir: str = None) -> List[str]:
"""Get list of all files (cached)."""
now = time.time()
target_dir = search_dir or self._root
# Return cached if fresh
if self._file_list is not None and (now - self._file_list_time) < self._cache_ttl:
# Return cached if fresh and same directory
if (self._file_list is not None and
self._file_list_dir == target_dir and
(now - self._file_list_time) < self._cache_ttl):
return self._file_list
files = []
try:
for root, dirs, filenames in os.walk(self._root):
for root, dirs, filenames in os.walk(target_dir):
# Calculate depth
rel_root = os.path.relpath(root, self._root)
rel_root = os.path.relpath(root, target_dir)
depth = 0 if rel_root == '.' else rel_root.count(os.sep) + 1
if depth > self._max_depth:
@@ -161,19 +166,20 @@ class FileSearchService:
# Add directories
for d in dirs:
rel_path = os.path.relpath(os.path.join(root, d), self._root)
rel_path = os.path.relpath(os.path.join(root, d), target_dir)
files.append(rel_path + os.sep)
# Add files
for f in filenames:
if not self._should_ignore(f):
rel_path = os.path.relpath(os.path.join(root, f), self._root)
rel_path = os.path.relpath(os.path.join(root, f), target_dir)
files.append(rel_path)
except OSError:
pass
self._file_list = files
self._file_list_time = now
self._file_list_dir = target_dir
return files
def _fuzzy_match(self, text: str, query: str) -> int:
@@ -221,6 +227,11 @@ class FileSearchService:
"""
Search for files matching query.
Supports:
- Relative paths: @file.txt, @src/main.py
- Home paths: @~/Documents, @~/.config
- Absolute paths: @/Users/name/file.txt, @/etc/hosts
Args:
query: Search query (fuzzy matched)
max_results: Maximum results to return
@@ -228,8 +239,51 @@ class FileSearchService:
Returns:
List of FileSuggestion sorted by score
"""
# Determine search directory based on query
search_dir = self._root
search_query = query
prefix = ""
# Handle home directory paths (@~/)
if query.startswith('~'):
expanded = os.path.expanduser(query)
# Find the directory part and filename part
if os.path.isdir(expanded):
search_dir = expanded
search_query = ""
prefix = query.rstrip('/') + '/'
else:
parent = os.path.dirname(expanded)
if os.path.isdir(parent):
search_dir = parent
search_query = os.path.basename(expanded)
prefix = os.path.dirname(query).rstrip('/') + '/'
else:
# Just expand ~ and search from home
search_dir = os.path.expanduser('~')
search_query = query[2:] if query.startswith('~/') else query[1:]
prefix = "~/"
# Handle absolute paths (@/Users/..., @/etc/...)
elif query.startswith('/'):
if os.path.isdir(query):
search_dir = query
search_query = ""
prefix = query.rstrip('/') + '/'
else:
parent = os.path.dirname(query)
if os.path.isdir(parent):
search_dir = parent
search_query = os.path.basename(query)
prefix = parent.rstrip('/') + '/'
else:
# Search from root with the query
search_dir = '/'
search_query = query[1:]
prefix = "/"
# Check cache
cache_key = f"{query}:{max_results}"
cache_key = f"{search_dir}:{search_query}:{max_results}"
now = time.time()
if cache_key in self._cache:
@@ -237,25 +291,26 @@ class FileSearchService:
if (now - cached_time) < self._cache_ttl:
return results
# Get all files
all_files = self._get_file_list()
# Get all files from the appropriate directory
all_files = self._get_file_list(search_dir)
# Score and filter
scored: List[Tuple[int, str]] = []
for path in all_files:
score = self._fuzzy_match(path, query)
score = self._fuzzy_match(path, search_query)
if score > 0:
scored.append((score, path))
# Sort by score descending
scored.sort(key=lambda x: (-x[0], x[1]))
# Build results
# Build results with proper prefix
results = []
for score, path in scored[:max_results]:
file_type = "directory" if path.endswith(os.sep) else "file"
display_path = prefix + path if prefix else path
results.append(FileSuggestion(
path=path,
path=display_path,
file_type=file_type,
score=score
))
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
COMPARE_MODES = {
"basic": {},
"tools": {"tools": "internet_search"},
"tools": {"tools": "calculator,internet_search"},
"research": {"research": True},
"planning": {"planning": True},
"memory": {"memory": True},
@@ -407,13 +407,7 @@ class CompareHandler:
from praisonaiagents.tools import internet_search
tools.append(internet_search)
elif name == "calculator":
def calculator(expression: str) -> str:
"""Evaluate a mathematical expression."""
try:
return str(eval(expression))
except Exception as e:
return f"Error: {e}"
tools.append(calculator)
tools.append(_calculator_tool)
except ImportError:
logger.warning(f"Could not load tool: {name}")
@@ -466,3 +460,50 @@ class CompareHandler:
print(f"[red]Failed to save results to: {output_path}[/red]")
return result
def _calculator_tool(expression: str) -> str:
"""
Evaluate a mathematical expression safely.
Args:
expression: Mathematical expression to evaluate (e.g., "2+2", "15*3", "100/4")
Returns:
The result of the calculation as a string
"""
import ast
import operator
# Safe operators
operators = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.USub: operator.neg,
ast.UAdd: operator.pos,
ast.Mod: operator.mod,
ast.FloorDiv: operator.floordiv,
}
def _eval(node):
if isinstance(node, ast.Constant):
return node.value
elif isinstance(node, ast.BinOp):
left = _eval(node.left)
right = _eval(node.right)
return operators[type(node.op)](left, right)
elif isinstance(node, ast.UnaryOp):
operand = _eval(node.operand)
return operators[type(node.op)](operand)
else:
raise ValueError(f"Unsupported operation: {type(node)}")
try:
tree = ast.parse(expression, mode='eval')
result = _eval(tree.body)
return str(result)
except Exception as e:
return f"Error evaluating '{expression}': {e}"
+55 -17
View File
@@ -26,42 +26,68 @@ class EvalHandler:
def run_accuracy(
self,
agent_file: str,
input_text: str,
expected_output: str,
agent_file: Optional[str] = None,
input_text: str = "",
expected_output: str = "",
iterations: int = 1,
model: Optional[str] = None,
output_file: Optional[str] = None
output_file: Optional[str] = None,
prompt: Optional[str] = None,
llm: Optional[str] = None
) -> Dict[str, Any]:
"""
Run accuracy evaluation on an agent.
Args:
agent_file: Path to agents.yaml file
agent_file: Path to agents.yaml file (optional if prompt is provided)
input_text: Input to provide to the agent
expected_output: Expected output to compare against
iterations: Number of evaluation iterations
model: LLM model for judging
output_file: Path to save results
prompt: Direct prompt (alternative to agent_file)
llm: LLM model for the agent (when using prompt)
Returns:
Evaluation result dictionary
"""
try:
from praisonaiagents.eval import AccuracyEvaluator
from praisonai.agents_generator import AgentsGenerator
from praisonaiagents import Agent
except ImportError as e:
logger.error(f"Failed to import evaluation modules: {e}")
return {"error": str(e)}
try:
generator = AgentsGenerator(agent_file)
agents = generator.generate_agents()
if not agents:
return {"error": "No agents found in configuration"}
agent = agents[0] if isinstance(agents, list) else agents
# Create agent either from file or from prompt
if prompt:
# Direct prompt mode - create agent on the fly
agent = Agent(
name="EvalAgent",
role="Assistant",
goal="Complete the given task",
backstory="You are a helpful assistant.",
llm=llm or model or "gpt-4o-mini",
verbose=False
)
# Use prompt as input if input_text not provided
if not input_text:
input_text = prompt
elif agent_file:
# Load from agents.yaml
try:
from praisonai.agents_generator import AgentsGenerator
generator = AgentsGenerator(agent_file)
agents = generator.generate_agents()
if not agents:
return {"error": "No agents found in configuration"}
agent = agents[0] if isinstance(agents, list) else agents
except Exception as e:
return {"error": f"Failed to load agents from {agent_file}: {e}"}
else:
return {"error": "Either --agent or --prompt must be provided"}
evaluator = AccuracyEvaluator(
agent=agent,
@@ -331,8 +357,16 @@ def handle_eval_command(args) -> None:
handler = EvalHandler(verbose=getattr(args, 'verbose', False))
eval_type = getattr(args, 'eval_type', 'accuracy')
agent_file = getattr(args, 'agent', 'agents.yaml')
agent_file = getattr(args, 'agent', None)
output_file = getattr(args, 'output', None)
prompt = getattr(args, 'prompt', None)
llm = getattr(args, 'llm', None)
# If no agent file and no prompt, check if agents.yaml exists
if not agent_file and not prompt:
import os
if os.path.exists('agents.yaml'):
agent_file = 'agents.yaml'
if eval_type == 'accuracy':
result = handler.run_accuracy(
@@ -341,7 +375,9 @@ def handle_eval_command(args) -> None:
expected_output=getattr(args, 'expected', ''),
iterations=getattr(args, 'iterations', 1),
model=getattr(args, 'model', None),
output_file=output_file
output_file=output_file,
prompt=prompt,
llm=llm
)
elif eval_type == 'performance':
result = handler.run_performance(
@@ -410,8 +446,10 @@ def add_eval_parser(subparsers) -> None:
'accuracy',
help='Run accuracy evaluation'
)
accuracy_parser.add_argument('--agent', '-a', default='agents.yaml', help='Agent config file')
accuracy_parser.add_argument('--input', '-i', required=True, help='Input text')
accuracy_parser.add_argument('--agent', '-a', help='Agent config file (optional if --prompt used)')
accuracy_parser.add_argument('--prompt', '-p', help='Direct prompt (alternative to --agent)')
accuracy_parser.add_argument('--llm', help='LLM model for agent (when using --prompt)')
accuracy_parser.add_argument('--input', '-i', help='Input text (defaults to --prompt if not provided)')
accuracy_parser.add_argument('--expected', '-e', required=True, help='Expected output')
accuracy_parser.add_argument('--iterations', '-n', type=int, default=1, help='Number of iterations')
accuracy_parser.add_argument('--model', '-m', help='Judge model')
@@ -3,12 +3,21 @@ Fast Context Handler for CLI.
Provides codebase search capability using FastContext.
Usage: praisonai "Find authentication code" --fast-context ./src
Fast Context Strategy:
1. Extract search keywords from natural language query using LLM
2. Provide folder tree context to help understand codebase structure
3. Use both file name search AND content search (ripgrep-style)
4. Return relevant code snippets for context
"""
import os
import logging
from typing import Any, Dict, Tuple, List
from .base import FlagHandler
logger = logging.getLogger(__name__)
class FastContextHandler(FlagHandler):
"""
@@ -61,14 +70,172 @@ class FastContextHandler(FlagHandler):
return True, ""
def _get_folder_tree(self, path: str, max_depth: int = 2) -> str:
"""Get folder tree structure for context."""
tree_lines = []
path = os.path.abspath(path)
ignore_dirs = {'.git', '__pycache__', 'node_modules', '.venv', 'venv', '.tox', 'dist', 'build', '.egg-info'}
ignore_exts = {'.pyc', '.pyo', '.so', '.o', '.a', '.dylib'}
def walk_tree(current_path, prefix="", depth=0):
if depth > max_depth:
return
try:
entries = sorted(os.listdir(current_path))
except PermissionError:
return
dirs = []
files = []
for entry in entries:
full_path = os.path.join(current_path, entry)
if os.path.isdir(full_path):
if entry not in ignore_dirs and not entry.startswith('.'):
dirs.append(entry)
else:
ext = os.path.splitext(entry)[1]
if ext not in ignore_exts and not entry.startswith('.'):
files.append(entry)
for i, d in enumerate(dirs):
is_last = (i == len(dirs) - 1) and not files
tree_lines.append(f"{prefix}{'└── ' if is_last else '├── '}{d}/")
walk_tree(os.path.join(current_path, d),
prefix + (' ' if is_last else '│ '), depth + 1)
for i, f in enumerate(files[:10]): # Limit files shown
is_last = i == len(files[:10]) - 1
tree_lines.append(f"{prefix}{'└── ' if is_last else '├── '}{f}")
if len(files) > 10:
tree_lines.append(f"{prefix} ... and {len(files) - 10} more files")
tree_lines.append(os.path.basename(path) + "/")
walk_tree(path)
return "\n".join(tree_lines[:50]) # Limit total lines
def _extract_keywords(self, query: str, folder_tree: str) -> List[str]:
"""Extract search keywords from natural language query using LLM."""
try:
from praisonaiagents import Agent
extractor = Agent(
name="KeywordExtractor",
role="Search Query Analyzer",
goal="Extract file search keywords from natural language",
backstory="Expert at understanding code search queries",
llm="gpt-4o-mini",
verbose=False
)
prompt = f"""Given this folder structure:
{folder_tree}
And this search query: "{query}"
Extract 1-5 specific keywords/patterns to search for in file names and content.
Return ONLY the keywords, one per line, no explanations.
Focus on: file names, function names, class names, variable names that might match.
If the query mentions a folder name that exists, include files from that folder."""
response = extractor.chat(prompt, stream=False)
keywords = [k.strip() for k in str(response).strip().split('\n') if k.strip()]
logger.debug(f"Extracted keywords: {keywords}")
return keywords[:5] # Limit to 5 keywords
except Exception as e:
logger.debug(f"Keyword extraction failed: {e}")
# Fallback: extract words from query
return [w for w in query.split() if len(w) > 2][:3]
def _search_files_by_name(self, path: str, keywords: List[str]) -> List[Dict[str, Any]]:
"""Search for files matching keywords in their names."""
matches = []
path = os.path.abspath(path)
ignore_dirs = {'.git', '__pycache__', 'node_modules', '.venv', 'venv'}
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if d not in ignore_dirs]
for filename in files:
filepath = os.path.join(root, filename)
rel_path = os.path.relpath(filepath, path)
# Check if any keyword matches filename or path
for kw in keywords:
kw_lower = kw.lower()
if kw_lower in filename.lower() or kw_lower in rel_path.lower():
matches.append({
'file': rel_path,
'lines': [(1, 50)], # First 50 lines
'relevance': 1.0,
'match_type': 'filename'
})
break
return matches[:20] # Limit results
def _search_files_by_content(self, path: str, keywords: List[str]) -> List[Dict[str, Any]]:
"""Search for files containing keywords in content."""
matches = []
path = os.path.abspath(path)
ignore_dirs = {'.git', '__pycache__', 'node_modules', '.venv', 'venv'}
code_exts = {'.py', '.js', '.ts', '.jsx', '.tsx', '.java', '.go', '.rs', '.rb', '.sh', '.yaml', '.yml', '.json', '.md', '.txt'}
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if d not in ignore_dirs]
for filename in files:
ext = os.path.splitext(filename)[1]
if ext not in code_exts:
continue
filepath = os.path.join(root, filename)
rel_path = os.path.relpath(filepath, path)
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read(50000) # Read first 50KB
content_lower = content.lower()
for kw in keywords:
if kw.lower() in content_lower:
# Find line numbers with matches
lines = content.split('\n')
match_lines = []
for i, line in enumerate(lines[:200], 1): # Check first 200 lines
if kw.lower() in line.lower():
match_lines.append((max(1, i-2), min(len(lines), i+2)))
if match_lines:
matches.append({
'file': rel_path,
'lines': match_lines[:5], # Limit line ranges
'relevance': 0.8,
'match_type': 'content'
})
break
except Exception:
continue
return matches[:15] # Limit results
def search_context(self, query: str, path: str, **kwargs) -> List[Dict[str, Any]]:
"""
Search for relevant context in the codebase.
Uses a smart multi-step approach:
1. Get folder tree structure
2. Use LLM to extract search keywords from query
3. Search by filename AND content
4. Return combined results
Args:
query: Search query
query: Natural language search query
path: Path to search
**kwargs: Additional search options
- use_llm_keywords: Use LLM to extract keywords (default: True)
Returns:
List of matching results
@@ -84,32 +251,50 @@ class FastContextHandler(FlagHandler):
return []
try:
from praisonaiagents import FastContext
use_llm_keywords = kwargs.get('use_llm_keywords', True)
fc = FastContext(
path=path,
model=kwargs.get('model', 'gpt-4o-mini'),
max_turns=kwargs.get('max_turns', 4),
parallelism=kwargs.get('parallelism', 8),
timeout=kwargs.get('timeout', 30.0)
)
# Step 1: Get folder tree for context
logger.debug(f"Getting folder tree for: {path}")
folder_tree = self._get_folder_tree(path)
logger.debug(f"Folder tree:\n{folder_tree}")
result = fc.search(query)
# Step 2: Extract keywords
if use_llm_keywords:
logger.debug(f"Extracting keywords from query: {query}")
keywords = self._extract_keywords(query, folder_tree)
else:
keywords = [w for w in query.split() if len(w) > 2][:3]
# Convert to list of dicts
logger.debug(f"Search keywords: {keywords}")
if not keywords:
self.print_status("Could not extract search keywords", "warning")
return []
# Step 3: Search by filename
logger.debug("Searching by filename...")
filename_matches = self._search_files_by_name(path, keywords)
logger.debug(f"Found {len(filename_matches)} filename matches")
# Step 4: Search by content
logger.debug("Searching by content...")
content_matches = self._search_files_by_content(path, keywords)
logger.debug(f"Found {len(content_matches)} content matches")
# Combine and deduplicate
seen_files = set()
matches = []
if hasattr(result, 'files'):
for file_match in result.files:
matches.append({
'file': file_match.path,
'lines': [(lr.start, lr.end) for lr in file_match.line_ranges],
'relevance': getattr(file_match, 'relevance', 1.0)
})
for m in filename_matches + content_matches:
if m['file'] not in seen_files:
seen_files.add(m['file'])
matches.append(m)
self.print_status(f"🔍 Found {len(matches)} relevant files", "success")
return matches
except Exception as e:
logger.error(f"FastContext search error: {e}")
self.log(f"FastContext search error: {e}", "error")
return []
@@ -75,7 +75,7 @@ class GuardrailHandler(FlagHandler):
Args:
description: Natural language description of validation criteria
llm: Optional LLM instance for validation
llm: Optional LLM instance for validation (defaults to gpt-4o-mini)
Returns:
LLMGuardrail instance or None if unavailable
@@ -87,6 +87,12 @@ class GuardrailHandler(FlagHandler):
)
return None
# Provide default LLM if none specified
if llm is None:
import os
# Try to get from environment or use default
llm = os.environ.get('OPENAI_MODEL_NAME', 'gpt-4o-mini')
return LLMGuardrail(description=description, llm=llm)
def validate_output(self, output: str, description: str, llm: Any = None) -> Tuple[bool, str]:
@@ -95,9 +95,9 @@ class HandoffHandler(FlagHandler):
for i, agent in enumerate(agents[:-1]):
next_agent = agents[i + 1]
handoff = Handoff(
target=next_agent,
name=f"handoff_to_{agent_names[i + 1]}",
description=f"Hand off task to {agent_names[i + 1]} for further processing"
agent=next_agent,
tool_name_override=f"handoff_to_{agent_names[i + 1]}",
tool_description_override=f"Hand off task to {agent_names[i + 1]} for further processing"
)
# Add handoff to agent
+39 -27
View File
@@ -313,6 +313,18 @@ class PraisonAI:
elif hasattr(args, 'direct_prompt') and args.direct_prompt:
# Only handle direct prompt if agent_file wasn't explicitly set in constructor
if original_agent_file == "agents.yaml": # Default value, so safe to use direct prompt
# Handle --compare flag for CLI mode comparison
if hasattr(args, 'compare') and args.compare:
from .features.compare import CompareHandler
handler = CompareHandler(verbose=getattr(args, 'verbose', False))
result = handler.execute(
args.direct_prompt,
args.compare,
model=getattr(args, 'llm', None),
output_path=getattr(args, 'compare_output', None)
)
return result
# Combine direct prompt with any available inputs (stdin and/or file)
prompt_parts = [args.direct_prompt]
if stdin_input:
@@ -4741,16 +4753,14 @@ Provide a concise summary (max 200 words):"""
def _process_interactive_prompt_async(self, prompt, tools_list, console, session_state=None):
"""
Process a prompt asynchronously with live status display.
Process a prompt asynchronously with non-blocking status display.
Shows real-time status (tools, commands) while processing in background.
Shows status updates without blocking user input.
Allows user to queue more messages while processing.
"""
import threading
import time
from rich.live import Live
from rich.panel import Panel
from rich.text import Text
import sys
from praisonai.cli.features.message_queue import ProcessingState
state_manager = session_state['state_manager']
@@ -4762,8 +4772,11 @@ Provide a concise summary (max 200 words):"""
live_status.clear()
live_status.update_status("Thinking...")
# Print initial status (non-blocking - user can still type)
console.print(f"[dim cyan]⏳ {live_status.current_status}[/dim cyan]")
# Result container for thread communication
result_container = {'response': None, 'error': None, 'done': False}
result_container = {'response': None, 'error': None, 'done': False, 'last_status': ''}
def process_in_background():
"""Run agent processing in background thread."""
@@ -4809,37 +4822,36 @@ Provide a concise summary (max 200 words):"""
bg_thread = threading.Thread(target=process_in_background, daemon=True)
bg_thread.start()
# Show live status while processing
try:
with Live(console=console, refresh_per_second=4, transient=True) as live:
while not result_container['done']:
# Build status display
status_text = Text()
status_text.append("⏳ ", style="cyan")
status_text.append(live_status.current_status or "Processing...", style="dim")
# Show queued messages if any
queue_count = message_queue.count
if queue_count > 0:
status_text.append(f"\n📋 Queued: {queue_count}", style="dim yellow")
live.update(Panel(status_text, border_style="dim"))
time.sleep(0.1)
except KeyboardInterrupt:
console.print("\n[dim]Processing interrupted[/dim]")
# Wait for completion with periodic status updates (non-blocking style)
start_time = time.time()
last_status_update = ""
while not result_container['done']:
time.sleep(0.2)
current_status = live_status.current_status
# Only print status if it changed
if current_status != last_status_update:
# Use carriage return to update in place
elapsed = time.time() - start_time
queue_count = message_queue.count
queue_info = f" | 📋 Queued: {queue_count}" if queue_count > 0 else ""
sys.stdout.write(f"\r[dim cyan]⏳ {current_status} ({elapsed:.1f}s){queue_info}[/dim cyan]")
sys.stdout.flush()
last_status_update = current_status
# Clear the status line
sys.stdout.write("\r" + " " * 80 + "\r")
sys.stdout.flush()
# Wait for thread to finish
bg_thread.join(timeout=1.0)
# Handle result
if result_container['error']:
console.print(f"\n[red]Error: {result_container['error']}[/red]")
console.print(f"[red]Error: {result_container['error']}[/red]")
elif result_container['response']:
# Print response with streaming effect
console.print()
response_str = result_container['response']
words = response_str.split()
import sys
for i, word in enumerate(words):
console.print(word + " ", end="")
sys.stdout.flush()
+1 -1
View File
@@ -57,7 +57,7 @@ class CloudDeployer:
file.write("FROM python:3.11-slim\n")
file.write("WORKDIR /app\n")
file.write("COPY . .\n")
file.write("RUN pip install flask praisonai==2.3.76 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.78 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.3.76"
__version__ = "2.3.78"