praisonai-code 0.0.1__py3-none-any.whl

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 (309) hide show
  1. praisonai_code/__init__.py +17 -0
  2. praisonai_code/cli/__init__.py +12 -0
  3. praisonai_code/cli/_forward_shim.py +10 -0
  4. praisonai_code/cli/_paths.py +88 -0
  5. praisonai_code/cli/_warnings.py +58 -0
  6. praisonai_code/cli/app.py +757 -0
  7. praisonai_code/cli/approval_backend.py +272 -0
  8. praisonai_code/cli/branding.py +94 -0
  9. praisonai_code/cli/commands/__init__.py +114 -0
  10. praisonai_code/cli/commands/acp.py +80 -0
  11. praisonai_code/cli/commands/agent.py +116 -0
  12. praisonai_code/cli/commands/agents.py +80 -0
  13. praisonai_code/cli/commands/app.py +139 -0
  14. praisonai_code/cli/commands/attach.py +95 -0
  15. praisonai_code/cli/commands/audit.py +102 -0
  16. praisonai_code/cli/commands/auth.py +508 -0
  17. praisonai_code/cli/commands/batch.py +848 -0
  18. praisonai_code/cli/commands/benchmark.py +286 -0
  19. praisonai_code/cli/commands/browser.py +299 -0
  20. praisonai_code/cli/commands/call.py +45 -0
  21. praisonai_code/cli/commands/chat.py +332 -0
  22. praisonai_code/cli/commands/checkpoint.py +170 -0
  23. praisonai_code/cli/commands/code.py +276 -0
  24. praisonai_code/cli/commands/command.py +114 -0
  25. praisonai_code/cli/commands/commit.py +47 -0
  26. praisonai_code/cli/commands/completion.py +333 -0
  27. praisonai_code/cli/commands/config.py +681 -0
  28. praisonai_code/cli/commands/context.py +414 -0
  29. praisonai_code/cli/commands/daemon.py +203 -0
  30. praisonai_code/cli/commands/debug.py +142 -0
  31. praisonai_code/cli/commands/deploy.py +71 -0
  32. praisonai_code/cli/commands/diag.py +55 -0
  33. praisonai_code/cli/commands/docs.py +1575 -0
  34. praisonai_code/cli/commands/doctor.py +332 -0
  35. praisonai_code/cli/commands/endpoints.py +51 -0
  36. praisonai_code/cli/commands/environment.py +179 -0
  37. praisonai_code/cli/commands/eval.py +131 -0
  38. praisonai_code/cli/commands/examples.py +953 -0
  39. praisonai_code/cli/commands/flow.py +436 -0
  40. praisonai_code/cli/commands/github.py +752 -0
  41. praisonai_code/cli/commands/hooks.py +74 -0
  42. praisonai_code/cli/commands/init.py +174 -0
  43. praisonai_code/cli/commands/knowledge.py +440 -0
  44. praisonai_code/cli/commands/langextract.py +120 -0
  45. praisonai_code/cli/commands/langfuse.py +984 -0
  46. praisonai_code/cli/commands/loop.py +211 -0
  47. praisonai_code/cli/commands/lsp.py +112 -0
  48. praisonai_code/cli/commands/managed.py +659 -0
  49. praisonai_code/cli/commands/mcp.py +763 -0
  50. praisonai_code/cli/commands/memory.py +298 -0
  51. praisonai_code/cli/commands/models.py +264 -0
  52. praisonai_code/cli/commands/n8n.py +326 -0
  53. praisonai_code/cli/commands/obs.py +19 -0
  54. praisonai_code/cli/commands/package.py +76 -0
  55. praisonai_code/cli/commands/paths.py +106 -0
  56. praisonai_code/cli/commands/permissions.py +272 -0
  57. praisonai_code/cli/commands/plugins.py +609 -0
  58. praisonai_code/cli/commands/port.py +530 -0
  59. praisonai_code/cli/commands/profile.py +466 -0
  60. praisonai_code/cli/commands/publish.py +193 -0
  61. praisonai_code/cli/commands/rag.py +913 -0
  62. praisonai_code/cli/commands/realtime.py +52 -0
  63. praisonai_code/cli/commands/recipe.py +684 -0
  64. praisonai_code/cli/commands/registry.py +59 -0
  65. praisonai_code/cli/commands/replay.py +830 -0
  66. praisonai_code/cli/commands/research.py +49 -0
  67. praisonai_code/cli/commands/retrieval.py +377 -0
  68. praisonai_code/cli/commands/rules.py +71 -0
  69. praisonai_code/cli/commands/run.py +1573 -0
  70. praisonai_code/cli/commands/sandbox.py +371 -0
  71. praisonai_code/cli/commands/schedule.py +529 -0
  72. praisonai_code/cli/commands/serve.py +690 -0
  73. praisonai_code/cli/commands/session.py +450 -0
  74. praisonai_code/cli/commands/setup.py +174 -0
  75. praisonai_code/cli/commands/skills.py +545 -0
  76. praisonai_code/cli/commands/standardise.py +711 -0
  77. praisonai_code/cli/commands/templates.py +54 -0
  78. praisonai_code/cli/commands/test.py +558 -0
  79. praisonai_code/cli/commands/todo.py +74 -0
  80. praisonai_code/cli/commands/tools.py +205 -0
  81. praisonai_code/cli/commands/traces.py +145 -0
  82. praisonai_code/cli/commands/tracker.py +852 -0
  83. praisonai_code/cli/commands/train.py +613 -0
  84. praisonai_code/cli/commands/ui.py +172 -0
  85. praisonai_code/cli/commands/up.py +354 -0
  86. praisonai_code/cli/commands/validate.py +291 -0
  87. praisonai_code/cli/commands/version.py +101 -0
  88. praisonai_code/cli/commands/workflow.py +97 -0
  89. praisonai_code/cli/config_loader.py +437 -0
  90. praisonai_code/cli/configuration/__init__.py +27 -0
  91. praisonai_code/cli/configuration/config.schema.json +57 -0
  92. praisonai_code/cli/configuration/credentials.py +446 -0
  93. praisonai_code/cli/configuration/loader.py +364 -0
  94. praisonai_code/cli/configuration/model_resolver.py +161 -0
  95. praisonai_code/cli/configuration/oauth.py +389 -0
  96. praisonai_code/cli/configuration/paths.py +224 -0
  97. praisonai_code/cli/configuration/resolver.py +687 -0
  98. praisonai_code/cli/configuration/schema.py +317 -0
  99. praisonai_code/cli/execution/__init__.py +99 -0
  100. praisonai_code/cli/execution/core.py +208 -0
  101. praisonai_code/cli/execution/profiler.py +898 -0
  102. praisonai_code/cli/execution/request.py +85 -0
  103. praisonai_code/cli/execution/result.py +74 -0
  104. praisonai_code/cli/fallback_schema.py +416 -0
  105. praisonai_code/cli/features/__init__.py +278 -0
  106. praisonai_code/cli/features/_endpoint_registry.py +64 -0
  107. praisonai_code/cli/features/_search_registry.py +43 -0
  108. praisonai_code/cli/features/acp.py +236 -0
  109. praisonai_code/cli/features/action_orchestrator.py +576 -0
  110. praisonai_code/cli/features/agent_scheduler.py +773 -0
  111. praisonai_code/cli/features/agent_tools.py +603 -0
  112. praisonai_code/cli/features/agents.py +397 -0
  113. praisonai_code/cli/features/at_mentions.py +471 -0
  114. praisonai_code/cli/features/audit_cli.py +270 -0
  115. praisonai_code/cli/features/auto_memory.py +182 -0
  116. praisonai_code/cli/features/auto_mode.py +552 -0
  117. praisonai_code/cli/features/autonomy_mode.py +546 -0
  118. praisonai_code/cli/features/background.py +356 -0
  119. praisonai_code/cli/features/base.py +168 -0
  120. praisonai_code/cli/features/benchmark.py +1462 -0
  121. praisonai_code/cli/features/capabilities.py +1326 -0
  122. praisonai_code/cli/features/checkpoints.py +345 -0
  123. praisonai_code/cli/features/cli_profiler.py +335 -0
  124. praisonai_code/cli/features/code_intelligence.py +666 -0
  125. praisonai_code/cli/features/compaction.py +294 -0
  126. praisonai_code/cli/features/compare.py +534 -0
  127. praisonai_code/cli/features/config_hierarchy.py +366 -0
  128. praisonai_code/cli/features/context_manager.py +597 -0
  129. praisonai_code/cli/features/cost_tracker.py +514 -0
  130. praisonai_code/cli/features/csv_test_runner.py +736 -0
  131. praisonai_code/cli/features/custom_definitions.py +790 -0
  132. praisonai_code/cli/features/debug.py +810 -0
  133. praisonai_code/cli/features/deploy.py +605 -0
  134. praisonai_code/cli/features/diag.py +289 -0
  135. praisonai_code/cli/features/display_jsonl.py +173 -0
  136. praisonai_code/cli/features/doctor/__init__.py +63 -0
  137. praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
  138. praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
  139. praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
  140. praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
  141. praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
  142. praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
  143. praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
  144. praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
  145. praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
  146. praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
  147. praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
  148. praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
  149. praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
  150. praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
  151. praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
  152. praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
  153. praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
  154. praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
  155. praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
  156. praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
  157. praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
  158. praisonai_code/cli/features/doctor/engine.py +266 -0
  159. praisonai_code/cli/features/doctor/formatters.py +377 -0
  160. praisonai_code/cli/features/doctor/handler.py +564 -0
  161. praisonai_code/cli/features/doctor/models.py +276 -0
  162. praisonai_code/cli/features/doctor/registry.py +239 -0
  163. praisonai_code/cli/features/endpoints.py +1016 -0
  164. praisonai_code/cli/features/eval.py +559 -0
  165. praisonai_code/cli/features/examples.py +707 -0
  166. praisonai_code/cli/features/external_agents.py +231 -0
  167. praisonai_code/cli/features/fast_context.py +410 -0
  168. praisonai_code/cli/features/file_history.py +320 -0
  169. praisonai_code/cli/features/flow_display.py +566 -0
  170. praisonai_code/cli/features/git_attribution.py +159 -0
  171. praisonai_code/cli/features/git_integration.py +651 -0
  172. praisonai_code/cli/features/guardrail.py +171 -0
  173. praisonai_code/cli/features/handoff.py +252 -0
  174. praisonai_code/cli/features/hooks.py +583 -0
  175. praisonai_code/cli/features/hybrid_workflow.py +391 -0
  176. praisonai_code/cli/features/image.py +384 -0
  177. praisonai_code/cli/features/interactive_core_headless.py +450 -0
  178. praisonai_code/cli/features/interactive_runtime.py +600 -0
  179. praisonai_code/cli/features/interactive_test_harness.py +537 -0
  180. praisonai_code/cli/features/interactive_tools.py +428 -0
  181. praisonai_code/cli/features/interactive_tui.py +603 -0
  182. praisonai_code/cli/features/job_workflow.py +906 -0
  183. praisonai_code/cli/features/jobs.py +632 -0
  184. praisonai_code/cli/features/knowledge.py +531 -0
  185. praisonai_code/cli/features/knowledge_cli.py +438 -0
  186. praisonai_code/cli/features/lite.py +244 -0
  187. praisonai_code/cli/features/logs.py +200 -0
  188. praisonai_code/cli/features/lsp_cli.py +225 -0
  189. praisonai_code/cli/features/lsp_diagnostics.py +185 -0
  190. praisonai_code/cli/features/mcp.py +344 -0
  191. praisonai_code/cli/features/message_queue.py +587 -0
  192. praisonai_code/cli/features/metrics.py +210 -0
  193. praisonai_code/cli/features/migrate.py +1329 -0
  194. praisonai_code/cli/features/migration_flow.py +463 -0
  195. praisonai_code/cli/features/migration_spec.py +276 -0
  196. praisonai_code/cli/features/n8n.py +703 -0
  197. praisonai_code/cli/features/observability.py +293 -0
  198. praisonai_code/cli/features/ollama.py +361 -0
  199. praisonai_code/cli/features/output_modes.py +155 -0
  200. praisonai_code/cli/features/output_style.py +273 -0
  201. praisonai_code/cli/features/package.py +631 -0
  202. praisonai_code/cli/features/performance.py +308 -0
  203. praisonai_code/cli/features/persistence.py +636 -0
  204. praisonai_code/cli/features/profiler/__init__.py +81 -0
  205. praisonai_code/cli/features/profiler/core.py +558 -0
  206. praisonai_code/cli/features/profiler/optimizations.py +652 -0
  207. praisonai_code/cli/features/profiler/suite.py +386 -0
  208. praisonai_code/cli/features/queue/__init__.py +73 -0
  209. praisonai_code/cli/features/queue/manager.py +435 -0
  210. praisonai_code/cli/features/queue/models.py +289 -0
  211. praisonai_code/cli/features/queue/persistence.py +564 -0
  212. praisonai_code/cli/features/queue/scheduler.py +529 -0
  213. praisonai_code/cli/features/queue/worker.py +400 -0
  214. praisonai_code/cli/features/recipe.py +2187 -0
  215. praisonai_code/cli/features/recipe_creator.py +996 -0
  216. praisonai_code/cli/features/recipe_optimizer.py +1364 -0
  217. praisonai_code/cli/features/recipe_prompts.py +226 -0
  218. praisonai_code/cli/features/registry.py +229 -0
  219. praisonai_code/cli/features/repo_map.py +860 -0
  220. praisonai_code/cli/features/router.py +466 -0
  221. praisonai_code/cli/features/safe_shell.py +427 -0
  222. praisonai_code/cli/features/sandbox_cli.py +283 -0
  223. praisonai_code/cli/features/sandbox_executor.py +536 -0
  224. praisonai_code/cli/features/sdk_knowledge.py +500 -0
  225. praisonai_code/cli/features/session.py +222 -0
  226. praisonai_code/cli/features/session_checkpoints.py +208 -0
  227. praisonai_code/cli/features/setup/__init__.py +9 -0
  228. praisonai_code/cli/features/setup/handler.py +355 -0
  229. praisonai_code/cli/features/setup/templates.py +62 -0
  230. praisonai_code/cli/features/skills.py +940 -0
  231. praisonai_code/cli/features/slash_commands.py +692 -0
  232. praisonai_code/cli/features/telemetry.py +179 -0
  233. praisonai_code/cli/features/templates.py +1390 -0
  234. praisonai_code/cli/features/thinking.py +343 -0
  235. praisonai_code/cli/features/todo.py +334 -0
  236. praisonai_code/cli/features/tools.py +680 -0
  237. praisonai_code/cli/features/tui/__init__.py +83 -0
  238. praisonai_code/cli/features/tui/app.py +871 -0
  239. praisonai_code/cli/features/tui/cli.py +580 -0
  240. praisonai_code/cli/features/tui/config.py +150 -0
  241. praisonai_code/cli/features/tui/debug.py +526 -0
  242. praisonai_code/cli/features/tui/events.py +99 -0
  243. praisonai_code/cli/features/tui/mock_provider.py +328 -0
  244. praisonai_code/cli/features/tui/orchestrator.py +652 -0
  245. praisonai_code/cli/features/tui/screens/__init__.py +50 -0
  246. praisonai_code/cli/features/tui/screens/help.py +157 -0
  247. praisonai_code/cli/features/tui/screens/main.py +568 -0
  248. praisonai_code/cli/features/tui/screens/queue.py +174 -0
  249. praisonai_code/cli/features/tui/screens/session.py +124 -0
  250. praisonai_code/cli/features/tui/screens/settings.py +148 -0
  251. praisonai_code/cli/features/tui/session_store.py +198 -0
  252. praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
  253. praisonai_code/cli/features/tui/widgets/chat.py +263 -0
  254. praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
  255. praisonai_code/cli/features/tui/widgets/composer.py +292 -0
  256. praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
  257. praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
  258. praisonai_code/cli/features/tui/widgets/status.py +181 -0
  259. praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
  260. praisonai_code/cli/features/wizard.py +289 -0
  261. praisonai_code/cli/features/workflow.py +802 -0
  262. praisonai_code/cli/features/yaml_utils.py +321 -0
  263. praisonai_code/cli/interactive/__init__.py +48 -0
  264. praisonai_code/cli/interactive/async_tui.py +1218 -0
  265. praisonai_code/cli/interactive/config.py +139 -0
  266. praisonai_code/cli/interactive/core.py +618 -0
  267. praisonai_code/cli/interactive/events.py +131 -0
  268. praisonai_code/cli/interactive/frontends/__init__.py +31 -0
  269. praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
  270. praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
  271. praisonai_code/cli/interactive/praison_io.py +502 -0
  272. praisonai_code/cli/interactive/repl.py +297 -0
  273. praisonai_code/cli/interactive/split_tui.py +456 -0
  274. praisonai_code/cli/interactive/tui_app.py +457 -0
  275. praisonai_code/cli/langfuse_client.py +360 -0
  276. praisonai_code/cli/main.py +7421 -0
  277. praisonai_code/cli/output/__init__.py +25 -0
  278. praisonai_code/cli/output/console.py +456 -0
  279. praisonai_code/cli/output/event_bridge.py +191 -0
  280. praisonai_code/cli/schedule_cli.py +54 -0
  281. praisonai_code/cli/schema_provider.py +23 -0
  282. praisonai_code/cli/session/__init__.py +16 -0
  283. praisonai_code/cli/session/resume.py +148 -0
  284. praisonai_code/cli/session/unified.py +548 -0
  285. praisonai_code/cli/state/__init__.py +31 -0
  286. praisonai_code/cli/state/identifiers.py +161 -0
  287. praisonai_code/cli/state/project_sessions.py +383 -0
  288. praisonai_code/cli/state/sessions.py +390 -0
  289. praisonai_code/cli/ui/__init__.py +160 -0
  290. praisonai_code/cli/ui/config.py +46 -0
  291. praisonai_code/cli/ui/events.py +61 -0
  292. praisonai_code/cli/ui/mg_backend.py +342 -0
  293. praisonai_code/cli/ui/plain.py +133 -0
  294. praisonai_code/cli/ui/rich_backend.py +162 -0
  295. praisonai_code/cli/unified_schema.py +655 -0
  296. praisonai_code/cli/utils/env_utils.py +126 -0
  297. praisonai_code/cli/utils/project.py +131 -0
  298. praisonai_code/cli_backends/__init__.py +73 -0
  299. praisonai_code/cli_backends/claude.py +373 -0
  300. praisonai_code/cli_backends/registry.py +113 -0
  301. praisonai_code/runtime/__init__.py +36 -0
  302. praisonai_code/runtime/__main__.py +81 -0
  303. praisonai_code/runtime/client.py +131 -0
  304. praisonai_code/runtime/descriptor.py +209 -0
  305. praisonai_code/runtime/server.py +356 -0
  306. praisonai_code-0.0.1.dist-info/METADATA +80 -0
  307. praisonai_code-0.0.1.dist-info/RECORD +309 -0
  308. praisonai_code-0.0.1.dist-info/WHEEL +5 -0
  309. praisonai_code-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1364 @@
