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.
Files changed (124) hide show
  1. {praisonai-2.3.78 → praisonai-2.3.79}/PKG-INFO +1 -1
  2. {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/PKG-INFO +1 -1
  3. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/compare.py +27 -2
  4. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/fast_context.py +46 -10
  5. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/handoff.py +35 -16
  6. praisonai-2.3.79/praisonai/cli/features/image.py +376 -0
  7. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/metrics.py +16 -0
  8. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/main.py +28 -3
  9. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/deploy.py +1 -1
  10. praisonai-2.3.79/praisonai/version.py +1 -0
  11. praisonai-2.3.78/praisonai/cli/features/image.py +0 -207
  12. praisonai-2.3.78/praisonai/version.py +0 -1
  13. {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/SOURCES.txt +0 -0
  14. {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/dependency_links.txt +0 -0
  15. {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/entry_points.txt +0 -0
  16. {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/requires.txt +0 -0
  17. {praisonai-2.3.78 → praisonai-2.3.79}/PraisonAI.egg-info/top_level.txt +0 -0
  18. {praisonai-2.3.78 → praisonai-2.3.79}/README.md +0 -0
  19. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/__init__.py +0 -0
  20. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/__main__.py +0 -0
  21. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/agent_scheduler.py +0 -0
  22. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/agents_generator.py +0 -0
  23. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/api/call.py +0 -0
  24. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/auto.py +0 -0
  25. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/chainlit_ui.py +0 -0
  26. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/__init__.py +0 -0
  27. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/__init__.py +0 -0
  28. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/agent_scheduler.py +0 -0
  29. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/at_mentions.py +0 -0
  30. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/auto_memory.py +0 -0
  31. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/autonomy_mode.py +0 -0
  32. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/base.py +0 -0
  33. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/cost_tracker.py +0 -0
  34. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/eval.py +0 -0
  35. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/external_agents.py +0 -0
  36. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/flow_display.py +0 -0
  37. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/git_integration.py +0 -0
  38. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/guardrail.py +0 -0
  39. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/interactive_tui.py +0 -0
  40. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/knowledge.py +0 -0
  41. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/mcp.py +0 -0
  42. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/message_queue.py +0 -0
  43. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/n8n.py +0 -0
  44. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/profiling.py +0 -0
  45. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/repo_map.py +0 -0
  46. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/router.py +0 -0
  47. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/sandbox_executor.py +0 -0
  48. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/session.py +0 -0
  49. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/skills.py +0 -0
  50. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/slash_commands.py +0 -0
  51. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/telemetry.py +0 -0
  52. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/todo.py +0 -0
  53. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/tools.py +0 -0
  54. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/features/workflow.py +0 -0
  55. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/cli/schedule_cli.py +0 -0
  56. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/__init__.py +0 -0
  57. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/agent_tools.py +0 -0
  58. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/diff/__init__.py +0 -0
  59. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/diff/diff_strategy.py +0 -0
  60. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/__init__.py +0 -0
  61. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/apply_diff.py +0 -0
  62. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/execute_command.py +0 -0
  63. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/list_files.py +0 -0
  64. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/read_file.py +0 -0
  65. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/search_replace.py +0 -0
  66. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/tools/write_file.py +0 -0
  67. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/utils/__init__.py +0 -0
  68. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/utils/file_utils.py +0 -0
  69. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/utils/ignore_utils.py +0 -0
  70. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/code/utils/text_utils.py +0 -0
  71. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/inbuilt_tools/__init__.py +0 -0
  72. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/inbuilt_tools/autogen_tools.py +0 -0
  73. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/inc/__init__.py +0 -0
  74. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/inc/config.py +0 -0
  75. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/inc/models.py +0 -0
  76. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/__init__.py +0 -0
  77. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/base.py +0 -0
  78. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/claude_code.py +0 -0
  79. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/codex_cli.py +0 -0
  80. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/cursor_cli.py +0 -0
  81. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/integrations/gemini_cli.py +0 -0
  82. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/profiler.py +0 -0
  83. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/__init__.py +0 -0
  84. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/agent_scheduler.py +0 -0
  85. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/base.py +0 -0
  86. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/daemon_manager.py +0 -0
  87. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/state_manager.py +0 -0
  88. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler/yaml_loader.py +0 -0
  89. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/scheduler.py +0 -0
  90. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/setup/__init__.py +0 -0
  91. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/setup/build.py +0 -0
  92. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/setup/post_install.py +0 -0
  93. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/setup/setup_conda_env.py +0 -0
  94. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/setup.py +0 -0
  95. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/test.py +0 -0
  96. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/train.py +0 -0
  97. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/train_vision.py +0 -0
  98. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/agents.py +0 -0
  99. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/callbacks.py +0 -0
  100. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/chat.py +0 -0
  101. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/code.py +0 -0
  102. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/colab.py +0 -0
  103. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/colab_chainlit.py +0 -0
  104. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/components/aicoder.py +0 -0
  105. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/context.py +0 -0
  106. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/database_config.py +0 -0
  107. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/db.py +0 -0
  108. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/realtime.py +0 -0
  109. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/realtimeclient/__init__.py +0 -0
  110. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/realtimeclient/tools.py +0 -0
  111. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/ui/sql_alchemy.py +0 -0
  112. {praisonai-2.3.78 → praisonai-2.3.79}/praisonai/upload_vision.py +0 -0
  113. {praisonai-2.3.78 → praisonai-2.3.79}/pyproject.toml +0 -0
  114. {praisonai-2.3.78 → praisonai-2.3.79}/setup.cfg +0 -0
  115. {praisonai-2.3.78 → praisonai-2.3.79}/tests/test.py +0 -0
  116. {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_agents_playbook.py +0 -0
  117. {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_approval_system.py +0 -0
  118. {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_basic.py +0 -0
  119. {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_cli_features.py +0 -0
  120. {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_custom_tools_fix.py +0 -0
  121. {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_double_api_fix.py +0 -0
  122. {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_n8n_integration.py +0 -0
  123. {praisonai-2.3.78 → praisonai-2.3.79}/tests/test_phase2_refactor.py +0 -0
  124. {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.78
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.78
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
- """Get folder tree structure for context."""
75
- tree_lines = []
76
- path = os.path.abspath(path)
86
+ """
87
+ Get a compact folder tree structure for context.
77
88
 
78
- ignore_dirs = {'.git', '__pycache__', 'node_modules', '.venv', 'venv', '.tox', 'dist', 'build', '.egg-info'}
79
- ignore_exts = {'.pyc', '.pyo', '.so', '.o', '.a', '.dylib'}
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
- if depth > max_depth:
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
- for i, d in enumerate(dirs):
102
- is_last = (i == len(dirs) - 1) and not files
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
- for i, f in enumerate(files[:10]): # Limit files shown
108
- is_last = i == len(files[:10]) - 1
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, Handoff
75
+ from praisonaiagents import Agent, handoff
76
76
 
77
77
  agents = []
78
78
 
79
- # Create agents for each role
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 between agents
95
- for i, agent in enumerate(agents[:-1]):
96
- next_agent = agents[i + 1]
97
- handoff = Handoff(
98
- agent=next_agent,
99
- tool_name_override=f"handoff_to_{agent_names[i + 1]}",
100
- tool_description_override=f"Hand off task to {agent_names[i + 1]} for further processing"
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
- # Add handoff to agent
104
- if not hasattr(agent, 'handoffs') or agent.handoffs is None:
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(agents)} agents with handoff chain", "success")
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 agents
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: