Compare commits

...
2 Commits
Author SHA1 Message Date
Mervin Praison 99ab421ce2 Release v2.3.54 2025-12-19 16:04:43 +00:00
Mervin Praison c1f334905e Release v2.3.53 2025-12-19 15:38:30 +00:00
16 changed files with 1866 additions and 74 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.52" \
"praisonai>=2.3.54" \
"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.52" \
"praisonai>=2.3.54" \
"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.52" \
"praisonai>=2.3.54" \
"praisonai[ui]" \
"praisonai[crewai]"
@@ -0,0 +1,33 @@
# PraisonAI CLI - Benchmark Results
**Generated:** 2025-12-19 16:04:40
## Import Times
| Module | Time (ms) | Target | Status |
|--------|-----------|--------|--------|
| praisonai | 4.11 | <10 | ✅ |
| message_queue | 6.87 | <50 | ✅ |
| at_mentions | 0.58 | <50 | ✅ |
| profiler | 0.90 | <10 | ✅ |
## Instantiation Times
| Class | Time (μs) | Target | Status |
|-------|-----------|--------|--------|
| MessageQueue | 0.26 | <100 | ✅ |
| StateManager | 0.30 | <100 | ✅ |
| FileSearchService | 8.42 | <100 | ✅ |
## Lazy Loading
| Operation | Time (ms) |
|-----------|----------|
| PraisonAI lazy load | 3886.47 |
## How to Reproduce
```bash
cd praisonai
python benchmarks/simple_benchmark.py
```
@@ -0,0 +1,231 @@
#!/usr/bin/env python3
"""
PraisonAI CLI - Performance Benchmark
Measures import times and module loading performance.
Usage:
python benchmarks/simple_benchmark.py
python benchmarks/simple_benchmark.py --no-save
"""
import time
import sys
import os
import argparse
from datetime import datetime
ITERATIONS = 100
def measure_import(module_name: str) -> float:
"""Measure import time in milliseconds."""
# Clear from cache if present
modules_to_remove = [k for k in sys.modules.keys() if k.startswith(module_name)]
for mod in modules_to_remove:
del sys.modules[mod]
start = time.perf_counter()
__import__(module_name)
return (time.perf_counter() - start) * 1000
def measure_instantiation(create_fn, iterations=ITERATIONS) -> float:
"""Measure average instantiation time in microseconds."""
times = []
for _ in range(iterations):
start = time.perf_counter()
create_fn()
times.append((time.perf_counter() - start) * 1_000_000)
return sum(times) / len(times)
def run_benchmark():
"""Run the benchmark."""
results = {}
print('=' * 60)
print('PraisonAI CLI - Performance Benchmark')
print('=' * 60)
print(f'\nIterations: {ITERATIONS}')
print('Metrics: Import time (ms), Instantiation time (μs)\n')
# 1. Package import time
print("1. Testing package imports...")
# Clear cache
modules_to_remove = [k for k in sys.modules.keys() if 'praisonai' in k]
for mod in modules_to_remove:
del sys.modules[mod]
start = time.perf_counter()
import praisonai
results['praisonai_import_ms'] = (time.perf_counter() - start) * 1000
print(f" import praisonai: {results['praisonai_import_ms']:.2f}ms")
# 2. CLI features import
print("\n2. Testing CLI features imports...")
start = time.perf_counter()
from praisonai.cli.features.message_queue import MessageQueue
results['message_queue_import_ms'] = (time.perf_counter() - start) * 1000
print(f" message_queue: {results['message_queue_import_ms']:.2f}ms")
start = time.perf_counter()
from praisonai.cli.features.at_mentions import CombinedCompleter
results['at_mentions_import_ms'] = (time.perf_counter() - start) * 1000
print(f" at_mentions: {results['at_mentions_import_ms']:.2f}ms")
# 3. Profiler import (should be fast, standalone)
print("\n3. Testing profiler import...")
start = time.perf_counter()
from praisonai.profiler import Profiler
results['profiler_import_ms'] = (time.perf_counter() - start) * 1000
print(f" profiler: {results['profiler_import_ms']:.2f}ms")
# 4. Instantiation benchmarks
print(f"\n4. Testing instantiation ({ITERATIONS} iterations)...")
from praisonai.cli.features.message_queue import MessageQueue, StateManager
from praisonai.cli.features.at_mentions import FileSearchService
results['MessageQueue_us'] = measure_instantiation(lambda: MessageQueue())
print(f" MessageQueue: {results['MessageQueue_us']:.2f}μs")
results['StateManager_us'] = measure_instantiation(lambda: StateManager())
print(f" StateManager: {results['StateManager_us']:.2f}μs")
results['FileSearchService_us'] = measure_instantiation(
lambda: FileSearchService(root_dir=".")
)
print(f" FileSearchService: {results['FileSearchService_us']:.2f}μs")
# 5. PraisonAI class (lazy load)
print("\n5. Testing PraisonAI class access (lazy load)...")
start = time.perf_counter()
PraisonAI = praisonai.PraisonAI
results['PraisonAI_lazy_ms'] = (time.perf_counter() - start) * 1000
print(f" PraisonAI lazy load: {results['PraisonAI_lazy_ms']:.2f}ms")
return results
def print_summary(results: dict):
"""Print benchmark summary."""
print('\n' + '=' * 60)
print('SUMMARY')
print('=' * 60)
print('\nImport Times (target: <50ms for features, <10ms for package):')
print(f" {'Module':<25} {'Time':>10} {'Status':>10}")
print(f" {'-'*25} {'-'*10} {'-'*10}")
import_checks = [
('praisonai', results['praisonai_import_ms'], 10),
('message_queue', results['message_queue_import_ms'], 50),
('at_mentions', results['at_mentions_import_ms'], 50),
('profiler', results['profiler_import_ms'], 10),
]
all_pass = True
for name, time_ms, threshold in import_checks:
status = '✅ PASS' if time_ms < threshold else '❌ SLOW'
if time_ms >= threshold:
all_pass = False
print(f" {name:<25} {time_ms:>8.2f}ms {status:>10}")
print('\nInstantiation Times (target: <100μs):')
print(f" {'Class':<25} {'Time':>10} {'Status':>10}")
print(f" {'-'*25} {'-'*10} {'-'*10}")
inst_checks = [
('MessageQueue', results['MessageQueue_us'], 100),
('StateManager', results['StateManager_us'], 100),
('FileSearchService', results['FileSearchService_us'], 100),
]
for name, time_us, threshold in inst_checks:
status = '✅ PASS' if time_us < threshold else '❌ SLOW'
if time_us >= threshold:
all_pass = False
print(f" {name:<25} {time_us:>8.2f}μs {status:>10}")
print('\n' + '=' * 60)
if all_pass:
print('✅ All benchmarks PASSED')
else:
print('❌ Some benchmarks FAILED')
print('=' * 60)
return all_pass
def save_results(results: dict):
"""Save benchmark results to markdown file."""
filepath = os.path.join(os.path.dirname(__file__), 'BENCHMARK_RESULTS.md')
with open(filepath, 'w') as f:
f.write('# PraisonAI CLI - Benchmark Results\n\n')
f.write(f'**Generated:** {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}\n\n')
f.write('## Import Times\n\n')
f.write('| Module | Time (ms) | Target | Status |\n')
f.write('|--------|-----------|--------|--------|\n')
import_data = [
('praisonai', results['praisonai_import_ms'], 10),
('message_queue', results['message_queue_import_ms'], 50),
('at_mentions', results['at_mentions_import_ms'], 50),
('profiler', results['profiler_import_ms'], 10),
]
for name, time_ms, threshold in import_data:
status = '✅' if time_ms < threshold else '❌'
f.write(f'| {name} | {time_ms:.2f} | <{threshold} | {status} |\n')
f.write('\n## Instantiation Times\n\n')
f.write('| Class | Time (μs) | Target | Status |\n')
f.write('|-------|-----------|--------|--------|\n')
inst_data = [
('MessageQueue', results['MessageQueue_us'], 100),
('StateManager', results['StateManager_us'], 100),
('FileSearchService', results['FileSearchService_us'], 100),
]
for name, time_us, threshold in inst_data:
status = '✅' if time_us < threshold else '❌'
f.write(f'| {name} | {time_us:.2f} | <{threshold} | {status} |\n')
f.write('\n## Lazy Loading\n\n')
f.write(f'| Operation | Time (ms) |\n')
f.write(f'|-----------|----------|\n')
f.write(f'| PraisonAI lazy load | {results["PraisonAI_lazy_ms"]:.2f} |\n')
f.write('\n## How to Reproduce\n\n')
f.write('```bash\n')
f.write('cd praisonai\n')
f.write('python benchmarks/simple_benchmark.py\n')
f.write('```\n')
print(f'\nResults saved to: {filepath}')
return filepath
def main():
parser = argparse.ArgumentParser(description='PraisonAI CLI Performance Benchmark')
parser.add_argument('--no-save', action='store_true', help='Do not save results to file')
args = parser.parse_args()
results = run_benchmark()
all_pass = print_summary(results)
if not args.no_save:
save_results(results)
return 0 if all_pass else 1
if __name__ == '__main__':
sys.exit(main())
+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.52.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.52.tar.gz | shasum -a 256`.split.first
url "https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.54.tar.gz"
sha256 `curl -sL https://github.com/MervinPraison/PraisonAI/archive/refs/tags/v2.3.54.tar.gz | shasum -a 256`.split.first
license "MIT"
depends_on "[email protected]"
+19 -20
View File
@@ -6,30 +6,29 @@ logging.getLogger('crewai.cli.config').setLevel(logging.ERROR)
import os
os.environ["OTEL_SDK_DISABLED"] = "true"
os.environ["EC_TELEMETRY"] = "false"
from .cli import PraisonAI
# Version is lightweight, import directly
from .version import __version__
# Re-export all classes from praisonaiagents to enable:
# from praisonai import Agent, Task, PraisonAIAgents
try:
import praisonaiagents
# Import all symbols from praisonaiagents using * import
from praisonaiagents import *
except ImportError:
# If praisonaiagents is not available, these imports will fail gracefully
pass
# Define __all__ to include both PraisonAI core classes and praisonaiagents exports
# Define __all__ for lazy loading
__all__ = [
# Core PraisonAI classes
'PraisonAI',
'__version__',
]
# Dynamically extend __all__ with praisonaiagents exports
try:
import praisonaiagents
__all__.extend(praisonaiagents.__all__)
except (ImportError, AttributeError):
# If praisonaiagents is not available or doesn't have __all__, fail gracefully
pass
# Lazy loading for heavy imports
def __getattr__(name):
"""Lazy load heavy modules to improve import time."""
if name == 'PraisonAI':
from .cli import PraisonAI
return PraisonAI
# Try praisonaiagents exports
try:
import praisonaiagents
if hasattr(praisonaiagents, name):
return getattr(praisonaiagents, name)
except ImportError:
pass
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+9 -2
View File
@@ -8,12 +8,19 @@ Structure:
- features/: Feature handlers for CLI flags and commands
"""
from .main import PraisonAI
__all__ = ["PraisonAI"]
def __getattr__(name):
"""Lazy load PraisonAI to avoid slow imports."""
if name == "PraisonAI":
from .main import PraisonAI
return PraisonAI
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def main():
"""CLI entry point function."""
from .main import PraisonAI
praison_ai = PraisonAI()
praison_ai.main()
@@ -26,6 +26,7 @@ Features:
- autonomy_mode: Autonomy levels (suggest, auto-edit, full-auto)
- cost_tracker: Real-time cost and token tracking
- message_queue: Message queuing while agent is processing
- at_mentions: @ mention autocomplete for files and directories
"""
# Type hints available for IDE support
@@ -58,6 +59,10 @@ __all__ = [
'GitIntegrationHandler',
'SandboxExecutorHandler',
'MessageQueueHandler',
# @ mention autocomplete
'AtMentionCompleter',
'CombinedCompleter',
'FileSearchService',
]
def __getattr__(name):
@@ -138,4 +143,14 @@ def __getattr__(name):
elif name == 'MessageQueueHandler':
from .message_queue import MessageQueueHandler
return MessageQueueHandler
# @ mention autocomplete
elif name == 'AtMentionCompleter':
from .at_mentions import AtMentionCompleter
return AtMentionCompleter
elif name == 'CombinedCompleter':
from .at_mentions import CombinedCompleter
return CombinedCompleter
elif name == 'FileSearchService':
from .at_mentions import FileSearchService
return FileSearchService
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -0,0 +1,416 @@
"""
@ Mention Autocomplete for PraisonAI CLI.
Provides file/directory autocomplete when user types @.
Uses only stdlib imports for zero performance impact.
Features:
- Detect @ trigger in input
- Fuzzy search files/directories
- Cache results for performance
- Integrate with prompt_toolkit
"""
import os
import time
import fnmatch
from dataclasses import dataclass
from typing import List, Optional, Dict, Tuple
# ============================================================================
# Data Classes
# ============================================================================
@dataclass
class AtMentionContext:
"""Context for an @ mention in user input."""
is_active: bool
query: str
start_pos: int
mention_type: str = "file" # file, directory, web, etc.
@dataclass
class FileSuggestion:
"""A file or directory suggestion."""
path: str
file_type: str # "file" or "directory"
score: int = 100
display: str = ""
def __post_init__(self):
if not self.display:
self.display = self.path
# ============================================================================
# @ Mention Detection
# ============================================================================
def detect_at_mention(text: str, cursor_pos: int) -> Optional[AtMentionContext]:
"""
Detect if cursor is within an @ mention context.
Scans backward from cursor to find @ symbol.
Returns None if not in @ context.
Args:
text: Full input text
cursor_pos: Current cursor position
Returns:
AtMentionContext if in @ mention, None otherwise
"""
if cursor_pos <= 0 or cursor_pos > len(text):
return None
# Scan backward from cursor to find @
at_pos = -1
for i in range(cursor_pos - 1, -1, -1):
char = text[i]
# Found @
if char == '@':
at_pos = i
break
# Space/newline breaks @ context
if char in ' \t\n\r':
return None
if at_pos == -1:
return None
# Extract query between @ and cursor
query = text[at_pos + 1:cursor_pos]
return AtMentionContext(
is_active=True,
query=query,
start_pos=at_pos
)
# ============================================================================
# File Search Service
# ============================================================================
class FileSearchService:
"""
Service for searching files in a directory.
Features:
- Walk directory tree
- Fuzzy match file paths
- Cache results with TTL
- Respect .gitignore (basic)
"""
def __init__(
self,
root_dir: str,
cache_ttl: int = 30,
max_depth: int = 5,
ignore_patterns: Optional[List[str]] = None
):
self._root = os.path.abspath(root_dir)
self._cache_ttl = cache_ttl
self._max_depth = max_depth
self._ignore_patterns = ignore_patterns or [
'.git', '__pycache__', 'node_modules', '.venv', 'venv',
'*.pyc', '*.pyo', '.DS_Store', '*.egg-info'
]
# Cache: query -> (results, timestamp)
self._cache: Dict[str, Tuple[List[FileSuggestion], float]] = {}
# File list cache
self._file_list: Optional[List[str]] = None
self._file_list_time: float = 0
def _should_ignore(self, name: str) -> bool:
"""Check if file/dir should be ignored."""
for pattern in self._ignore_patterns:
if fnmatch.fnmatch(name, pattern):
return True
return False
def _get_file_list(self) -> List[str]:
"""Get list of all files (cached)."""
now = time.time()
# Return cached if fresh
if self._file_list is not None and (now - self._file_list_time) < self._cache_ttl:
return self._file_list
files = []
try:
for root, dirs, filenames in os.walk(self._root):
# Calculate depth
rel_root = os.path.relpath(root, self._root)
depth = 0 if rel_root == '.' else rel_root.count(os.sep) + 1
if depth > self._max_depth:
dirs.clear() # Don't descend further
continue
# Filter ignored directories
dirs[:] = [d for d in dirs if not self._should_ignore(d)]
# Add directories
for d in dirs:
rel_path = os.path.relpath(os.path.join(root, d), self._root)
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)
files.append(rel_path)
except OSError:
pass
self._file_list = files
self._file_list_time = now
return files
def _fuzzy_match(self, text: str, query: str) -> int:
"""
Simple fuzzy match scoring.
Returns score 0-100, higher is better.
"""
if not query:
return 50 # Default score for empty query
text_lower = text.lower()
query_lower = query.lower()
# Exact match
if text_lower == query_lower:
return 100
# Starts with
if text_lower.startswith(query_lower):
return 90
# Contains
if query_lower in text_lower:
return 80
# Fuzzy: all query chars appear in order
text_idx = 0
query_idx = 0
matches = 0
while text_idx < len(text_lower) and query_idx < len(query_lower):
if text_lower[text_idx] == query_lower[query_idx]:
matches += 1
query_idx += 1
text_idx += 1
if query_idx == len(query_lower):
# All query chars found
return 50 + int(30 * matches / len(text))
return 0
def search(self, query: str, max_results: int = 20) -> List[FileSuggestion]:
"""
Search for files matching query.
Args:
query: Search query (fuzzy matched)
max_results: Maximum results to return
Returns:
List of FileSuggestion sorted by score
"""
# Check cache
cache_key = f"{query}:{max_results}"
now = time.time()
if cache_key in self._cache:
results, cached_time = self._cache[cache_key]
if (now - cached_time) < self._cache_ttl:
return results
# Get all files
all_files = self._get_file_list()
# Score and filter
scored: List[Tuple[int, str]] = []
for path in all_files:
score = self._fuzzy_match(path, query)
if score > 0:
scored.append((score, path))
# Sort by score descending
scored.sort(key=lambda x: (-x[0], x[1]))
# Build results
results = []
for score, path in scored[:max_results]:
file_type = "directory" if path.endswith(os.sep) else "file"
results.append(FileSuggestion(
path=path,
file_type=file_type,
score=score
))
# Cache results
self._cache[cache_key] = (results, now)
return results
def clear_cache(self) -> None:
"""Clear all caches."""
self._cache.clear()
self._file_list = None
# ============================================================================
# Completers for prompt_toolkit
# ============================================================================
# Lazy import to avoid performance impact
_Completer = None
_Completion = None
def _get_prompt_toolkit():
"""Lazy import prompt_toolkit."""
global _Completer, _Completion
if _Completer is None:
from prompt_toolkit.completion import Completer, Completion
_Completer = Completer
_Completion = Completion
return _Completer, _Completion
class AtMentionCompleter:
"""
Completer for @ mentions.
Shows file suggestions when user types @.
Implements prompt_toolkit Completer interface.
"""
def __init__(self, root_dir: str):
self._file_service = FileSearchService(root_dir)
def get_completions(self, document, complete_event):
"""Get completions for @ mentions."""
_, Completion = _get_prompt_toolkit()
text = document.text_before_cursor
cursor_pos = len(text)
# Detect @ context
context = detect_at_mention(text, cursor_pos)
if context is None or not context.is_active:
return
# Search files
results = self._file_service.search(context.query, max_results=15)
# Yield completions
for suggestion in results:
# Icon based on type
icon = "📁 " if suggestion.file_type == "directory" else "📄 "
display = f"{icon}{suggestion.path}"
# Calculate replacement position
# Replace from @ to cursor
start_position = -(cursor_pos - context.start_pos)
yield Completion(
text=f"@{suggestion.path}",
start_position=start_position,
display=display
)
def create_combined_completer(commands: List[str], root_dir: str):
"""
Factory function to create a CombinedCompleter.
Returns a proper Completer subclass that handles both / and @.
"""
Completer, Completion = _get_prompt_toolkit()
file_service = FileSearchService(root_dir)
class CombinedCompleter(Completer):
"""
Combined completer for both / commands and @ mentions.
Properly inherits from prompt_toolkit Completer.
"""
def get_completions(self, document, complete_event):
"""Get completions for / or @."""
text = document.text_before_cursor.lstrip()
# Check for slash command
if text.startswith('/'):
cmd_text = text[1:].lower()
for cmd in commands:
if cmd.lower().startswith(cmd_text):
yield Completion(
f'/{cmd}',
start_position=-len(text),
display=f'/{cmd}'
)
return
# Check for @ mention
full_text = document.text_before_cursor
cursor_pos = len(full_text)
context = detect_at_mention(full_text, cursor_pos)
if context is None or not context.is_active:
return
# Search files
results = file_service.search(context.query, max_results=15)
# Yield completions
for suggestion in results:
icon = "📁 " if suggestion.file_type == "directory" else "📄 "
display = f"{icon}{suggestion.path}"
start_position = -(cursor_pos - context.start_pos)
yield Completion(
text=f"@{suggestion.path}",
start_position=start_position,
display=display
)
return CombinedCompleter()
# Backward compatible alias
class CombinedCompleter:
"""
Wrapper class for backward compatibility.
Use create_combined_completer() for proper Completer subclass.
"""
def __new__(cls, commands: List[str], root_dir: str):
return create_combined_completer(commands, root_dir)
# ============================================================================
# Exports
# ============================================================================
__all__ = [
'AtMentionContext',
'FileSuggestion',
'detect_at_mention',
'FileSearchService',
'AtMentionCompleter',
'CombinedCompleter',
]
+123 -45
View File
@@ -59,41 +59,14 @@ def _get_chainlit_run():
from chainlit.cli import chainlit_run
return chainlit_run
try:
import gradio as gr
GRADIO_AVAILABLE = True
except ImportError:
pass
try:
import praisonai.api.call as call_module
CALL_MODULE_AVAILABLE = True
except ImportError:
pass
try:
import crewai
CREWAI_AVAILABLE = True
except ImportError:
pass
try:
import autogen
AUTOGEN_AVAILABLE = True
except ImportError:
pass
try:
from praisonaiagents import Agent as PraisonAgent, Task as PraisonTask, PraisonAIAgents
PRAISONAI_AVAILABLE = True
except ImportError:
pass
try:
from unsloth import FastLanguageModel
TRAIN_AVAILABLE = True
except ImportError:
pass
# Use find_spec for fast availability checks (no actual import)
import importlib.util
GRADIO_AVAILABLE = importlib.util.find_spec("gradio") is not None
CALL_MODULE_AVAILABLE = importlib.util.find_spec("praisonai.api.call") is not None
CREWAI_AVAILABLE = importlib.util.find_spec("crewai") is not None
AUTOGEN_AVAILABLE = importlib.util.find_spec("autogen") is not None
PRAISONAI_AVAILABLE = importlib.util.find_spec("praisonaiagents") is not None
TRAIN_AVAILABLE = importlib.util.find_spec("unsloth") is not None
logging.basicConfig(level=os.environ.get('LOGLEVEL', 'INFO'), format='%(asctime)s - %(levelname)s - %(message)s')
logging.getLogger('alembic').setLevel(logging.ERROR)
@@ -1532,6 +1505,7 @@ class PraisonAI:
args: Parsed command line arguments
"""
try:
from praisonaiagents import Agent as PraisonAgent
from praisonaiagents.memory import WorkflowManager
from rich import print
from rich.table import Table
@@ -2919,6 +2893,8 @@ Provide ONLY the commit message, no explanations."""
prompt = f"{mention_context}# Task:\n{prompt}"
if PRAISONAI_AVAILABLE:
from praisonaiagents import Agent as PraisonAgent
agent_config = {
"name": "DirectAgent",
"role": "Assistant",
@@ -2926,8 +2902,11 @@ Provide ONLY the commit message, no explanations."""
"backstory": "You are a helpful AI assistant"
}
# Suppress verbose output in interactive mode (no Agent Info, Task, Response panels)
if getattr(self, '_interactive_mode', False):
# Set verbose=False by default, only enable with --verbose flag
# This shows minimal "Generating..." status instead of full Agent Info panels
if hasattr(self, 'args') and getattr(self.args, 'verbose', False):
agent_config["verbose"] = True
else:
agent_config["verbose"] = False
# Add llm if specified
@@ -3112,7 +3091,13 @@ Provide ONLY the commit message, no explanations."""
flow.display_workflow_start("Direct Prompt", ["DirectAgent"])
agent = PraisonAgent(**agent_config)
result = agent.start(prompt)
# Run with minimal status display when verbose=False
is_verbose = agent_config.get("verbose", False)
if not is_verbose:
result = self._run_with_status_display(agent, prompt)
else:
result = agent.start(prompt)
# ===== POST-PROCESSING WITH NEW FEATURES =====
@@ -3236,6 +3221,98 @@ Now, {final_instruction.lower()}:"""
print("pip install praisonaiagents # For PraisonAIAgents\n")
sys.exit(1)
def _run_with_status_display(self, agent, prompt):
"""
Run agent with minimal status display (spinner + tool/handoff updates).
Shows:
- "Generating..." with spinner while processing
- Real-time tool call notifications
- Agent handoff notifications
"""
import threading
import time
from rich.console import Console
from rich.live import Live
from rich.text import Text
console = Console()
status_info = {
'status': 'Generating...',
'tool_calls': [],
'handoffs': [],
'done': False,
'result': None,
'error': None,
'start_time': time.time()
}
def build_status_display():
"""Build the status display text."""
elapsed = time.time() - status_info['start_time']
# Main status with spinner
text = Text()
text.append("⏳ ", style="cyan")
text.append(f"{status_info['status']} ", style="bold")
text.append(f"({elapsed:.1f}s)", style="dim")
# Show recent tool calls
if status_info['tool_calls']:
text.append("\n")
for tool in status_info['tool_calls'][-3:]: # Show last 3
text.append(f" ⚙ {tool}", style="dim yellow")
text.append("\n")
# Show handoffs
if status_info['handoffs']:
for handoff in status_info['handoffs'][-2:]: # Show last 2
text.append(f" → {handoff}", style="dim cyan")
text.append("\n")
return text
def run_agent():
"""Run the agent in background thread."""
try:
# Monkey-patch agent's execute_tool to capture tool calls
original_execute_tool = agent.execute_tool
def patched_execute_tool(tool_name, *args, **kwargs):
status_info['tool_calls'].append(tool_name)
status_info['status'] = f"Using {tool_name}..."
return original_execute_tool(tool_name, *args, **kwargs)
agent.execute_tool = patched_execute_tool
status_info['result'] = agent.start(prompt)
except Exception as e:
status_info['error'] = e
finally:
status_info['done'] = True
# Start agent in background thread
thread = threading.Thread(target=run_agent, daemon=True)
thread.start()
# Show live status while processing
try:
with Live(build_status_display(), console=console, refresh_per_second=4, transient=True) as live:
while not status_info['done']:
live.update(build_status_display())
time.sleep(0.1)
except KeyboardInterrupt:
console.print("\n[dim]Interrupted[/dim]")
return None
# Wait for thread to complete
thread.join(timeout=1.0)
# Handle result
if status_info['error']:
console.print(f"[red]Error: {status_info['error']}[/red]")
return None
return status_info['result']
def _handle_serve_command(self, args, unknown_args):
"""
Handle the serve command - start API server for agents.
@@ -3845,22 +3922,23 @@ Now, {final_instruction.lower()}:"""
prompt_session = None
try:
from prompt_toolkit import PromptSession
from prompt_toolkit.completion import WordCompleter
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
from prompt_toolkit.history import InMemoryHistory
from praisonai.cli.features.at_mentions import CombinedCompleter
# Create completer for slash commands
# Create combined completer for / commands and @ mentions
commands = ['help', 'exit', 'quit', 'clear', 'tools', 'profile', 'model', 'stats', 'compact', 'undo', 'queue', 'q']
slash_completer = WordCompleter(
[f'/{cmd}' for cmd in commands],
ignore_case=True
combined_completer = CombinedCompleter(
commands=commands,
root_dir=os.getcwd()
)
prompt_session = PromptSession(
message="❯ ",
completer=slash_completer,
completer=combined_completer,
auto_suggest=AutoSuggestFromHistory(),
history=InMemoryHistory()
history=InMemoryHistory(),
complete_while_typing=True
)
except ImportError:
pass # Fall back to simple input
+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.52 gunicorn markdown\n")
file.write("RUN pip install flask praisonai==2.3.54 gunicorn markdown\n")
file.write("EXPOSE 8080\n")
file.write('CMD ["gunicorn", "-b", "0.0.0.0:8080", "api:app"]\n')
+465
View File
@@ -0,0 +1,465 @@
"""
PraisonAI Profiler Module
Standardized profiling for performance monitoring across praisonai and praisonai-agents.
Features:
- Import timing
- Function execution timing
- Flow tracking
- File/module usage tracking
- Memory usage (optional)
Usage:
from praisonai.profiler import Profiler, profile, profile_imports
# Profile a function
@profile
def my_function():
pass
# Profile a block
with Profiler.block("my_operation"):
do_something()
# Profile imports
with profile_imports():
import heavy_module
# Get report
Profiler.report()
"""
import time
import functools
import threading
import sys
import os
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Callable, Any
from contextlib import contextmanager
# ============================================================================
# Data Classes
# ============================================================================
@dataclass
class TimingRecord:
"""Record of a single timing measurement."""
name: str
duration_ms: float
category: str = "function"
file: str = ""
line: int = 0
timestamp: float = field(default_factory=time.time)
@dataclass
class ImportRecord:
"""Record of a module import."""
module: str
duration_ms: float
parent: str = ""
timestamp: float = field(default_factory=time.time)
@dataclass
class FlowRecord:
"""Record of execution flow."""
step: int
name: str
file: str
line: int
duration_ms: float
timestamp: float = field(default_factory=time.time)
# ============================================================================
# Profiler Class
# ============================================================================
class Profiler:
"""
Centralized profiler for performance monitoring.
Thread-safe singleton pattern for global access.
"""
_instance: Optional['Profiler'] = None
_lock = threading.Lock()
# Class-level storage
_timings: List[TimingRecord] = []
_imports: List[ImportRecord] = []
_flow: List[FlowRecord] = []
_enabled: bool = False
_flow_step: int = 0
_files_accessed: Dict[str, int] = {}
def __new__(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
def enable(cls) -> None:
"""Enable profiling."""
cls._enabled = True
@classmethod
def disable(cls) -> None:
"""Disable profiling."""
cls._enabled = False
@classmethod
def is_enabled(cls) -> bool:
"""Check if profiling is enabled."""
return cls._enabled or os.environ.get('PRAISONAI_PROFILE', '').lower() in ('1', 'true', 'yes')
@classmethod
def clear(cls) -> None:
"""Clear all profiling data."""
with cls._lock:
cls._timings.clear()
cls._imports.clear()
cls._flow.clear()
cls._flow_step = 0
cls._files_accessed.clear()
@classmethod
def record_timing(cls, name: str, duration_ms: float, category: str = "function",
file: str = "", line: int = 0) -> None:
"""Record a timing measurement."""
if not cls.is_enabled():
return
with cls._lock:
cls._timings.append(TimingRecord(
name=name,
duration_ms=duration_ms,
category=category,
file=file,
line=line
))
# Track file access
if file:
cls._files_accessed[file] = cls._files_accessed.get(file, 0) + 1
@classmethod
def record_import(cls, module: str, duration_ms: float, parent: str = "") -> None:
"""Record an import timing."""
if not cls.is_enabled():
return
with cls._lock:
cls._imports.append(ImportRecord(
module=module,
duration_ms=duration_ms,
parent=parent
))
@classmethod
def record_flow(cls, name: str, duration_ms: float, file: str = "", line: int = 0) -> None:
"""Record a flow step."""
if not cls.is_enabled():
return
with cls._lock:
cls._flow_step += 1
cls._flow.append(FlowRecord(
step=cls._flow_step,
name=name,
file=file,
line=line,
duration_ms=duration_ms
))
@classmethod
@contextmanager
def block(cls, name: str, category: str = "block"):
"""Context manager for profiling a block of code."""
start = time.time()
frame = sys._getframe(2) if hasattr(sys, '_getframe') else None
file = frame.f_code.co_filename if frame else ""
line = frame.f_lineno if frame else 0
try:
yield
finally:
duration_ms = (time.time() - start) * 1000
cls.record_timing(name, duration_ms, category, file, line)
cls.record_flow(name, duration_ms, file, line)
@classmethod
def get_timings(cls, category: Optional[str] = None) -> List[TimingRecord]:
"""Get timing records, optionally filtered by category."""
with cls._lock:
if category:
return [t for t in cls._timings if t.category == category]
return cls._timings.copy()
@classmethod
def get_imports(cls, min_duration_ms: float = 0) -> List[ImportRecord]:
"""Get import records, optionally filtered by minimum duration."""
with cls._lock:
if min_duration_ms > 0:
return [i for i in cls._imports if i.duration_ms >= min_duration_ms]
return cls._imports.copy()
@classmethod
def get_flow(cls) -> List[FlowRecord]:
"""Get flow records."""
with cls._lock:
return cls._flow.copy()
@classmethod
def get_files_accessed(cls) -> Dict[str, int]:
"""Get files accessed with counts."""
with cls._lock:
return cls._files_accessed.copy()
@classmethod
def get_summary(cls) -> Dict[str, Any]:
"""Get profiling summary."""
with cls._lock:
total_time = sum(t.duration_ms for t in cls._timings)
import_time = sum(i.duration_ms for i in cls._imports)
# Group by category
by_category: Dict[str, float] = {}
for t in cls._timings:
by_category[t.category] = by_category.get(t.category, 0) + t.duration_ms
# Top slowest
slowest = sorted(cls._timings, key=lambda x: x.duration_ms, reverse=True)[:10]
slowest_imports = sorted(cls._imports, key=lambda x: x.duration_ms, reverse=True)[:10]
return {
'total_time_ms': total_time,
'import_time_ms': import_time,
'timing_count': len(cls._timings),
'import_count': len(cls._imports),
'flow_steps': len(cls._flow),
'files_accessed': len(cls._files_accessed),
'by_category': by_category,
'slowest_operations': [(s.name, s.duration_ms) for s in slowest],
'slowest_imports': [(s.module, s.duration_ms) for s in slowest_imports],
}
@classmethod
def report(cls, output: str = "console") -> str:
"""Generate and output profiling report."""
summary = cls.get_summary()
lines = [
"=" * 60,
"PraisonAI Profiling Report",
"=" * 60,
"",
f"Total Time: {summary['total_time_ms']:.2f}ms",
f"Import Time: {summary['import_time_ms']:.2f}ms",
f"Timing Records: {summary['timing_count']}",
f"Import Records: {summary['import_count']}",
f"Flow Steps: {summary['flow_steps']}",
f"Files Accessed: {summary['files_accessed']}",
"",
"By Category:",
]
for cat, time_ms in summary['by_category'].items():
lines.append(f" {cat}: {time_ms:.2f}ms")
lines.extend([
"",
"Slowest Operations:",
])
for name, time_ms in summary['slowest_operations']:
lines.append(f" {name}: {time_ms:.2f}ms")
lines.extend([
"",
"Slowest Imports:",
])
for module, time_ms in summary['slowest_imports']:
lines.append(f" {module}: {time_ms:.2f}ms")
lines.append("=" * 60)
report_text = "\n".join(lines)
if output == "console":
print(report_text)
return report_text
# ============================================================================
# Decorators
# ============================================================================
def profile(func: Optional[Callable] = None, *, category: str = "function"):
"""
Decorator to profile a function.
Usage:
@profile
def my_function():
pass
@profile(category="api")
def api_call():
pass
"""
def decorator(fn: Callable) -> Callable:
@functools.wraps(fn)
def wrapper(*args, **kwargs):
if not Profiler.is_enabled():
return fn(*args, **kwargs)
start = time.time()
try:
return fn(*args, **kwargs)
finally:
duration_ms = (time.time() - start) * 1000
file = fn.__code__.co_filename if hasattr(fn, '__code__') else ""
line = fn.__code__.co_firstlineno if hasattr(fn, '__code__') else 0
Profiler.record_timing(fn.__name__, duration_ms, category, file, line)
Profiler.record_flow(fn.__name__, duration_ms, file, line)
return wrapper
if func is not None:
return decorator(func)
return decorator
def profile_async(func: Optional[Callable] = None, *, category: str = "async"):
"""
Decorator to profile an async function.
"""
def decorator(fn: Callable) -> Callable:
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
if not Profiler.is_enabled():
return await fn(*args, **kwargs)
start = time.time()
try:
return await fn(*args, **kwargs)
finally:
duration_ms = (time.time() - start) * 1000
file = fn.__code__.co_filename if hasattr(fn, '__code__') else ""
line = fn.__code__.co_firstlineno if hasattr(fn, '__code__') else 0
Profiler.record_timing(fn.__name__, duration_ms, category, file, line)
Profiler.record_flow(fn.__name__, duration_ms, file, line)
return wrapper
if func is not None:
return decorator(func)
return decorator
# ============================================================================
# Import Profiling
# ============================================================================
class ImportProfiler:
"""
Context manager to profile imports.
Usage:
with profile_imports() as profiler:
import heavy_module
print(profiler.get_imports())
"""
def __init__(self):
self._original_import = None
self._imports: List[ImportRecord] = []
def __enter__(self):
import builtins
self._original_import = builtins.__import__
def profiled_import(name, globals=None, locals=None, fromlist=(), level=0):
start = time.time()
try:
return self._original_import(name, globals, locals, fromlist, level)
finally:
duration_ms = (time.time() - start) * 1000
if duration_ms > 1: # Only record imports > 1ms
record = ImportRecord(module=name, duration_ms=duration_ms)
self._imports.append(record)
Profiler.record_import(name, duration_ms)
builtins.__import__ = profiled_import
return self
def __exit__(self, exc_type, exc_val, exc_tb):
import builtins
builtins.__import__ = self._original_import
return False
def get_imports(self, min_duration_ms: float = 0) -> List[ImportRecord]:
"""Get recorded imports."""
if min_duration_ms > 0:
return [i for i in self._imports if i.duration_ms >= min_duration_ms]
return self._imports.copy()
def get_slowest(self, n: int = 10) -> List[ImportRecord]:
"""Get N slowest imports."""
return sorted(self._imports, key=lambda x: x.duration_ms, reverse=True)[:n]
def profile_imports():
"""Create an import profiler context manager."""
return ImportProfiler()
# ============================================================================
# Quick Profiling Functions
# ============================================================================
def time_import(module_name: str) -> float:
"""
Time how long it takes to import a module.
Returns duration in milliseconds.
"""
start = time.time()
__import__(module_name)
return (time.time() - start) * 1000
def check_module_available(module_name: str) -> bool:
"""
Check if a module is available without importing it.
Uses importlib.util.find_spec which is fast.
"""
import importlib.util
return importlib.util.find_spec(module_name) is not None
# ============================================================================
# Exports
# ============================================================================
__all__ = [
'Profiler',
'profile',
'profile_async',
'profile_imports',
'ImportProfiler',
'time_import',
'check_module_available',
'TimingRecord',
'ImportRecord',
'FlowRecord',
]
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.3.52"
__version__ = "2.3.54"
@@ -0,0 +1,289 @@
"""
Unit tests for @ mention autocomplete feature.
Tests for:
1. AtMentionContext - Context detection
2. FileSearchService - File search with fuzzy matching
3. AtMentionCompleter - Autocomplete integration
"""
import os
import tempfile
import time
# ============================================================================
# 1. AtMentionContext Tests
# ============================================================================
class TestAtMentionContext:
"""Test @ mention context detection."""
def test_detect_at_start(self):
"""Should detect @ at start of input."""
from praisonai.cli.features.at_mentions import detect_at_mention
result = detect_at_mention("@", cursor_pos=1)
assert result is not None
assert result.is_active
assert result.query == ""
assert result.start_pos == 0
def test_detect_at_with_query(self):
"""Should detect @ with partial query."""
from praisonai.cli.features.at_mentions import detect_at_mention
result = detect_at_mention("@main", cursor_pos=5)
assert result is not None
assert result.is_active
assert result.query == "main"
assert result.start_pos == 0
def test_detect_at_mid_text(self):
"""Should detect @ in middle of text."""
from praisonai.cli.features.at_mentions import detect_at_mention
result = detect_at_mention("read @src/", cursor_pos=10)
assert result is not None
assert result.is_active
assert result.query == "src/"
assert result.start_pos == 5
def test_no_at_symbol(self):
"""Should return None when no @ present."""
from praisonai.cli.features.at_mentions import detect_at_mention
result = detect_at_mention("hello world", cursor_pos=11)
assert result is None
def test_at_after_space_breaks_context(self):
"""Should not detect @ if space after query."""
from praisonai.cli.features.at_mentions import detect_at_mention
result = detect_at_mention("@file.txt ", cursor_pos=10)
assert result is None # Space breaks the @ context
def test_cursor_before_at(self):
"""Should not detect if cursor is before @."""
from praisonai.cli.features.at_mentions import detect_at_mention
result = detect_at_mention("hello @file", cursor_pos=3)
assert result is None
# ============================================================================
# 2. FileSuggestion Tests
# ============================================================================
class TestFileSuggestion:
"""Test FileSuggestion dataclass."""
def test_create_file_suggestion(self):
"""Should create file suggestion."""
from praisonai.cli.features.at_mentions import FileSuggestion
suggestion = FileSuggestion(
path="src/main.py",
file_type="file",
score=100
)
assert suggestion.path == "src/main.py"
assert suggestion.file_type == "file"
assert suggestion.score == 100
def test_directory_suggestion(self):
"""Should create directory suggestion."""
from praisonai.cli.features.at_mentions import FileSuggestion
suggestion = FileSuggestion(
path="src/",
file_type="directory",
score=90
)
assert suggestion.file_type == "directory"
# ============================================================================
# 3. FileSearchService Tests
# ============================================================================
class TestFileSearchService:
"""Test FileSearchService for file discovery."""
def test_service_creation(self):
"""Should create service."""
from praisonai.cli.features.at_mentions import FileSearchService
service = FileSearchService(root_dir=".")
assert service is not None
def test_search_files(self):
"""Should search files in directory."""
from praisonai.cli.features.at_mentions import FileSearchService
with tempfile.TemporaryDirectory() as tmpdir:
# Create test files
open(os.path.join(tmpdir, "main.py"), "w").close()
open(os.path.join(tmpdir, "test.py"), "w").close()
os.makedirs(os.path.join(tmpdir, "src"))
open(os.path.join(tmpdir, "src", "app.py"), "w").close()
service = FileSearchService(root_dir=tmpdir)
results = service.search("main")
assert len(results) >= 1
assert any("main.py" in r.path for r in results)
def test_fuzzy_match(self):
"""Should fuzzy match file names."""
from praisonai.cli.features.at_mentions import FileSearchService
with tempfile.TemporaryDirectory() as tmpdir:
open(os.path.join(tmpdir, "my_long_filename.py"), "w").close()
service = FileSearchService(root_dir=tmpdir)
results = service.search("mlf") # Fuzzy match
assert len(results) >= 1
def test_empty_query_returns_all(self):
"""Should return files when query is empty."""
from praisonai.cli.features.at_mentions import FileSearchService
with tempfile.TemporaryDirectory() as tmpdir:
open(os.path.join(tmpdir, "a.py"), "w").close()
open(os.path.join(tmpdir, "b.py"), "w").close()
service = FileSearchService(root_dir=tmpdir)
results = service.search("")
assert len(results) >= 2
def test_respects_max_results(self):
"""Should limit results."""
from praisonai.cli.features.at_mentions import FileSearchService
with tempfile.TemporaryDirectory() as tmpdir:
for i in range(20):
open(os.path.join(tmpdir, f"file{i}.py"), "w").close()
service = FileSearchService(root_dir=tmpdir)
results = service.search("", max_results=5)
assert len(results) <= 5
def test_cache_works(self):
"""Should cache results."""
from praisonai.cli.features.at_mentions import FileSearchService
with tempfile.TemporaryDirectory() as tmpdir:
open(os.path.join(tmpdir, "test.py"), "w").close()
service = FileSearchService(root_dir=tmpdir, cache_ttl=10)
# First search
start = time.time()
results1 = service.search("test")
first_time = time.time() - start
# Second search (cached)
start = time.time()
results2 = service.search("test")
second_time = time.time() - start
assert len(results1) == len(results2)
# Cache should be faster (or at least not slower)
# ============================================================================
# 4. AtMentionCompleter Tests
# ============================================================================
class TestAtMentionCompleter:
"""Test AtMentionCompleter for prompt_toolkit integration."""
def test_completer_creation(self):
"""Should create completer."""
from praisonai.cli.features.at_mentions import AtMentionCompleter
completer = AtMentionCompleter(root_dir=".")
assert completer is not None
def test_no_completions_without_at(self):
"""Should not complete without @."""
from praisonai.cli.features.at_mentions import AtMentionCompleter
from prompt_toolkit.document import Document
completer = AtMentionCompleter(root_dir=".")
doc = Document("hello world")
completions = list(completer.get_completions(doc, None))
assert len(completions) == 0
def test_completions_with_at(self):
"""Should complete with @."""
from praisonai.cli.features.at_mentions import AtMentionCompleter
from prompt_toolkit.document import Document
with tempfile.TemporaryDirectory() as tmpdir:
open(os.path.join(tmpdir, "main.py"), "w").close()
completer = AtMentionCompleter(root_dir=tmpdir)
doc = Document("@main")
completions = list(completer.get_completions(doc, None))
assert len(completions) >= 1
# ============================================================================
# 5. CombinedCompleter Tests
# ============================================================================
class TestCombinedCompleter:
"""Test CombinedCompleter that handles both / and @."""
def test_slash_commands(self):
"""Should complete slash commands."""
from praisonai.cli.features.at_mentions import CombinedCompleter
from prompt_toolkit.document import Document
completer = CombinedCompleter(
commands=["help", "exit", "queue"],
root_dir="."
)
doc = Document("/he")
completions = list(completer.get_completions(doc, None))
assert len(completions) >= 1
assert any("/help" in c.text for c in completions)
def test_at_mentions(self):
"""Should complete @ mentions."""
from praisonai.cli.features.at_mentions import CombinedCompleter
from prompt_toolkit.document import Document
with tempfile.TemporaryDirectory() as tmpdir:
open(os.path.join(tmpdir, "test.py"), "w").close()
completer = CombinedCompleter(
commands=["help"],
root_dir=tmpdir
)
doc = Document("@test")
completions = list(completer.get_completions(doc, None))
assert len(completions) >= 1
def test_no_completions_for_regular_text(self):
"""Should not complete regular text."""
from praisonai.cli.features.at_mentions import CombinedCompleter
from prompt_toolkit.document import Document
completer = CombinedCompleter(
commands=["help"],
root_dir="."
)
doc = Document("hello world")
completions = list(completer.get_completions(doc, None))
assert len(completions) == 0
+259
View File
@@ -0,0 +1,259 @@
"""
Unit tests for PraisonAI Profiler module.
"""
import time
class TestProfiler:
"""Test Profiler class."""
def test_profiler_singleton(self):
"""Should return same instance."""
from praisonai.profiler import Profiler
p1 = Profiler()
p2 = Profiler()
assert p1 is p2
def test_enable_disable(self):
"""Should enable and disable profiling."""
from praisonai.profiler import Profiler
Profiler.disable()
assert not Profiler.is_enabled()
Profiler.enable()
assert Profiler.is_enabled()
Profiler.disable()
def test_record_timing(self):
"""Should record timing."""
from praisonai.profiler import Profiler
Profiler.enable()
Profiler.clear()
Profiler.record_timing("test_op", 100.5, "test")
timings = Profiler.get_timings()
assert len(timings) >= 1
assert timings[-1].name == "test_op"
assert timings[-1].duration_ms == 100.5
Profiler.disable()
Profiler.clear()
def test_record_import(self):
"""Should record import timing."""
from praisonai.profiler import Profiler
Profiler.enable()
Profiler.clear()
Profiler.record_import("test_module", 50.0)
imports = Profiler.get_imports()
assert len(imports) >= 1
assert imports[-1].module == "test_module"
Profiler.disable()
Profiler.clear()
def test_block_context_manager(self):
"""Should profile a block of code."""
from praisonai.profiler import Profiler
Profiler.enable()
Profiler.clear()
with Profiler.block("test_block"):
time.sleep(0.01)
timings = Profiler.get_timings(category="block")
assert len(timings) >= 1
assert timings[-1].name == "test_block"
assert timings[-1].duration_ms >= 10
Profiler.disable()
Profiler.clear()
def test_get_summary(self):
"""Should return summary."""
from praisonai.profiler import Profiler
Profiler.enable()
Profiler.clear()
Profiler.record_timing("op1", 100, "function")
Profiler.record_timing("op2", 200, "function")
Profiler.record_import("mod1", 50)
summary = Profiler.get_summary()
assert summary['total_time_ms'] == 300
assert summary['import_time_ms'] == 50
assert summary['timing_count'] == 2
assert summary['import_count'] == 1
Profiler.disable()
Profiler.clear()
class TestProfileDecorator:
"""Test @profile decorator."""
def test_profile_function(self):
"""Should profile a function."""
from praisonai.profiler import Profiler, profile
Profiler.enable()
Profiler.clear()
@profile
def test_func():
time.sleep(0.01)
return "result"
result = test_func()
assert result == "result"
timings = Profiler.get_timings(category="function")
assert len(timings) >= 1
assert timings[-1].name == "test_func"
assert timings[-1].duration_ms >= 10
Profiler.disable()
Profiler.clear()
def test_profile_with_category(self):
"""Should profile with custom category."""
from praisonai.profiler import Profiler, profile
Profiler.enable()
Profiler.clear()
@profile(category="api")
def api_call():
return "response"
api_call()
timings = Profiler.get_timings(category="api")
assert len(timings) >= 1
assert timings[-1].name == "api_call"
Profiler.disable()
Profiler.clear()
def test_profile_disabled(self):
"""Should not record when disabled."""
from praisonai.profiler import Profiler, profile
Profiler.disable()
Profiler.clear()
@profile
def test_func():
return "result"
test_func()
timings = Profiler.get_timings()
assert len(timings) == 0
class TestImportProfiler:
"""Test ImportProfiler."""
def test_profile_imports(self):
"""Should profile imports."""
from praisonai.profiler import profile_imports
with profile_imports() as profiler:
import json
import re
imports = profiler.get_imports()
# May or may not have imports depending on cache
assert isinstance(imports, list)
def test_get_slowest(self):
"""Should get slowest imports."""
from praisonai.profiler import profile_imports
with profile_imports() as profiler:
import collections
slowest = profiler.get_slowest(5)
assert isinstance(slowest, list)
assert len(slowest) <= 5
class TestUtilityFunctions:
"""Test utility functions."""
def test_check_module_available(self):
"""Should check module availability."""
from praisonai.profiler import check_module_available
assert check_module_available("os") is True
assert check_module_available("nonexistent_module_xyz") is False
def test_time_import(self):
"""Should time import."""
from praisonai.profiler import time_import
duration = time_import("json")
assert isinstance(duration, float)
assert duration >= 0
class TestDataClasses:
"""Test data classes."""
def test_timing_record(self):
"""Should create TimingRecord."""
from praisonai.profiler import TimingRecord
record = TimingRecord(
name="test",
duration_ms=100.0,
category="function",
file="test.py",
line=10
)
assert record.name == "test"
assert record.duration_ms == 100.0
assert record.category == "function"
def test_import_record(self):
"""Should create ImportRecord."""
from praisonai.profiler import ImportRecord
record = ImportRecord(
module="test_module",
duration_ms=50.0,
parent="parent_module"
)
assert record.module == "test_module"
assert record.duration_ms == 50.0
def test_flow_record(self):
"""Should create FlowRecord."""
from praisonai.profiler import FlowRecord
record = FlowRecord(
step=1,
name="step1",
file="test.py",
line=20,
duration_ms=30.0
)
assert record.step == 1
assert record.name == "step1"