1
+ """
2
+ Recipe Optimizer for PraisonAI.
3
+
4
+ Iteratively optimizes recipes using LLM-as-judge feedback.
5
+ Runs the recipe, judges the output, proposes improvements, and applies them.
6
+
7
+ DRY: Reuses ContextEffectivenessJudge from replay module.
8
+ Reuses SDK knowledge and tool categories from recipe_creator.
9
+ Reuses image reading and URL crawling from judge module.
10
+ """
11
+
12
+ import os
13
+ import re
14
+ import logging
15
+ import base64
16
+ from pathlib import Path
17
+ from typing import Optional, List, Tuple, Any, Dict
18
+
19
+ # DRY: Import SDK knowledge from shared modules
20
+ from .sdk_knowledge import get_sdk_knowledge_prompt
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def _read_image_for_optimizer(image_path: str, model: str = "gpt-4o-mini") -> str:
26
+ """
27
+ Read and describe an image for optimizer context.
28
+
29
+ Uses vision LLM to describe the image content so the optimizer can
30
+ validate if agent outputs match the actual image.
31
+
32
+ Args:
33
+ image_path: Local file path or URL to the image
34
+ model: Vision-capable model to use
35
+
36
+ Returns:
37
+ Description of the image content
38
+ """
39
+ try:
40
+ import litellm
41
+
42
+ # Prepare image content
43
+ if image_path.startswith(('http://', 'https://')):
44
+ # URL - pass directly
45
+ image_content = {
46
+ "type": "image_url",
47
+ "image_url": {"url": image_path}
48
+ }
49
+ elif os.path.exists(image_path):
50
+ # Local file - encode as base64
51
+ with open(image_path, 'rb') as f:
52
+ image_data = base64.b64encode(f.read()).decode('utf-8')
53
+
54
+ # Detect mime type
55
+ ext = os.path.splitext(image_path)[1].lower()
56
+ mime_types = {
57
+ '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
58
+ '.png': 'image/png', '.gif': 'image/gif',
59
+ '.webp': 'image/webp', '.bmp': 'image/bmp'
60
+ }
61
+ mime_type = mime_types.get(ext, 'image/jpeg')
62
+
63
+ image_content = {
64
+ "type": "image_url",
65
+ "image_url": {"url": f"data:{mime_type};base64,{image_data}"}
66
+ }
67
+ else:
68
+ return f"[Image not found: {image_path}]"
69
+
70
+ # Use vision model to describe the image
71
+ response = litellm.completion(
72
+ model=model,
73
+ messages=[{
74
+ "role": "user",
75
+ "content": [
76
+ {"type": "text", "text": "Describe this image in detail. Include: main subject, colors, text visible, key elements, and overall composition. Be factual and specific."},
77
+ image_content
78
+ ]
79
+ }],
80
+ max_tokens=500,
81
+ )
82
+
83
+ description = response.choices[0].message.content or ""
84
+ return f"[IMAGE DESCRIPTION: {description}]"
85
+
86
+ except Exception as e:
87
+ logger.warning(f"Failed to read image for optimizer: {e}")
88
+ return f"[Failed to read image: {str(e)[:100]}]"
89
+
90
+
91
+ def _crawl_url_for_optimizer(url: str) -> str:
92
+ """
93
+ Crawl a URL to get content for optimizer context.
94
+
95
+ Uses the web_crawl tool to extract content so the optimizer can
96
+ validate if agent outputs match the actual URL content.
97
+
98
+ Args:
99
+ url: URL to crawl
100
+
101
+ Returns:
102
+ Extracted content from the URL (truncated)
103
+ """
104
+ try:
105
+ # Try to use the web_crawl tool
106
+ from praisonaiagents.tools import web_crawl
107
+
108
+ result = web_crawl(url)
109
+
110
+ if isinstance(result, dict):
111
+ if result.get('error'):
112
+ return f"[URL crawl error: {result.get('error')}]"
113
+ content = result.get('content', '')
114
+ title = result.get('title', '')
115
+ provider = result.get('provider', 'unknown')
116
+
117
+ # Truncate content for optimizer context
118
+ if len(content) > 3000:
119
+ content = content[:3000] + "... [truncated]"
120
+
121
+ return f"[URL CONTENT (via {provider})]\nTitle: {title}\n\n{content}"
122
+
123
+ return f"[URL content: {str(result)[:3000]}]"
124
+
125
+ except ImportError:
126
+ # Fallback to basic HTTP fetch
127
+ try:
128
+ import urllib.request
129
+
130
+ with urllib.request.urlopen(url, timeout=30) as response:
131
+ content = response.read().decode('utf-8', errors='ignore')
132
+
133
+ # Basic HTML to text
134
+ content = re.sub(r'<script[^>]*>.*?</script>', '', content, flags=re.DOTALL | re.IGNORECASE)
135
+ content = re.sub(r'<style[^>]*>.*?</style>', '', content, flags=re.DOTALL | re.IGNORECASE)
136
+ content = re.sub(r'<[^>]+>', ' ', content)
137
+ content = re.sub(r'\s+', ' ', content).strip()
138
+
139
+ if len(content) > 3000:
140
+ content = content[:3000] + "... [truncated]"
141
+
142
+ return f"[URL CONTENT (via urllib)]\n{content}"
143
+
144
+ except Exception as e:
145
+ return f"[Failed to crawl URL: {str(e)[:100]}]"
146
+ except Exception as e:
147
+ logger.warning(f"Failed to crawl URL for optimizer: {e}")
148
+ return f"[Failed to crawl URL: {str(e)[:100]}]"
149
+
150
+
151
+ # Common issue patterns and their fixes - comprehensive patterns
152
+ COMMON_ISSUE_FIXES = {
153
+ "tool_not_called": {
154
+ "pattern": r"(failed to call|did not call|without calling|tool.*not.*used|did not use)",
155
+ "fix": "Add explicit tool call instruction: 'You MUST call the {tool} tool. Do NOT respond without calling {tool} first. Return [format] from the tool results.'",
156
+ "severity": "critical",
157
+ },
158
+ "missing_output_format": {
159
+ "pattern": r"(output.*format|expected.*format|format.*missing|unstructured|no format)",
160
+ "fix": "Add explicit expected_output format: 'Return a structured response with: 1) [section1], 2) [section2], 3) [section3]'",
161
+ "severity": "high",
162
+ },
163
+ "hallucination": {
164
+ "pattern": r"(hallucin|fabricat|made up|incorrect.*fact|invented|false information)",
165
+ "fix": "Add grounding instruction: 'Only use information from the provided context or tool results. Do NOT make up facts, statistics, or sources.'",
166
+ "severity": "critical",
167
+ },
168
+ "truncation": {
169
+ "pattern": r"(truncat|incomplete|cut off|partial|missing.*content)",
170
+ "fix": "Add completeness instruction: 'Ensure your response is COMPLETE. Include ALL required sections. Do not stop mid-response.'",
171
+ "severity": "high",
172
+ },
173
+ "context_not_used": {
174
+ "pattern": r"(context.*not.*used|ignored.*context|failed.*utilize|did not reference)",
175
+ "fix": "Add context reference: 'Use the information from {{previous_agent}}_output. Extract specific data points and reference them in your response.'",
176
+ "severity": "high",
177
+ },
178
+ "missing_goal": {
179
+ "pattern": r"(missing.*goal|no.*goal|goal.*empty|unclear.*objective)",
180
+ "fix": "Add clear goal: 'Your goal is to [specific objective]. Success means [measurable outcome].'",
181
+ "severity": "critical",
182
+ },
183
+ "vague_action": {
184
+ "pattern": r"(vague|unclear|ambiguous|not specific|too general)",
185
+ "fix": "Make action specific: Replace vague verbs with concrete instructions. Include exact tool names, expected counts, and output format.",
186
+ "severity": "high",
187
+ },
188
+ "missing_verification": {
189
+ "pattern": r"(no verification|unverified|not validated|needs.*check)",
190
+ "fix": "Add verification step: 'Verify your output contains all required elements before responding.'",
191
+ "severity": "medium",
192
+ },
193
+ "manager_rejected": {
194
+ "pattern": r"(manager rejected|hierarchical.*failed|validation.*failed|step.*rejected)",
195
+ "fix": "Make tool calls explicit and mandatory. Add: 'You MUST call [tool]. Do NOT respond without calling [tool] first. Return [format] from the tool results.'",
196
+ "severity": "critical",
197
+ },
198
+ "hierarchical_failure": {
199
+ "pattern": r"(status.*failed|failure_reason|workflow.*stopped)",
200
+ "fix": "Strengthen action instructions: explicit tool calls, specific output format, grounding against hallucination.",
201
+ "severity": "critical",
202
+ },
203
+ }
204
+
205
+ # Comprehensive tool documentation for optimizer
206
+ TOOL_DOCUMENTATION = """
207
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
208
+ AVAILABLE TOOLS (Use exact names)
209
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
210
+
211
+ ## WEB SEARCH (Safe - No approval required):
212
+ - `search_web` - ⭐ RECOMMENDED - Unified search with auto-fallback
213
+ - `internet_search` - DuckDuckGo search (FREE, no API key)
214
+ - `tavily_search` - High-quality AI search (needs TAVILY_API_KEY)
215
+ - `exa_search` - Exa AI search (needs EXA_API_KEY)
216
+
217
+ ## WEB SCRAPING (Safe):
218
+ - `scrape_page` - Scrape web page content
219
+ - `extract_links` - Extract links from URL
220
+ - `crawl4ai` - Advanced web crawling
221
+
222
+ ## FILE TOOLS - READ (Safe):
223
+ - `read_file` - Read local files
224
+ - `list_files` - List directory contents
225
+
226
+ ## FILE TOOLS - WRITE (⚠️ APPROVAL REQUIRED):
227
+ - `write_file` - Write to files
228
+ - `delete_file` - Delete files
229
+
230
+ ## CODE EXECUTION (⚠️ APPROVAL REQUIRED):
231
+ - `execute_command` - Run shell commands
232
+ - `execute_code` - Execute Python code
233
+
234
+ ## DATA PROCESSING (Safe):
235
+ - `read_csv`, `read_json`, `read_yaml`, `read_excel`
236
+ """
237
+
238
+ # Hierarchical process documentation for optimizer
239
+ HIERARCHICAL_PROCESS_DOC = """
240
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
241
+ HIERARCHICAL PROCESS (CRITICAL - MUST PRESERVE)
242
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
243
+
244
+ ## What is Hierarchical Process?
245
+ In hierarchical mode, a MANAGER agent validates each step's output before
246
+ proceeding to the next step. If the manager rejects a step, the workflow
247
+ stops with status='failed' and includes a failure_reason.
248
+
249
+ ## MANDATORY YAML Fields (NEVER REMOVE):
250
+ ```yaml
251
+ process: hierarchical
252
+ manager_llm: gpt-4o-mini
253
+ ```
254
+
255
+ ## Why Hierarchical Matters:
256
+ 1. **Quality Control**: Manager validates each step's output
257
+ 2. **Tool Call Enforcement**: Manager rejects steps where tools weren't called
258
+ 3. **Prevents Cascading Errors**: Bad outputs don't propagate to next steps
259
+ 4. **Hallucination Detection**: Manager catches fabricated data
260
+
261
+ ## Hierarchical-Specific Optimization Rules:
262
+ 1. **Tool calls are MANDATORY** - Manager will reject if tools aren't called
263
+ 2. **Output must match expected_output** - Manager validates format
264
+ 3. **Context must be used** - Manager checks if previous output was utilized
265
+ 4. **No hallucination** - Manager rejects fabricated data
266
+
267
+ ## Common Manager Rejection Reasons:
268
+ - "Agent did not call the required tool"
269
+ - "Output does not match expected format"
270
+ - "Agent did not use context from previous step"
271
+ - "Output contains fabricated/unverified information"
272
+
273
+ ## How to Fix Manager Rejections:
274
+ 1. Make tool calls EXPLICIT: "You MUST call search_web. Do NOT respond without calling it."
275
+ 2. Specify exact output format in expected_output
276
+ 3. Reference previous output: "Using {{agent}}_output, extract..."
277
+ 4. Add grounding: "Only use information from tool results. Do NOT fabricate."
278
+ """
279
+
280
+ # Specialized agent documentation
281
+ SPECIALIZED_AGENTS_DOC = """
282
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
283
+ SPECIALIZED AGENT TYPES
284
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
285
+
286
+ ## AudioAgent - Text-to-Speech / Speech-to-Text
287
+ - Use `agent: AudioAgent` in agent definition
288
+ - TTS: llm: openai/tts-1 or openai/tts-1-hd
289
+ - STT: llm: openai/whisper-1 or groq/whisper-large-v3
290
+
291
+ ## ImageAgent - Image Generation
292
+ - Use `agent: ImageAgent` in agent definition
293
+ - llm: openai/dall-e-3 or openai/dall-e-2
294
+
295
+ ## VideoAgent - Video Generation
296
+ - Use `agent: VideoAgent` in agent definition
297
+ - llm: openai/sora-2 or gemini/veo-3.0-generate-preview
298
+
299
+ ## OCRAgent - Text Extraction from Documents
300
+ - Use `agent: OCRAgent` in agent definition
301
+ - llm: mistral/mistral-ocr-latest (needs MISTRAL_API_KEY)
302
+
303
+ ## DeepResearchAgent - Comprehensive Research
304
+ - Use `agent: DeepResearchAgent` in agent definition
305
+ """
306
+
307
+
308
+ class RecipeOptimizer:
309
+ """
310
+ Iteratively optimizes recipes using judge feedback.
311
+
312
+ Supports dynamic judge injection for domain-agnostic optimization:
313
+ - Recipe/workflow optimization (default)
314
+ - Water flow optimization
315
+ - Data pipeline optimization
316
+ - Any custom domain via JudgeCriteriaConfig
317
+
318
+ Usage:
319
+ # Default usage (backward compatible)
320
+ optimizer = RecipeOptimizer(max_iterations=3, score_threshold=8.0)
321
+ final_report = optimizer.optimize(recipe_path)
322
+
323
+ # Custom judge for different domains
324
+ from praisonaiagents.eval import Judge, JudgeCriteriaConfig
325
+
326
+ water_config = JudgeCriteriaConfig(
327
+ name="water_flow",
328
+ description="Evaluate water flow optimization",
329
+ prompt_template="Is the water flow optimal? {output}",
330
+ scoring_dimensions=["flow_rate", "pressure", "efficiency"],
331
+ )
332
+ water_judge = Judge(criteria_config=water_config)
333
+ optimizer = RecipeOptimizer(judge=water_judge)
334
+ """
335
+
336
+ def __init__(
337
+ self,
338
+ max_iterations: int = 3,
339
+ score_threshold: float = 8.0,
340
+ model: Optional[str] = None,
341
+ judge: Optional[Any] = None,
342
+ rules: Optional[List[Any]] = None,
343
+ criteria: Optional[str] = None,
344
+ ):
345
+ """
346
+ Initialize the optimizer.
347
+
348
+ Args:
349
+ max_iterations: Maximum optimization iterations
350
+ score_threshold: Score threshold to stop optimization (1-10)
351
+ model: LLM model for judging and optimization
352
+ judge: Optional custom judge implementing JudgeProtocol (for domain-agnostic use)
353
+ rules: Optional list of optimization rules implementing OptimizationRuleProtocol
354
+ criteria: Optional custom criteria string for evaluation
355
+ """
356
+ self.max_iterations = max_iterations
357
+ self.score_threshold = score_threshold
358
+ self.model = model or os.getenv("OPENAI_MODEL_NAME", "gpt-4o-mini")
359
+ self.custom_judge = judge
360
+ self.custom_rules = rules or []
361
+ self.custom_criteria = criteria
362
+
363
+ def _get_litellm(self):
364
+ """Lazy import litellm."""
365
+ try:
366
+ import litellm
367
+ return litellm
368
+ except ImportError:
369
+ raise ImportError(
370
+ "litellm is required for recipe optimization. "
371
+ "Install with: pip install litellm"
372
+ )
373
+
374
+ def should_continue(self, report: Any, iteration: int) -> bool:
375
+ """
376
+ Determine if optimization should continue.
377
+
378
+ Args:
379
+ report: JudgeReport from last iteration
380
+ iteration: Current iteration number (1-indexed)
381
+
382
+ Returns:
383
+ True if should continue, False if should stop
384
+ """
385
+ # Stop if max iterations reached
386
+ if iteration >= self.max_iterations:
387
+ return False
388
+
389
+ # Stop if score threshold reached
390
+ if hasattr(report, 'overall_score') and report.overall_score >= self.score_threshold:
391
+ return False
392
+
393
+ return True
394
+
395
+ def run_iteration(
396
+ self,
397
+ recipe_path: Path,
398
+ input_data: str = "",
399
+ iteration: int = 1,
400
+ ) -> Tuple[Any, str]:
401
+ """
402
+ Run one optimization iteration: execute recipe and judge output.
403
+
404
+ Args:
405
+ recipe_path: Path to recipe folder
406
+ input_data: Input data for recipe
407
+ iteration: Current iteration number
408
+
409
+ Returns:
410
+ Tuple of (JudgeReport, trace_id)
411
+ """
412
+ from praisonai import recipe
413
+ from praisonai.replay import ContextTraceReader, ContextEffectivenessJudge
414
+
415
+ # Generate unique trace name for this iteration
416
+ trace_name = f"{recipe_path.name}-opt-{iteration}"
417
+
418
+ # Run the recipe with trace saving
419
+ logger.debug(f"Running recipe iteration {iteration} with trace: {trace_name}")
420
+ try:
421
+ _result = recipe.run(
422
+ str(recipe_path),
423
+ input={"input": input_data} if input_data else {},
424
+ options={
425
+ "save_replay": True,
426
+ "trace_name": trace_name,
427
+ }
428
+ )
429
+ logger.debug(f"Recipe run completed with status: {getattr(_result, 'status', 'unknown')}")
430
+ except Exception as e:
431
+ logger.warning(f"Recipe run failed for iteration {iteration}: {e}")
432
+ # Return a minimal report on failure
433
+ from praisonai.replay.judge import JudgeReport
434
+ return JudgeReport(
435
+ session_id=trace_name,
436
+ timestamp="",
437
+ total_agents=0,
438
+ overall_score=5.0,
439
+ agent_scores=[],
440
+ summary=f"Recipe execution failed: {e}",
441
+ recommendations=["Fix recipe execution errors"],
442
+ ), trace_name
443
+
444
+ # Judge the trace
445
+ reader = ContextTraceReader(trace_name)
446
+ events = reader.get_all()
447
+
448
+ if not events:
449
+ # Check if recipe result indicates an error
450
+ result_status = getattr(_result, 'status', None)
451
+ result_error = getattr(_result, 'error', None)
452
+ if result_error:
453
+ logger.warning(f"No events found for trace: {trace_name} (recipe error: {result_error})")
454
+ else:
455
+ logger.warning(f"No events found for trace: {trace_name} (status: {result_status})")
456
+ # Return a minimal report
457
+ from praisonai.replay.judge import JudgeReport
458
+ return JudgeReport(
459
+ session_id=trace_name,
460
+ timestamp="",
461
+ total_agents=0,
462
+ overall_score=5.0,
463
+ agent_scores=[],
464
+ summary="No events to judge",
465
+ recommendations=["Ensure recipe runs correctly"],
466
+ ), trace_name
467
+
468
+ # Run judge - use custom judge if provided, otherwise default
469
+ yaml_file = str(recipe_path / "agents.yaml")
470
+ if self.custom_judge is not None:
471
+ # Use custom judge for domain-agnostic evaluation
472
+ # Custom judge should implement JudgeProtocol
473
+ if hasattr(self.custom_judge, 'judge_trace'):
474
+ report = self.custom_judge.judge_trace(events, session_id=trace_name, yaml_file=yaml_file)
475
+ elif hasattr(self.custom_judge, 'run'):
476
+ # Use the run method with output from events
477
+ output = self._extract_output_from_events(events)
478
+ result = self.custom_judge.run(output=output)
479
+ # Convert to JudgeReport-like object
480
+ from praisonai.replay.judge import JudgeReport
481
+ report = JudgeReport(
482
+ session_id=trace_name,
483
+ timestamp="",
484
+ total_agents=len(events),
485
+ overall_score=result.score if hasattr(result, 'score') else 5.0,
486
+ agent_scores=[],
487
+ summary=result.reasoning if hasattr(result, 'reasoning') else "",
488
+ recommendations=result.suggestions if hasattr(result, 'suggestions') else [],
489
+ )
490
+ else:
491
+ logger.warning("Custom judge does not implement expected interface, using default")
492
+ judge = ContextEffectivenessJudge(model=self.model)
493
+ report = judge.judge_trace(events, session_id=trace_name, yaml_file=yaml_file)
494
+ else:
495
+ # Default: use ContextEffectivenessJudge
496
+ judge = ContextEffectivenessJudge(model=self.model)
497
+ report = judge.judge_trace(events, session_id=trace_name, yaml_file=yaml_file)
498
+
499
+ return report, trace_name
500
+
501
+ def _extract_output_from_events(self, events: List[Any]) -> str:
502
+ """
503
+ Extract output text from trace events for custom judge evaluation.
504
+
505
+ Args:
506
+ events: List of trace events
507
+
508
+ Returns:
509
+ Combined output text from all events
510
+ """
511
+ outputs = []
512
+ for event in events:
513
+ if hasattr(event, 'output'):
514
+ outputs.append(str(event.output))
515
+ elif hasattr(event, 'result'):
516
+ outputs.append(str(event.result))
517
+ elif hasattr(event, 'content'):
518
+ outputs.append(str(event.content))
519
+ return "\n\n".join(outputs) if outputs else ""
520
+
521
+ def _extract_agent_issues(self, report: Any) -> Dict[str, Dict[str, Any]]:
522
+ """
523
+ Extract detailed issues per agent from judge report.
524
+
525
+ Returns:
526
+ Dict mapping agent_name -> {scores, issues, suggestions}
527
+ """
528
+ agent_issues = {}
529
+
530
+ if hasattr(report, 'agent_scores'):
531
+ for score in report.agent_scores:
532
+ agent_name = getattr(score, 'agent_name', 'unknown')
533
+ agent_issues[agent_name] = {
534
+ 'task_score': getattr(score, 'task_achievement_score', 5.0),
535
+ 'context_score': getattr(score, 'context_utilization_score', 5.0),
536
+ 'quality_score': getattr(score, 'output_quality_score', 5.0),
537
+ 'instruction_score': getattr(score, 'instruction_following_score', 5.0),
538
+ 'hallucination_score': getattr(score, 'hallucination_score', 5.0),
539
+ 'error_score': getattr(score, 'error_handling_score', 5.0),
540
+ 'failure_detected': getattr(score, 'failure_detected', False),
541
+ 'failure_reason': getattr(score, 'failure_reason', ''),
542
+ 'suggestions': getattr(score, 'suggestions', []),
543
+ 'reasoning': getattr(score, 'reasoning', ''),
544
+ 'tool_evaluations': getattr(score, 'tool_evaluations', []),
545
+ }
546
+
547
+ return agent_issues
548
+
549
+ def _identify_fix_patterns(self, agent_issues: Dict[str, Dict[str, Any]]) -> List[Dict[str, Any]]:
550
+ """
551
+ Identify which common fix patterns apply based on issues.
552
+
553
+ Returns:
554
+ List of applicable fixes with agent context
555
+ """
556
+ applicable_fixes = []
557
+
558
+ for agent_name, issues in agent_issues.items():
559
+ reasoning = issues.get('reasoning', '').lower()
560
+ failure_reason = issues.get('failure_reason', '').lower()
561
+ combined_text = f"{reasoning} {failure_reason}"
562
+
563
+ for fix_name, fix_info in COMMON_ISSUE_FIXES.items():
564
+ if re.search(fix_info['pattern'], combined_text, re.IGNORECASE):
565
+ applicable_fixes.append({
566
+ 'agent': agent_name,
567
+ 'issue_type': fix_name,
568
+ 'fix_template': fix_info['fix'],
569
+ })
570
+
571
+ # Check for low scores and add specific fixes
572
+ if issues.get('task_score', 10) < 5:
573
+ applicable_fixes.append({
574
+ 'agent': agent_name,
575
+ 'issue_type': 'low_task_achievement',
576
+ 'fix_template': 'Clarify the action with specific, concrete instructions. Add step-by-step guidance.',
577
+ })
578
+
579
+ if issues.get('instruction_score', 10) < 5:
580
+ applicable_fixes.append({
581
+ 'agent': agent_name,
582
+ 'issue_type': 'poor_instruction_following',
583
+ 'fix_template': 'Make instructions more explicit. Use numbered steps and clear requirements.',
584
+ })
585
+
586
+ if issues.get('hallucination_score', 10) < 6:
587
+ applicable_fixes.append({
588
+ 'agent': agent_name,
589
+ 'issue_type': 'hallucination_risk',
590
+ 'fix_template': 'Add grounding: "Only use information from provided context. Do NOT fabricate data."',
591
+ })
592
+
593
+ return applicable_fixes
594
+
595
+ def propose_improvements(
596
+ self,
597
+ report: Any,
598
+ recipe_path: Path,
599
+ optimization_target: Optional[str] = None,
600
+ ) -> List[str]:
601
+ """
602
+ Use LLM to propose specific YAML improvements based on judge feedback.
603
+
604
+ Args:
605
+ report: JudgeReport with scores and suggestions
606
+ recipe_path: Path to recipe folder
607
+ optimization_target: Optional specific aspect to optimize
608
+
609
+ Returns:
610
+ List of improvement suggestions
611
+ """
612
+ litellm = self._get_litellm()
613
+
614
+ # Read current agents.yaml
615
+ agents_yaml = (recipe_path / "agents.yaml").read_text()
616
+
617
+ # Extract detailed agent issues
618
+ agent_issues = self._extract_agent_issues(report)
619
+ applicable_fixes = self._identify_fix_patterns(agent_issues)
620
+
621
+ # Build detailed issue summary with severity ranking
622
+ issue_summary = []
623
+ sorted_agents = sorted(
624
+ agent_issues.items(),
625
+ key=lambda x: x[1].get('task_score', 10) # Sort by lowest score first
626
+ )
627
+
628
+ for agent_name, issues in sorted_agents:
629
+ avg_score = (
630
+ issues['task_score'] + issues['context_score'] +
631
+ issues['quality_score'] + issues['instruction_score']
632
+ ) / 4
633
+ severity = "🔴 CRITICAL" if avg_score < 5 else "🟡 NEEDS WORK" if avg_score < 7 else "🟢 OK"
634
+
635
+ issue_summary.append(f"""Agent: {agent_name} [{severity}] (avg: {avg_score:.1f}/10)
636
+ - Task Achievement: {issues['task_score']}/10
637
+ - Context Utilization: {issues['context_score']}/10
638
+ - Output Quality: {issues['quality_score']}/10
639
+ - Instruction Following: {issues['instruction_score']}/10
640
+ - Hallucination Score: {issues['hallucination_score']}/10 (10=no hallucination)
641
+ - Failure Detected: {issues['failure_detected']}
642
+ - Failure Reason: {issues['failure_reason'] or 'N/A'}
643
+ - Suggestions: {issues['suggestions']}""")
644
+
645
+ # Build fix patterns summary with severity
646
+ fix_patterns = []
647
+ for fix in applicable_fixes:
648
+ severity = COMMON_ISSUE_FIXES.get(fix['issue_type'], {}).get('severity', 'medium')
649
+ severity_icon = "🔴" if severity == "critical" else "🟡" if severity == "high" else "🟢"
650
+ fix_patterns.append(f" {severity_icon} {fix['agent']}: {fix['issue_type']} → {fix['fix_template']}")
651
+
652
+ recommendations = getattr(report, 'recommendations', [])
653
+ overall_score = getattr(report, 'overall_score', 5.0)
654
+
655
+ # Get SDK knowledge for comprehensive context (used in prompt)
656
+ _sdk_knowledge = get_sdk_knowledge_prompt() # Reserved for future use
657
+
658
+ # Extract input context from YAML variables (image_path, url, etc.)
659
+ input_context = ""
660
+ try:
661
+ import yaml
662
+ yaml_data = yaml.safe_load(agents_yaml)
663
+ variables = yaml_data.get('variables', {})
664
+
665
+ # Check for image input
666
+ image_path = variables.get('image_path') or variables.get('image_url') or variables.get('image')
667
+ if image_path:
668
+ logger.info(f"Reading image for optimizer context: {image_path}")
669
+ image_desc = _read_image_for_optimizer(image_path, self.model)
670
+ input_context += f"\n\n{image_desc}"
671
+
672
+ # Check for URL input
673
+ url = variables.get('url') or variables.get('source_url') or variables.get('webpage')
674
+ if url:
675
+ logger.info(f"Crawling URL for optimizer context: {url}")
676
+ url_content = _crawl_url_for_optimizer(url)
677
+ input_context += f"\n\n{url_content}"
678
+ except Exception as e:
679
+ logger.debug(f"Could not extract input context: {e}")
680
+
681
+ # Build input context section for prompt
682
+ input_context_section = ""
683
+ if input_context:
684
+ input_context_section = f"""
685
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
686
+ ACTUAL INPUT CONTENT (for validation):
687
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
688
+ {input_context}
689
+
690
+ IMPORTANT: Use this actual input content to validate if the agent outputs are correct.
691
+ If the agent output doesn't match the actual input, flag it as a hallucination issue.
692
+ """
693
+
694
+ prompt = f"""You are an expert at optimizing PraisonAI agent recipes. Your goal is to INCREASE the score from {overall_score}/10 to 8.0+/10.
695
+ {input_context_section}
696
+
697
+ {TOOL_DOCUMENTATION}
698
+
699
+ {HIERARCHICAL_PROCESS_DOC}
700
+
701
+ {SPECIALIZED_AGENTS_DOC}
702
+
703
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
704
+ CURRENT RECIPE (agents.yaml):
705
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
706
+ ```yaml
707
+ {agents_yaml[:5000]}
708
+ ```
709
+
710
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
711
+ JUDGE REPORT (Current Score: {overall_score}/10 - Target: 8.0+/10):
712
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
713
+
714
+ Per-Agent Analysis (sorted by lowest score first):
715
+ {chr(10).join(issue_summary)}
716
+
717
+ Recommendations from Judge:
718
+ {chr(10).join(f' - {r}' for r in recommendations)}
719
+
720
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
721
+ IDENTIFIED FIX PATTERNS (Apply these fixes):
722
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
723
+ {chr(10).join(fix_patterns) if fix_patterns else ' No specific patterns identified'}
724
+
725
+ {f"Optimization Target: {optimization_target}" if optimization_target else ""}
726
+
727
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
728
+ MANDATORY OPTIMIZATION RULES (MUST FOLLOW ALL):
729
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
730
+
731
+ ## RULE 1: TOOL CALL RELIABILITY (CRITICAL - Most common failure)
732
+ If an agent has tools assigned but failed to call them or got low task score:
733
+ - Action MUST explicitly name the tool: "You MUST call the search_web tool..."
734
+ - Action MUST forbid responding without tool: "Do NOT respond without calling search_web first."
735
+ - Action MUST require using results: "Return [specific format] from the search results."
736
+ - Action MUST specify count: "Return at least 5 items..."
737
+
738
+ ❌ BAD: "Search for information about AI"
739
+ ✅ GOOD: "You MUST call the search_web tool to search for AI developments. Do NOT respond without calling search_web first. Return at least 5 findings with titles, descriptions, and URLs from the search results."
740
+
741
+ ## RULE 2: OUTPUT FORMAT SPECIFICATION (CRITICAL)
742
+ If output quality or format issues detected:
743
+ - expected_output MUST specify exact format with counts
744
+ - Include field names, counts, and structure
745
+
746
+ ❌ BAD: "A report about the topic"
747
+ ✅ GOOD: "A structured report with: 1) Executive summary (2-3 sentences), 2) Key findings (5 bullet points with title and description), 3) Recommendations (3 actionable items)"
748
+
749
+ ## RULE 3: CONTEXT UTILIZATION (CRITICAL - MOST COMMON FAILURE)
750
+ If context not properly used or agent didn't reference previous output:
751
+ - Action MUST start with: "IMPORTANT: You MUST use ONLY the information from {{{{previous_agent}}}}_output below."
752
+ - Action MUST forbid training data: "Do NOT use your training data or prior knowledge."
753
+ - Action MUST require quoting: "Quote specific findings from the input."
754
+ - Action MUST require source references: "Reference the source URLs from the input."
755
+
756
+ ❌ BAD: "Analyze the data"
757
+ ❌ BAD: "Analyze the research findings from {{{{researcher}}}}_output."
758
+ ✅ GOOD: "IMPORTANT: You MUST use ONLY the information from {{{{researcher}}}}_output below. Do NOT use your training data or prior knowledge. Read the research findings carefully, then: 1) Quote specific findings from the input, 2) Compare and identify the most significant insights, 3) Explain why each is important with references to the source URLs."
759
+
760
+ ## RULE 4: ANTI-HALLUCINATION (CRITICAL)
761
+ If hallucination detected (score < 8):
762
+ - Add grounding instruction: "Only use information from the provided context or tool results."
763
+ - Add verification: "Do NOT make up facts, statistics, or sources. Cite your sources."
764
+
765
+ ## RULE 5: GOAL CLARITY (CRITICAL)
766
+ If goal is missing or vague:
767
+ - Every agent MUST have a clear, specific goal
768
+ - Goal should describe measurable success criteria
769
+
770
+ ❌ BAD: goal: "Help with research"
771
+ ✅ GOOD: goal: "Find and compile the latest 5 developments in quantum computing with sources"
772
+
773
+ ## RULE 6: COMPLETENESS (HIGH)
774
+ If truncation or incomplete output detected:
775
+ - Add: "Ensure your response is COMPLETE. Include ALL required sections."
776
+ - Specify minimum counts: "Include at least 5 items..."
777
+
778
+ ## RULE 7: HIERARCHICAL PROCESS OPTIMIZATION (CRITICAL)
779
+ If workflow status is 'failed' or manager rejected a step:
780
+ - The manager validates EVERY step before proceeding
781
+ - Tool calls are MANDATORY - manager will reject if tools aren't called
782
+ - Make actions EXPLICIT: "You MUST call [tool]. Do NOT respond without calling [tool] first."
783
+ - Add grounding: "Only use information from tool results. Do NOT fabricate data."
784
+ - Specify exact output format in expected_output
785
+
786
+ Common manager rejection fixes:
787
+ - "Agent did not call tool" → Add explicit tool call instruction
788
+ - "Output format mismatch" → Specify exact format in expected_output
789
+
790
+ ## RULE 8: FORCE TOOL USAGE (CRITICAL - ROOT CAUSE FIX)
791
+ If an agent has tools assigned but tools weren't called:
792
+ - ADD `tool_choice: required` to the agent definition
793
+ - This forces the LLM to call a tool before responding
794
+ - Without this, the LLM may skip tool calls even with explicit instructions
795
+
796
+ Example fix for agent with tools:
797
+ ```yaml
798
+ agents:
799
+ researcher:
800
+ role: Research Specialist
801
+ tools:
802
+ - search_web
803
+ tool_choice: required # <-- ADD THIS to force tool usage
804
+ llm: gpt-4o-mini
805
+ ```
806
+ - "Hallucination detected" → Add grounding instruction
807
+ - "Context not used" → Reference {{previous_agent}}_output explicitly
808
+
809
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
810
+ EXAMPLE: OPTIMIZED RECIPE PATTERN
811
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
812
+
813
+ agents:
814
+ researcher:
815
+ role: Research Specialist
816
+ goal: Find the latest 5+ developments on the topic with verified sources
817
+ backstory: Expert at finding current, accurate information from web sources.
818
+ tools:
819
+ - search_web
820
+ tool_choice: required
821
+ llm: gpt-4o-mini
822
+
823
+ steps:
824
+ - agent: researcher
825
+ action: "You MUST call the search_web tool to search for [TOPIC]. Do NOT respond without calling search_web first. Return at least 5 key findings with: title, description (2-3 sentences), and source URL from the search results."
826
+ expected_output: "Raw research data with 5+ findings, each containing: title, description, source URL"
827
+
828
+ - agent: analyst
829
+ action: "Analyze the research findings from {{researcher}}_output. Compare and identify the top 3 most significant insights. For each insight, explain: what it is, why it matters, and supporting evidence from the research."
830
+ expected_output: "Analysis with 3 key insights, each with: insight name, significance explanation, supporting evidence from research"
831
+
832
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
833
+ YOUR TASK: Propose 3-5 specific improvements
834
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
835
+
836
+ Focus on the LOWEST scoring agents first (marked 🔴 CRITICAL).
837
+ Apply the mandatory rules above to fix each issue.
838
+
839
+ For EACH improvement, provide:
840
+ 1. AGENT: Which agent to modify
841
+ 2. FIELD: Which field to change (action, expected_output, goal, tools, etc.)
842
+ 3. ISSUE: What problem this fixes (reference the rule number)
843
+ 4. BEFORE: Current value (quote from YAML)
844
+ 5. AFTER: New value (exact text to use)
845
+
846
+ Format your response as:
847
+
848
+ ### IMPROVEMENT 1
849
+ - AGENT: [agent_name]
850
+ - FIELD: [field_name]
851
+ - ISSUE: [RULE X: brief description]
852
+ - BEFORE: [current value]
853
+ - AFTER: [new value]
854
+
855
+ ### IMPROVEMENT 2
856
+ ...
857
+
858
+ IMPORTANT: The AFTER value must be the EXACT text to use in the YAML file.
859
+ """
860
+
861
+ try:
862
+ response = litellm.completion(
863
+ model=self.model,
864
+ messages=[{"role": "user", "content": prompt}],
865
+ temperature=0.2,
866
+ max_tokens=2500,
867
+ )
868
+
869
+ improvements_text = response.choices[0].message.content or ""
870
+
871
+ # Parse structured improvements
872
+ improvements = self._parse_structured_improvements(improvements_text)
873
+
874
+ return improvements if improvements else [improvements_text]
875
+
876
+ except Exception as e:
877
+ logger.warning(f"Failed to propose improvements: {e}")
878
+ # Fall back to fix patterns
879
+ fallback = []
880
+ for fix in applicable_fixes[:3]:
881
+ fallback.append(f"{fix['agent']}: {fix['fix_template']}")
882
+ return fallback if fallback else recommendations if recommendations else ["Review agent instructions"]
883
+
884
+ def _parse_structured_improvements(self, text: str) -> List[str]:
885
+ """
886
+ Parse structured improvement suggestions from LLM response.
887
+
888
+ Returns:
889
+ List of improvement descriptions
890
+ """
891
+ improvements = []
892
+
893
+ # Split by improvement headers
894
+ sections = re.split(r'###\s*IMPROVEMENT\s*\d+', text, flags=re.IGNORECASE)
895
+
896
+ for section in sections[1:]: # Skip first empty section
897
+ section = section.strip()
898
+ if not section:
899
+ continue
900
+
901
+ # Extract key fields
902
+ agent_match = re.search(r'AGENT:\s*(.+?)(?:\n|$)', section)
903
+ field_match = re.search(r'FIELD:\s*(.+?)(?:\n|$)', section)
904
+ issue_match = re.search(r'ISSUE:\s*(.+?)(?:\n|$)', section)
905
+ after_match = re.search(r'AFTER:\s*(.+?)(?:###|$)', section, re.DOTALL)
906
+
907
+ if agent_match and field_match:
908
+ agent = agent_match.group(1).strip()
909
+ field = field_match.group(1).strip()
910
+ issue = issue_match.group(1).strip() if issue_match else "improve quality"
911
+ after = after_match.group(1).strip() if after_match else ""
912
+
913
+ improvement = f"Agent '{agent}' - {field}: {issue}"
914
+ if after:
915
+ # Clean up the after value
916
+ after = re.sub(r'^[`"\']|[`"\']$', '', after.split('\n')[0].strip())
917
+ improvement += f" → {after[:200]}{'...' if len(after) > 200 else ''}"
918
+
919
+ improvements.append(improvement)
920
+
921
+ # Fallback: extract any numbered items
922
+ if not improvements:
923
+ for line in text.split('\n'):
924
+ line = line.strip()
925
+ if re.match(r'^\d+\.\s+', line) or line.startswith(('-', '*')):
926
+ improvements.append(re.sub(r'^[\d\.\-\*]+\s*', '', line))
927
+
928
+ return improvements[:4] # Max 4 improvements
929
+
930
+ def _validate_yaml_structure(self, yaml_content: str) -> Tuple[bool, List[str]]:
931
+ """
932
+ Validate YAML structure for PraisonAI recipe requirements.
933
+
934
+ Returns:
935
+ Tuple of (is_valid, list_of_errors)
936
+ """
937
+ import yaml
938
+ errors = []
939
+
940
+ try:
941
+ data = yaml.safe_load(yaml_content)
942
+ if not data:
943
+ errors.append("YAML is empty")
944
+ return False, errors
945
+
946
+ # Check for required sections
947
+ if 'agents' not in data:
948
+ errors.append("Missing 'agents' section")
949
+ elif not isinstance(data['agents'], dict):
950
+ errors.append("'agents' must be a dictionary")
951
+ else:
952
+ # Validate each agent
953
+ for agent_name, agent_config in data['agents'].items():
954
+ if not isinstance(agent_config, dict):
955
+ errors.append(f"Agent '{agent_name}' config must be a dictionary")
956
+ continue
957
+
958
+ # Check for required agent fields
959
+ if 'role' not in agent_config:
960
+ errors.append(f"Agent '{agent_name}' missing 'role' field")
961
+ if 'goal' not in agent_config:
962
+ errors.append(f"Agent '{agent_name}' missing 'goal' field")
963
+
964
+ # Validate tools is a list if present
965
+ if 'tools' in agent_config and not isinstance(agent_config['tools'], list):
966
+ errors.append(f"Agent '{agent_name}' tools must be a list")
967
+
968
+ # Check for steps section
969
+ if 'steps' not in data:
970
+ errors.append("Missing 'steps' section")
971
+ elif not isinstance(data['steps'], list):
972
+ errors.append("'steps' must be a list")
973
+ else:
974
+ # Validate each step
975
+ for i, step in enumerate(data['steps']):
976
+ if not isinstance(step, dict):
977
+ errors.append(f"Step {i+1} must be a dictionary")
978
+ continue
979
+ if 'agent' not in step:
980
+ errors.append(f"Step {i+1} missing 'agent' field")
981
+ if 'action' not in step:
982
+ errors.append(f"Step {i+1} missing 'action' field")
983
+
984
+ return len(errors) == 0, errors
985
+
986
+ except yaml.YAMLError as e:
987
+ errors.append(f"Invalid YAML syntax: {str(e)[:200]}")
988
+ return False, errors
989
+
990
+ def _ensure_hierarchical_preserved(self, original_yaml: str, new_yaml: str) -> str:
991
+ """
992
+ Ensure hierarchical process fields are preserved after optimization.
993
+
994
+ If the original YAML had process: hierarchical and manager_llm,
995
+ ensure they are present in the new YAML. If missing, add them.
996
+
997
+ Args:
998
+ original_yaml: Original YAML content
999
+ new_yaml: New YAML content after optimization
1000
+
1001
+ Returns:
1002
+ Updated YAML with hierarchical fields preserved
1003
+ """
1004
+ import yaml
1005
+
1006
+ try:
1007
+ original_data = yaml.safe_load(original_yaml)
1008
+ new_data = yaml.safe_load(new_yaml)
1009
+
1010
+ if not original_data or not new_data:
1011
+ return new_yaml
1012
+
1013
+ # Check if original had hierarchical process
1014
+ original_process = original_data.get('process')
1015
+ original_manager_llm = original_data.get('manager_llm')
1016
+
1017
+ modified = False
1018
+
1019
+ # Preserve process field
1020
+ if original_process == 'hierarchical':
1021
+ if new_data.get('process') != 'hierarchical':
1022
+ new_data['process'] = 'hierarchical'
1023
+ modified = True
1024
+ logger.info("Restored process: hierarchical (was removed by LLM)")
1025
+
1026
+ # Preserve manager_llm field
1027
+ if original_manager_llm:
1028
+ if not new_data.get('manager_llm'):
1029
+ new_data['manager_llm'] = original_manager_llm
1030
+ modified = True
1031
+ logger.info(f"Restored manager_llm: {original_manager_llm} (was removed by LLM)")
1032
+
1033
+ if modified:
1034
+ # Rebuild YAML with proper ordering
1035
+ return self._rebuild_yaml_with_order(new_data)
1036
+
1037
+ return new_yaml
1038
+
1039
+ except Exception as e:
1040
+ logger.debug(f"Could not validate hierarchical preservation: {e}")
1041
+ return new_yaml
1042
+
1043
+ def _rebuild_yaml_with_order(self, data: Dict[str, Any]) -> str:
1044
+ """
1045
+ Rebuild YAML with proper field ordering.
1046
+
1047
+ Ensures metadata, process, manager_llm come before agents and steps.
1048
+ """
1049
+ import yaml
1050
+
1051
+ # Define preferred order
1052
+ ordered_keys = ['metadata', 'process', 'manager_llm', 'variables', 'agents', 'steps']
1053
+
1054
+ # Build ordered dict
1055
+ ordered_data = {}
1056
+
1057
+ # Add keys in preferred order
1058
+ for key in ordered_keys:
1059
+ if key in data:
1060
+ ordered_data[key] = data[key]
1061
+
1062
+ # Add any remaining keys
1063
+ for key in data:
1064
+ if key not in ordered_data:
1065
+ ordered_data[key] = data[key]
1066
+
1067
+ # Dump with proper formatting
1068
+ return yaml.dump(ordered_data, default_flow_style=False, sort_keys=False, allow_unicode=True)
1069
+
1070
+ def apply_improvements(
1071
+ self,
1072
+ recipe_path: Path,
1073
+ improvements: List[str],
1074
+ ) -> bool:
1075
+ """
1076
+ Apply improvements to agents.yaml using LLM.
1077
+
1078
+ Args:
1079
+ recipe_path: Path to recipe folder
1080
+ improvements: List of improvement suggestions
1081
+
1082
+ Returns:
1083
+ True if improvements were applied
1084
+ """
1085
+ litellm = self._get_litellm()
1086
+
1087
+ # Read current agents.yaml
1088
+ agents_yaml_path = recipe_path / "agents.yaml"
1089
+ current_yaml = agents_yaml_path.read_text()
1090
+
1091
+ prompt = f"""You are an expert at modifying PraisonAI agent YAML files.
1092
+
1093
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1094
+ CURRENT agents.yaml:
1095
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1096
+ ```yaml
1097
+ {current_yaml}
1098
+ ```
1099
+
1100
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1101
+ IMPROVEMENTS TO APPLY:
1102
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1103
+ {chr(10).join(f"- {imp}" for imp in improvements)}
1104
+
1105
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1106
+ MANDATORY RULES FOR OUTPUT:
1107
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1108
+
1109
+ 1. Output ONLY valid YAML - no markdown code blocks, no explanations
1110
+ 2. Preserve ALL existing structure (metadata, agents, steps)
1111
+ 3. Only modify the specific fields mentioned in improvements
1112
+ 4. Ensure all agents have: role, goal, backstory, tools, llm
1113
+ 5. Ensure all steps have: agent, action, expected_output
1114
+ 6. Use proper YAML indentation (2 spaces)
1115
+ 7. Keep tools as a list: tools: [tool1, tool2] or tools: []
1116
+ 8. Use double curly braces for variable references: {{{{agent_name}}}}_output
1117
+ 9. CRITICAL: Quote ALL string values that contain colons (:) using double quotes
1118
+ Example: action: "Return findings with: title, description"
1119
+ NOT: action: Return findings with: title, description
1120
+
1121
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1122
+ HIERARCHICAL PROCESS PRESERVATION (CRITICAL - NEVER REMOVE):
1123
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1124
+
1125
+ If the original YAML contains these fields, you MUST preserve them EXACTLY:
1126
+ - process: hierarchical
1127
+ - manager_llm: gpt-4o-mini
1128
+
1129
+ These fields enable manager-based validation. NEVER remove them.
1130
+ If they are missing, ADD them after the metadata section.
1131
+
1132
+ CRITICAL: Do NOT remove any existing agents or steps unless explicitly requested.
1133
+ CRITICAL: Do NOT change the overall structure of the file.
1134
+ CRITICAL: ALWAYS preserve process: hierarchical and manager_llm fields.
1135
+
1136
+ Output the complete, updated agents.yaml file now:
1137
+ """
1138
+
1139
+ try:
1140
+ response = litellm.completion(
1141
+ model=self.model,
1142
+ messages=[{"role": "user", "content": prompt}],
1143
+ temperature=0.1, # Lower temperature for more consistent output
1144
+ max_tokens=4000,
1145
+ )
1146
+
1147
+ new_yaml = response.choices[0].message.content or ""
1148
+
1149
+ # Clean up any markdown
1150
+ new_yaml = new_yaml.strip()
1151
+ if new_yaml.startswith('```'):
1152
+ lines = new_yaml.split('\n')
1153
+ # Find the end of the code block
1154
+ end_idx = len(lines) - 1
1155
+ for i in range(len(lines) - 1, 0, -1):
1156
+ if lines[i].strip().startswith('```'):
1157
+ end_idx = i
1158
+ break
1159
+ new_yaml = '\n'.join(lines[1:end_idx])
1160
+
1161
+ # Fix common YAML issues: quote strings with colons
1162
+ new_yaml = self._fix_yaml_string_quoting(new_yaml)
1163
+
1164
+ # Validate YAML structure
1165
+ is_valid, errors = self._validate_yaml_structure(new_yaml)
1166
+
1167
+ if not is_valid:
1168
+ logger.warning(f"Generated YAML has validation errors: {errors}")
1169
+ # Try to fix common issues
1170
+ if "Invalid YAML syntax" in str(errors):
1171
+ logger.warning("YAML syntax error, keeping original file")
1172
+ return False
1173
+
1174
+ # Ensure hierarchical process is preserved
1175
+ new_yaml = self._ensure_hierarchical_preserved(current_yaml, new_yaml)
1176
+
1177
+ # Create backup before writing
1178
+ backup_path = agents_yaml_path.with_suffix('.yaml.bak')
1179
+ backup_path.write_text(current_yaml)
1180
+
1181
+ # Write updated YAML
1182
+ agents_yaml_path.write_text(new_yaml)
1183
+ logger.info(f"Applied improvements to {agents_yaml_path}")
1184
+ logger.info(f"Backup saved to {backup_path}")
1185
+
1186
+ return True
1187
+
1188
+ except Exception as e:
1189
+ logger.warning(f"Failed to apply improvements: {e}")
1190
+ return False
1191
+
1192
+ def _ensure_hierarchical_preserved(self, original_yaml: str, new_yaml: str) -> str:
1193
+ """
1194
+ Ensure hierarchical process fields are preserved from original YAML.
1195
+
1196
+ The LLM sometimes removes process: hierarchical and manager_llm fields.
1197
+ This method ensures they are always preserved or added.
1198
+
1199
+ Args:
1200
+ original_yaml: Original YAML content
1201
+ new_yaml: New YAML content from LLM
1202
+
1203
+ Returns:
1204
+ YAML with hierarchical fields preserved
1205
+ """
1206
+ import yaml
1207
+
1208
+ try:
1209
+ original_data = yaml.safe_load(original_yaml)
1210
+ new_data = yaml.safe_load(new_yaml)
1211
+
1212
+ if not new_data:
1213
+ return new_yaml
1214
+
1215
+ # Check if original had hierarchical process
1216
+ had_hierarchical = original_data.get('process') == 'hierarchical'
1217
+ had_manager_llm = 'manager_llm' in original_data
1218
+
1219
+ # Preserve or add hierarchical fields
1220
+ if had_hierarchical or 'process' not in new_data:
1221
+ new_data['process'] = 'hierarchical'
1222
+
1223
+ if had_manager_llm or 'manager_llm' not in new_data:
1224
+ new_data['manager_llm'] = original_data.get('manager_llm', 'gpt-4o-mini')
1225
+
1226
+ # Rebuild YAML with proper ordering
1227
+ ordered_data = {}
1228
+
1229
+ # Put metadata first
1230
+ if 'metadata' in new_data:
1231
+ ordered_data['metadata'] = new_data.pop('metadata')
1232
+
1233
+ # Put process and manager_llm next
1234
+ if 'process' in new_data:
1235
+ ordered_data['process'] = new_data.pop('process')
1236
+ if 'manager_llm' in new_data:
1237
+ ordered_data['manager_llm'] = new_data.pop('manager_llm')
1238
+
1239
+ # Put variables next if present
1240
+ if 'variables' in new_data:
1241
+ ordered_data['variables'] = new_data.pop('variables')
1242
+
1243
+ # Add remaining keys
1244
+ ordered_data.update(new_data)
1245
+
1246
+ return yaml.dump(ordered_data, default_flow_style=False, sort_keys=False, allow_unicode=True)
1247
+
1248
+ except Exception as e:
1249
+ logger.warning(f"Failed to ensure hierarchical preserved: {e}")
1250
+ return new_yaml
1251
+
1252
+ def _fix_yaml_string_quoting(self, yaml_content: str) -> str:
1253
+ """
1254
+ Fix YAML strings that contain colons but aren't properly quoted.
1255
+
1256
+ The LLM sometimes generates YAML like:
1257
+ action: Return findings with: title, description
1258
+ Which should be:
1259
+ action: "Return findings with: title, description"
1260
+
1261
+ Args:
1262
+ yaml_content: Raw YAML content
1263
+
1264
+ Returns:
1265
+ Fixed YAML content with properly quoted strings
1266
+ """
1267
+ import re
1268
+
1269
+ fixed_lines = []
1270
+ for line in yaml_content.split('\n'):
1271
+ # Skip empty lines and comments
1272
+ if not line.strip() or line.strip().startswith('#'):
1273
+ fixed_lines.append(line)
1274
+ continue
1275
+
1276
+ # Check if line has a key: value pattern
1277
+ match = re.match(r'^(\s*)([\w_-]+):\s*(.+)$', line)
1278
+ if match:
1279
+ indent, key, value = match.groups()
1280
+ value = value.strip()
1281
+
1282
+ # Skip if already quoted or is a special YAML value
1283
+ if value.startswith('"') or value.startswith("'"):
1284
+ fixed_lines.append(line)
1285
+ continue
1286
+ if value.startswith('[') or value.startswith('{'):
1287
+ fixed_lines.append(line)
1288
+ continue
1289
+ if value in ('true', 'false', 'null', 'yes', 'no', '~'):
1290
+ fixed_lines.append(line)
1291
+ continue
1292
+ if re.match(r'^-?\d+(\.\d+)?$', value):
1293
+ fixed_lines.append(line)
1294
+ continue
1295
+
1296
+ # Check if value contains a colon (needs quoting)
1297
+ if ':' in value:
1298
+ # Escape any existing quotes and wrap in double quotes
1299
+ escaped_value = value.replace('\\', '\\\\').replace('"', '\\"')
1300
+ fixed_lines.append(f'{indent}{key}: "{escaped_value}"')
1301
+ else:
1302
+ fixed_lines.append(line)
1303
+ else:
1304
+ fixed_lines.append(line)
1305
+
1306
+ return '\n'.join(fixed_lines)
1307
+
1308
+ def optimize(
1309
+ self,
1310
+ recipe_path: Path,
1311
+ input_data: str = "",
1312
+ optimization_target: Optional[str] = None,
1313
+ ) -> Any:
1314
+ """
1315
+ Run the full optimization loop.
1316
+
1317
+ Args:
1318
+ recipe_path: Path to recipe folder
1319
+ input_data: Input data for recipe runs
1320
+ optimization_target: Optional specific aspect to optimize
1321
+
1322
+ Returns:
1323
+ Final JudgeReport after optimization
1324
+ """
1325
+ recipe_path = Path(recipe_path)
1326
+
1327
+ if not recipe_path.exists():
1328
+ raise ValueError(f"Recipe path does not exist: {recipe_path}")
1329
+
1330
+ final_report = None
1331
+
1332
+ for iteration in range(1, self.max_iterations + 1):
1333
+ logger.info(f"Optimization iteration {iteration}/{self.max_iterations}")
1334
+
1335
+ # Run and judge
1336
+ report, trace_id = self.run_iteration(recipe_path, input_data, iteration)
1337
+ final_report = report
1338
+
1339
+ score = getattr(report, 'overall_score', 0)
1340
+ logger.info(f" Score: {score}/10")
1341
+
1342
+ # Check if we should continue
1343
+ if not self.should_continue(report, iteration):
1344
+ if score >= self.score_threshold:
1345
+ logger.info(" ✅ Score threshold reached!")
1346
+ else:
1347
+ logger.info(" Max iterations reached")
1348
+ break
1349
+
1350
+ # Propose and apply improvements
1351
+ improvements = self.propose_improvements(report, recipe_path, optimization_target)
1352
+ logger.info(f" Proposed {len(improvements)} improvements")
1353
+
1354
+ if improvements:
1355
+ applied = self.apply_improvements(recipe_path, improvements)
1356
+ if applied:
1357
+ logger.info(" ✏️ Applied improvements")
1358
+ else:
1359
+ logger.warning(" Failed to apply improvements")
1360
+
1361
+ return final_report
1362
+
1363
+
1364
+ __all__ = ['RecipeOptimizer']