praisonAI 2.3.78__tar.gz → 2.3.79__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {praisonai-2.3.78 → praisonai-2.3.79}/PKG-INFO +1 -1
- {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/PKG-INFO +1 -1
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/compare.py +27 -2
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/fast_context.py +46 -10
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/handoff.py +35 -16
- praisonai-2.3.79/praisonai/cli/features/image.py +376 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/metrics.py +16 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/main.py +28 -3
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/deploy.py +1 -1
- praisonai-2.3.79/praisonai/version.py +1 -0
- praisonai-2.3.78/praisonai/cli/features/image.py +0 -207
- praisonai-2.3.78/praisonai/version.py +0 -1
- {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/SOURCES.txt +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/dependency_links.txt +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/entry_points.txt +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/requires.txt +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/top_level.txt +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/README.md +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/__main__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/agent_scheduler.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/agents_generator.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/api/call.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/auto.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/chainlit_ui.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/agent_scheduler.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/at_mentions.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/auto_memory.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/autonomy_mode.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/base.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/cost_tracker.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/eval.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/external_agents.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/flow_display.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/git_integration.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/guardrail.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/interactive_tui.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/knowledge.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/mcp.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/message_queue.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/n8n.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/profiling.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/repo_map.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/router.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/sandbox_executor.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/session.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/skills.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/slash_commands.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/telemetry.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/todo.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/tools.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/workflow.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/schedule_cli.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/agent_tools.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/diff/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/diff/diff_strategy.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/apply_diff.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/execute_command.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/list_files.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/read_file.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/search_replace.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/write_file.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/utils/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/utils/file_utils.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/utils/ignore_utils.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/utils/text_utils.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/inbuilt_tools/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/inbuilt_tools/autogen_tools.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/inc/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/inc/config.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/inc/models.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/base.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/claude_code.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/codex_cli.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/cursor_cli.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/gemini_cli.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/profiler.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/agent_scheduler.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/base.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/daemon_manager.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/state_manager.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/yaml_loader.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/setup/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/setup/build.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/setup/post_install.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/setup/setup_conda_env.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/setup.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/test.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/train.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/train_vision.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/agents.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/callbacks.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/chat.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/code.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/colab.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/colab_chainlit.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/components/aicoder.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/context.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/database_config.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/db.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/realtime.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/realtimeclient/__init__.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/realtimeclient/tools.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/sql_alchemy.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/upload_vision.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/pyproject.toml +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/setup.cfg +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/tests/test.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_agents_playbook.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_approval_system.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_basic.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_cli_features.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_custom_tools_fix.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_double_api_fix.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_n8n_integration.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_phase2_refactor.py +0 -0
- {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_runner.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PraisonAI
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.79
|
|
4
4
|
Summary: PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration.
|
|
5
5
|
Author: Mervin Praison
|
|
6
6
|
License: MIT
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PraisonAI
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.79
|
|
4
4
|
Summary: PraisonAI is an AI Agents Framework with Self Reflection. PraisonAI application combines PraisonAI Agents, AutoGen, and CrewAI into a low-code solution for building and managing multi-agent LLM systems, focusing on simplicity, customisation, and efficient human-agent collaboration.
|
|
5
5
|
Author: Mervin Praison
|
|
6
6
|
License: MIT
|
|
@@ -177,12 +177,13 @@ def save_compare_result(result: CompareResult, path: str) -> bool:
|
|
|
177
177
|
return False
|
|
178
178
|
|
|
179
179
|
|
|
180
|
-
def format_comparison_table(result: CompareResult) -> str:
|
|
180
|
+
def format_comparison_table(result: CompareResult, show_responses: bool = True) -> str:
|
|
181
181
|
"""
|
|
182
|
-
Format comparison result as a table string.
|
|
182
|
+
Format comparison result as a table string with optional response display.
|
|
183
183
|
|
|
184
184
|
Args:
|
|
185
185
|
result: CompareResult to format
|
|
186
|
+
show_responses: Whether to show actual responses from each mode
|
|
186
187
|
|
|
187
188
|
Returns:
|
|
188
189
|
Formatted table string
|
|
@@ -190,10 +191,12 @@ def format_comparison_table(result: CompareResult) -> str:
|
|
|
190
191
|
try:
|
|
191
192
|
from rich.console import Console
|
|
192
193
|
from rich.table import Table
|
|
194
|
+
from rich.panel import Panel
|
|
193
195
|
from io import StringIO
|
|
194
196
|
|
|
195
197
|
console = Console(file=StringIO(), force_terminal=True)
|
|
196
198
|
|
|
199
|
+
# Main comparison table
|
|
197
200
|
table = Table(title=f"Comparison: {result.query[:50]}...")
|
|
198
201
|
table.add_column("Mode", style="cyan")
|
|
199
202
|
table.add_column("Time (ms)", style="green")
|
|
@@ -224,6 +227,26 @@ def format_comparison_table(result: CompareResult) -> str:
|
|
|
224
227
|
)
|
|
225
228
|
|
|
226
229
|
console.print(table)
|
|
230
|
+
|
|
231
|
+
# Show responses if enabled
|
|
232
|
+
if show_responses:
|
|
233
|
+
console.print("\n[bold cyan]📝 Responses:[/bold cyan]")
|
|
234
|
+
for c in result.comparisons:
|
|
235
|
+
if c.error is None:
|
|
236
|
+
# Truncate long responses
|
|
237
|
+
response_preview = c.output[:500] + "..." if len(c.output) > 500 else c.output
|
|
238
|
+
console.print(Panel(
|
|
239
|
+
response_preview,
|
|
240
|
+
title=f"[bold]{c.mode}[/bold] ({c.execution_time_ms:.0f}ms)",
|
|
241
|
+
border_style="cyan"
|
|
242
|
+
))
|
|
243
|
+
else:
|
|
244
|
+
console.print(Panel(
|
|
245
|
+
f"[red]Error: {c.error}[/red]",
|
|
246
|
+
title=f"[bold]{c.mode}[/bold]",
|
|
247
|
+
border_style="red"
|
|
248
|
+
))
|
|
249
|
+
|
|
227
250
|
return console.file.getvalue()
|
|
228
251
|
except ImportError:
|
|
229
252
|
lines = [f"Comparison: {result.query}"]
|
|
@@ -231,6 +254,8 @@ def format_comparison_table(result: CompareResult) -> str:
|
|
|
231
254
|
for c in result.comparisons:
|
|
232
255
|
status = "OK" if c.error is None else f"ERROR: {c.error}"
|
|
233
256
|
lines.append(f"{c.mode}: {c.execution_time_ms:.1f}ms - {status}")
|
|
257
|
+
if show_responses and c.error is None:
|
|
258
|
+
lines.append(f" Response: {c.output[:200]}...")
|
|
234
259
|
return "\n".join(lines)
|
|
235
260
|
|
|
236
261
|
|
|
@@ -70,17 +70,46 @@ class FastContextHandler(FlagHandler):
|
|
|
70
70
|
|
|
71
71
|
return True, ""
|
|
72
72
|
|
|
73
|
+
def _count_files_quick(self, path: str, max_count: int = 1000) -> int:
|
|
74
|
+
"""Quickly count files in directory, stopping at max_count."""
|
|
75
|
+
count = 0
|
|
76
|
+
ignore_dirs = {'.git', '__pycache__', 'node_modules', '.venv', 'venv', '.cache', 'dist', 'build'}
|
|
77
|
+
|
|
78
|
+
for root, dirs, files in os.walk(path):
|
|
79
|
+
dirs[:] = [d for d in dirs if d not in ignore_dirs and not d.startswith('.')]
|
|
80
|
+
count += len(files)
|
|
81
|
+
if count >= max_count:
|
|
82
|
+
return count
|
|
83
|
+
return count
|
|
84
|
+
|
|
73
85
|
def _get_folder_tree(self, path: str, max_depth: int = 2) -> str:
|
|
74
|
-
"""
|
|
75
|
-
|
|
76
|
-
path = os.path.abspath(path)
|
|
86
|
+
"""
|
|
87
|
+
Get a compact folder tree structure for context.
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
|
|
89
|
+
Optimized for large directories:
|
|
90
|
+
- Limits depth to max_depth (default 2)
|
|
91
|
+
- Shows max 10 files per directory
|
|
92
|
+
- Truncates total output to 50 lines
|
|
93
|
+
- Skips common non-code directories
|
|
94
|
+
"""
|
|
95
|
+
# Quick check for very large directories
|
|
96
|
+
file_count = self._count_files_quick(path, 500)
|
|
97
|
+
if file_count >= 500:
|
|
98
|
+
# For large directories, reduce depth and show summary
|
|
99
|
+
max_depth = 1
|
|
100
|
+
logger.debug(f"Large directory detected ({file_count}+ files), reducing tree depth")
|
|
101
|
+
|
|
102
|
+
tree_lines = []
|
|
103
|
+
ignore_dirs = {'.git', '__pycache__', 'node_modules', '.venv', 'venv', '.cache', 'dist', 'build', 'coverage', '.pytest_cache', '.mypy_cache', 'eggs', '*.egg-info'}
|
|
104
|
+
ignore_exts = {'.pyc', '.pyo', '.so', '.o', '.a', '.dylib', '.class', '.jar'}
|
|
105
|
+
total_files = 0
|
|
106
|
+
max_total_files = 100 # Stop after seeing this many files
|
|
80
107
|
|
|
81
108
|
def walk_tree(current_path, prefix="", depth=0):
|
|
82
|
-
|
|
109
|
+
nonlocal total_files
|
|
110
|
+
if depth > max_depth or total_files >= max_total_files:
|
|
83
111
|
return
|
|
112
|
+
|
|
84
113
|
try:
|
|
85
114
|
entries = sorted(os.listdir(current_path))
|
|
86
115
|
except PermissionError:
|
|
@@ -98,14 +127,21 @@ class FastContextHandler(FlagHandler):
|
|
|
98
127
|
if ext not in ignore_exts and not entry.startswith('.'):
|
|
99
128
|
files.append(entry)
|
|
100
129
|
|
|
101
|
-
|
|
102
|
-
|
|
130
|
+
# Limit directories shown at each level
|
|
131
|
+
dirs_to_show = dirs[:15]
|
|
132
|
+
for i, d in enumerate(dirs_to_show):
|
|
133
|
+
is_last = (i == len(dirs_to_show) - 1) and not files
|
|
103
134
|
tree_lines.append(f"{prefix}{'└── ' if is_last else '├── '}{d}/")
|
|
104
135
|
walk_tree(os.path.join(current_path, d),
|
|
105
136
|
prefix + (' ' if is_last else '│ '), depth + 1)
|
|
137
|
+
if len(dirs) > 15:
|
|
138
|
+
tree_lines.append(f"{prefix} ... and {len(dirs) - 15} more directories")
|
|
106
139
|
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
# Limit files shown
|
|
141
|
+
files_to_show = files[:10]
|
|
142
|
+
total_files += len(files)
|
|
143
|
+
for i, f in enumerate(files_to_show):
|
|
144
|
+
is_last = i == len(files_to_show) - 1
|
|
109
145
|
tree_lines.append(f"{prefix}{'└── ' if is_last else '├── '}{f}")
|
|
110
146
|
if len(files) > 10:
|
|
111
147
|
tree_lines.append(f"{prefix} ... and {len(files) - 10} more files")
|
|
@@ -72,11 +72,11 @@ class HandoffHandler(FlagHandler):
|
|
|
72
72
|
self.print_status(msg, "error")
|
|
73
73
|
return []
|
|
74
74
|
|
|
75
|
-
from praisonaiagents import Agent,
|
|
75
|
+
from praisonaiagents import Agent, handoff
|
|
76
76
|
|
|
77
77
|
agents = []
|
|
78
78
|
|
|
79
|
-
# Create agents
|
|
79
|
+
# First pass: Create all agents without handoffs
|
|
80
80
|
for i, name in enumerate(agent_names):
|
|
81
81
|
agent_config = {
|
|
82
82
|
"name": name.title().replace("_", " "),
|
|
@@ -91,26 +91,45 @@ class HandoffHandler(FlagHandler):
|
|
|
91
91
|
agent = Agent(**agent_config)
|
|
92
92
|
agents.append(agent)
|
|
93
93
|
|
|
94
|
-
# Set up handoffs
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
94
|
+
# Second pass: Set up handoffs by recreating agents with handoff config
|
|
95
|
+
final_agents = []
|
|
96
|
+
for i, name in enumerate(agent_names):
|
|
97
|
+
agent_config = {
|
|
98
|
+
"name": name.title().replace("_", " "),
|
|
99
|
+
"role": name.title().replace("_", " "),
|
|
100
|
+
"goal": f"Complete tasks as {name}",
|
|
101
|
+
"backstory": f"Expert {name} with specialized skills"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if llm:
|
|
105
|
+
agent_config["llm"] = llm
|
|
106
|
+
|
|
107
|
+
# Add handoff to next agent if not the last agent
|
|
108
|
+
if i < len(agent_names) - 1:
|
|
109
|
+
next_agent = agents[i + 1]
|
|
110
|
+
next_agent_name = agent_names[i + 1]
|
|
111
|
+
|
|
112
|
+
# Pass handoffs via constructor - this properly registers them as tools
|
|
113
|
+
agent_config["handoffs"] = [
|
|
114
|
+
handoff(
|
|
115
|
+
agent=next_agent,
|
|
116
|
+
tool_name_override=f"handoff_to_{next_agent_name}",
|
|
117
|
+
tool_description_override=f"Hand off the task to {next_agent_name}. Call this after completing your part."
|
|
118
|
+
)
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
# Add instructions to use handoff
|
|
122
|
+
agent_config["instructions"] = f"After completing your task, you MUST use the handoff_to_{next_agent_name} tool to pass your work to the next agent."
|
|
102
123
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
agent.handoffs = []
|
|
106
|
-
agent.handoffs.append(handoff)
|
|
124
|
+
final_agent = Agent(**agent_config)
|
|
125
|
+
final_agents.append(final_agent)
|
|
107
126
|
|
|
108
|
-
self.print_status(f"🤝 Created {len(
|
|
127
|
+
self.print_status(f"🤝 Created {len(final_agents)} agents with handoff chain", "success")
|
|
109
128
|
for i, name in enumerate(agent_names):
|
|
110
129
|
arrow = " → " if i < len(agent_names) - 1 else ""
|
|
111
130
|
self.print_status(f" {name}{arrow}", "info")
|
|
112
131
|
|
|
113
|
-
return
|
|
132
|
+
return final_agents
|
|
114
133
|
|
|
115
134
|
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
|
|
116
135
|
"""
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image Handler for CLI.
|
|
3
|
+
|
|
4
|
+
Provides two distinct image capabilities:
|
|
5
|
+
1. Image Description (Vision) - Analyze existing images using vision-capable LLMs
|
|
6
|
+
Usage: praisonai "Describe this image" --image path/to/image.png
|
|
7
|
+
|
|
8
|
+
2. Image Generation - Generate new images from text prompts
|
|
9
|
+
Usage: praisonai "A sunset over mountains" --image-generate
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import base64
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Dict, Tuple, List, Optional
|
|
16
|
+
from .base import FlagHandler
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
# Supported image extensions for vision/description
|
|
21
|
+
SUPPORTED_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.tiff'}
|
|
22
|
+
|
|
23
|
+
# Default vision-capable models (gpt-4o has better vision support than gpt-4o-mini)
|
|
24
|
+
DEFAULT_VISION_MODEL = "gpt-4o"
|
|
25
|
+
|
|
26
|
+
# Default image generation models
|
|
27
|
+
DEFAULT_IMAGE_GEN_MODEL = "dall-e-3"
|
|
28
|
+
SUPPORTED_IMAGE_GEN_MODELS = [
|
|
29
|
+
'gpt-image-1', 'gpt-image-1-mini', 'gpt-image-1.5',
|
|
30
|
+
'chatgpt-image-latest', 'dall-e-2', 'dall-e-3'
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ImageHandler(FlagHandler):
|
|
35
|
+
"""
|
|
36
|
+
Handler for --image flag (Image Description/Vision).
|
|
37
|
+
|
|
38
|
+
Describes/analyzes existing images using vision-capable LLMs like GPT-4o.
|
|
39
|
+
This is for understanding what's IN an image, not generating new images.
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
praisonai "Describe this image" --image photo.png
|
|
43
|
+
praisonai "What's in this image?" --image /path/to/image.jpg
|
|
44
|
+
praisonai "Count the objects" --image image1.png,image2.png
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def feature_name(self) -> str:
|
|
49
|
+
return "image_describe"
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def flag_name(self) -> str:
|
|
53
|
+
return "image"
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def flag_help(self) -> str:
|
|
57
|
+
return "Path to image file for vision-based description/analysis"
|
|
58
|
+
|
|
59
|
+
def check_dependencies(self) -> Tuple[bool, str]:
|
|
60
|
+
"""Check if litellm is available for vision."""
|
|
61
|
+
try:
|
|
62
|
+
import litellm
|
|
63
|
+
return True, ""
|
|
64
|
+
except ImportError:
|
|
65
|
+
return False, "litellm not installed. Install with: pip install litellm"
|
|
66
|
+
|
|
67
|
+
def validate_image_path(self, path: str) -> Tuple[bool, str]:
|
|
68
|
+
"""
|
|
69
|
+
Validate that the image path is valid.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
path: Path to the image file
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Tuple of (is_valid, error_message)
|
|
76
|
+
"""
|
|
77
|
+
if not path:
|
|
78
|
+
return False, "No image path provided"
|
|
79
|
+
|
|
80
|
+
# Check extension
|
|
81
|
+
ext = os.path.splitext(path)[1].lower()
|
|
82
|
+
if ext not in SUPPORTED_EXTENSIONS:
|
|
83
|
+
return False, f"Unsupported image format: {ext}. Supported: {', '.join(SUPPORTED_EXTENSIONS)}"
|
|
84
|
+
|
|
85
|
+
# Check if file exists (for local files)
|
|
86
|
+
if not path.startswith(('http://', 'https://')):
|
|
87
|
+
if not os.path.exists(path):
|
|
88
|
+
return False, f"Image file not found: {path}"
|
|
89
|
+
|
|
90
|
+
return True, ""
|
|
91
|
+
|
|
92
|
+
def _encode_image_to_base64(self, image_path: str) -> str:
|
|
93
|
+
"""Encode a local image file to base64."""
|
|
94
|
+
with open(image_path, 'rb') as f:
|
|
95
|
+
return base64.b64encode(f.read()).decode('utf-8')
|
|
96
|
+
|
|
97
|
+
def _get_image_mime_type(self, image_path: str) -> str:
|
|
98
|
+
"""Get MIME type based on file extension."""
|
|
99
|
+
ext = os.path.splitext(image_path)[1].lower()
|
|
100
|
+
mime_types = {
|
|
101
|
+
'.png': 'image/png',
|
|
102
|
+
'.jpg': 'image/jpeg',
|
|
103
|
+
'.jpeg': 'image/jpeg',
|
|
104
|
+
'.gif': 'image/gif',
|
|
105
|
+
'.webp': 'image/webp',
|
|
106
|
+
'.bmp': 'image/bmp',
|
|
107
|
+
'.svg': 'image/svg+xml',
|
|
108
|
+
'.tiff': 'image/tiff',
|
|
109
|
+
}
|
|
110
|
+
return mime_types.get(ext, 'image/png')
|
|
111
|
+
|
|
112
|
+
def _build_image_content(self, image_path: str) -> Dict[str, Any]:
|
|
113
|
+
"""Build image content for vision API."""
|
|
114
|
+
if image_path.startswith(('http://', 'https://')):
|
|
115
|
+
return {
|
|
116
|
+
"type": "image_url",
|
|
117
|
+
"image_url": {"url": image_path}
|
|
118
|
+
}
|
|
119
|
+
else:
|
|
120
|
+
mime_type = self._get_image_mime_type(image_path)
|
|
121
|
+
base64_data = self._encode_image_to_base64(image_path)
|
|
122
|
+
return {
|
|
123
|
+
"type": "image_url",
|
|
124
|
+
"image_url": {"url": f"data:{mime_type};base64,{base64_data}"}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
|
|
128
|
+
"""Apply image configuration to agent config."""
|
|
129
|
+
if flag_value:
|
|
130
|
+
if isinstance(flag_value, str):
|
|
131
|
+
config["images"] = [flag_value]
|
|
132
|
+
elif isinstance(flag_value, list):
|
|
133
|
+
config["images"] = flag_value
|
|
134
|
+
config["use_vision"] = True
|
|
135
|
+
return config
|
|
136
|
+
|
|
137
|
+
def describe_image(self, prompt: str, image_path: str, llm: str = None) -> str:
|
|
138
|
+
"""
|
|
139
|
+
Describe an image using vision-capable LLM.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
prompt: The prompt/question about the image
|
|
143
|
+
image_path: Path to the image file
|
|
144
|
+
llm: Optional LLM model name (must be vision-capable)
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Description of the image
|
|
148
|
+
"""
|
|
149
|
+
valid, msg = self.validate_image_path(image_path)
|
|
150
|
+
if not valid:
|
|
151
|
+
return f"Error: {msg}"
|
|
152
|
+
|
|
153
|
+
available, dep_msg = self.check_dependencies()
|
|
154
|
+
if not available:
|
|
155
|
+
return f"Error: {dep_msg}"
|
|
156
|
+
|
|
157
|
+
import litellm
|
|
158
|
+
|
|
159
|
+
model = llm or DEFAULT_VISION_MODEL
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Build message with image
|
|
163
|
+
content = [
|
|
164
|
+
{"type": "text", "text": prompt},
|
|
165
|
+
self._build_image_content(image_path)
|
|
166
|
+
]
|
|
167
|
+
|
|
168
|
+
response = litellm.completion(
|
|
169
|
+
model=model,
|
|
170
|
+
messages=[{"role": "user", "content": content}]
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
return response.choices[0].message.content
|
|
174
|
+
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"Error describing image: {e}")
|
|
177
|
+
return f"Error describing image: {e}"
|
|
178
|
+
|
|
179
|
+
def describe_multiple_images(self, prompt: str, image_paths: List[str], llm: str = None) -> str:
|
|
180
|
+
"""
|
|
181
|
+
Describe multiple images using vision-capable LLM.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
prompt: The prompt/question about the images
|
|
185
|
+
image_paths: List of paths to image files
|
|
186
|
+
llm: Optional LLM model name
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
Description of the images
|
|
190
|
+
"""
|
|
191
|
+
for path in image_paths:
|
|
192
|
+
valid, msg = self.validate_image_path(path)
|
|
193
|
+
if not valid:
|
|
194
|
+
return f"Error: {msg}"
|
|
195
|
+
|
|
196
|
+
available, dep_msg = self.check_dependencies()
|
|
197
|
+
if not available:
|
|
198
|
+
return f"Error: {dep_msg}"
|
|
199
|
+
|
|
200
|
+
import litellm
|
|
201
|
+
|
|
202
|
+
model = llm or DEFAULT_VISION_MODEL
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
# Build message with all images
|
|
206
|
+
content = [{"type": "text", "text": prompt}]
|
|
207
|
+
for path in image_paths:
|
|
208
|
+
content.append(self._build_image_content(path))
|
|
209
|
+
|
|
210
|
+
response = litellm.completion(
|
|
211
|
+
model=model,
|
|
212
|
+
messages=[{"role": "user", "content": content}]
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return response.choices[0].message.content
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(f"Error describing images: {e}")
|
|
219
|
+
return f"Error describing images: {e}"
|
|
220
|
+
|
|
221
|
+
def execute(self, prompt: str = None, image_path: str = None, llm: str = None, **kwargs) -> str:
|
|
222
|
+
"""
|
|
223
|
+
Execute image description (vision).
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
prompt: The prompt/question about the image
|
|
227
|
+
image_path: Path to the image file (or comma-separated paths)
|
|
228
|
+
llm: Optional vision-capable LLM model name
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Description of the image(s)
|
|
232
|
+
"""
|
|
233
|
+
if not prompt:
|
|
234
|
+
return "Error: No prompt provided"
|
|
235
|
+
|
|
236
|
+
if not image_path:
|
|
237
|
+
return "Error: No image path provided"
|
|
238
|
+
|
|
239
|
+
# Handle multiple images (comma-separated)
|
|
240
|
+
if ',' in image_path:
|
|
241
|
+
paths = [p.strip() for p in image_path.split(',')]
|
|
242
|
+
return self.describe_multiple_images(prompt, paths, llm)
|
|
243
|
+
|
|
244
|
+
return self.describe_image(prompt, image_path, llm)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class ImageGenerateHandler(FlagHandler):
|
|
248
|
+
"""
|
|
249
|
+
Handler for --image-generate flag (Image Generation).
|
|
250
|
+
|
|
251
|
+
Generates NEW images from text prompts using DALL-E or similar models.
|
|
252
|
+
This is for CREATING images, not analyzing existing ones.
|
|
253
|
+
|
|
254
|
+
Example:
|
|
255
|
+
praisonai "A sunset over mountains" --image-generate
|
|
256
|
+
praisonai "A cat wearing a hat" --image-generate --llm dall-e-3
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
@property
|
|
260
|
+
def feature_name(self) -> str:
|
|
261
|
+
return "image_generate"
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def flag_name(self) -> str:
|
|
265
|
+
return "image_generate"
|
|
266
|
+
|
|
267
|
+
@property
|
|
268
|
+
def flag_help(self) -> str:
|
|
269
|
+
return "Generate an image from the text prompt"
|
|
270
|
+
|
|
271
|
+
def check_dependencies(self) -> Tuple[bool, str]:
|
|
272
|
+
"""Check if ImageAgent is available."""
|
|
273
|
+
try:
|
|
274
|
+
import importlib.util
|
|
275
|
+
if importlib.util.find_spec("praisonaiagents") is not None:
|
|
276
|
+
return True, ""
|
|
277
|
+
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
|
|
278
|
+
except ImportError:
|
|
279
|
+
return False, "praisonaiagents not installed. Install with: pip install praisonaiagents"
|
|
280
|
+
|
|
281
|
+
def apply_to_agent_config(self, config: Dict[str, Any], flag_value: Any) -> Dict[str, Any]:
|
|
282
|
+
"""Apply image generation configuration."""
|
|
283
|
+
if flag_value:
|
|
284
|
+
config["use_image_generation"] = True
|
|
285
|
+
return config
|
|
286
|
+
|
|
287
|
+
def generate_image(self, prompt: str, llm: str = None, output_path: str = None) -> Dict[str, Any]:
|
|
288
|
+
"""
|
|
289
|
+
Generate an image from a text prompt.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
prompt: Text description of the image to generate
|
|
293
|
+
llm: Image generation model (dall-e-3, dall-e-2, etc.)
|
|
294
|
+
output_path: Optional path to save the generated image
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
Dict with image URL or path, or error message
|
|
298
|
+
"""
|
|
299
|
+
available, msg = self.check_dependencies()
|
|
300
|
+
if not available:
|
|
301
|
+
return {"error": msg}
|
|
302
|
+
|
|
303
|
+
from praisonaiagents import ImageAgent
|
|
304
|
+
|
|
305
|
+
# Use provided model or default
|
|
306
|
+
model = llm or DEFAULT_IMAGE_GEN_MODEL
|
|
307
|
+
|
|
308
|
+
# Validate model is supported for generation
|
|
309
|
+
if model not in SUPPORTED_IMAGE_GEN_MODELS:
|
|
310
|
+
return {
|
|
311
|
+
"error": f"Invalid model: '{model}'. Supported image generation models: {', '.join(SUPPORTED_IMAGE_GEN_MODELS)}"
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
agent = ImageAgent(
|
|
316
|
+
name="ImageGenerator",
|
|
317
|
+
role="Image Generation Assistant",
|
|
318
|
+
goal="Generate high-quality images from text descriptions",
|
|
319
|
+
llm=model
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
result = agent.chat(prompt)
|
|
323
|
+
|
|
324
|
+
# Save image if output path provided
|
|
325
|
+
if output_path and isinstance(result, dict) and 'data' in result:
|
|
326
|
+
self._save_image(result, output_path)
|
|
327
|
+
result['saved_to'] = output_path
|
|
328
|
+
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
logger.error(f"Error generating image: {e}")
|
|
333
|
+
return {"error": str(e)}
|
|
334
|
+
|
|
335
|
+
def _save_image(self, result: Dict[str, Any], output_path: str) -> bool:
|
|
336
|
+
"""Save generated image to file."""
|
|
337
|
+
try:
|
|
338
|
+
import requests
|
|
339
|
+
|
|
340
|
+
if 'data' in result and len(result['data']) > 0:
|
|
341
|
+
image_data = result['data'][0]
|
|
342
|
+
|
|
343
|
+
if 'url' in image_data:
|
|
344
|
+
# Download from URL
|
|
345
|
+
response = requests.get(image_data['url'])
|
|
346
|
+
with open(output_path, 'wb') as f:
|
|
347
|
+
f.write(response.content)
|
|
348
|
+
return True
|
|
349
|
+
elif 'b64_json' in image_data:
|
|
350
|
+
# Decode base64
|
|
351
|
+
import base64
|
|
352
|
+
img_bytes = base64.b64decode(image_data['b64_json'])
|
|
353
|
+
with open(output_path, 'wb') as f:
|
|
354
|
+
f.write(img_bytes)
|
|
355
|
+
return True
|
|
356
|
+
return False
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.error(f"Error saving image: {e}")
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
def execute(self, prompt: str = None, llm: str = None, output_path: str = None, **kwargs) -> Dict[str, Any]:
|
|
362
|
+
"""
|
|
363
|
+
Execute image generation.
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
prompt: Text description of the image to generate
|
|
367
|
+
llm: Image generation model name
|
|
368
|
+
output_path: Optional path to save the image
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Dict with generated image info or error
|
|
372
|
+
"""
|
|
373
|
+
if not prompt:
|
|
374
|
+
return {"error": "No prompt provided for image generation"}
|
|
375
|
+
|
|
376
|
+
return self.generate_image(prompt, llm, output_path)
|
|
@@ -107,6 +107,14 @@ class MetricsHandler(FlagHandler):
|
|
|
107
107
|
if hasattr(agent, 'last_token_metrics'):
|
|
108
108
|
metrics.update(agent.last_token_metrics or {})
|
|
109
109
|
|
|
110
|
+
# Check llm_model property for model info
|
|
111
|
+
if hasattr(agent, 'llm_model'):
|
|
112
|
+
llm_model = agent.llm_model
|
|
113
|
+
if hasattr(llm_model, 'model'):
|
|
114
|
+
metrics['model'] = llm_model.model
|
|
115
|
+
elif isinstance(llm_model, str):
|
|
116
|
+
metrics['model'] = llm_model
|
|
117
|
+
|
|
110
118
|
if hasattr(agent, 'llm') and agent.llm:
|
|
111
119
|
llm = agent.llm
|
|
112
120
|
if hasattr(llm, 'last_token_metrics'):
|
|
@@ -117,6 +125,14 @@ class MetricsHandler(FlagHandler):
|
|
|
117
125
|
if hasattr(agent, 'metrics') and isinstance(agent.metrics, dict):
|
|
118
126
|
metrics.update(agent.metrics or {})
|
|
119
127
|
|
|
128
|
+
# Try to get from litellm's global tracking
|
|
129
|
+
try:
|
|
130
|
+
import litellm
|
|
131
|
+
if hasattr(litellm, '_current_cost'):
|
|
132
|
+
metrics['cost'] = litellm._current_cost
|
|
133
|
+
except:
|
|
134
|
+
pass
|
|
135
|
+
|
|
120
136
|
return metrics
|
|
121
137
|
|
|
122
138
|
def post_process_result(self, result: Any, flag_value: Any) -> Any:
|