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.
- praisonai_code/__init__.py +17 -0
- praisonai_code/cli/__init__.py +12 -0
- praisonai_code/cli/_forward_shim.py +10 -0
- praisonai_code/cli/_paths.py +88 -0
- praisonai_code/cli/_warnings.py +58 -0
- praisonai_code/cli/app.py +757 -0
- praisonai_code/cli/approval_backend.py +272 -0
- praisonai_code/cli/branding.py +94 -0
- praisonai_code/cli/commands/__init__.py +114 -0
- praisonai_code/cli/commands/acp.py +80 -0
- praisonai_code/cli/commands/agent.py +116 -0
- praisonai_code/cli/commands/agents.py +80 -0
- praisonai_code/cli/commands/app.py +139 -0
- praisonai_code/cli/commands/attach.py +95 -0
- praisonai_code/cli/commands/audit.py +102 -0
- praisonai_code/cli/commands/auth.py +508 -0
- praisonai_code/cli/commands/batch.py +848 -0
- praisonai_code/cli/commands/benchmark.py +286 -0
- praisonai_code/cli/commands/browser.py +299 -0
- praisonai_code/cli/commands/call.py +45 -0
- praisonai_code/cli/commands/chat.py +332 -0
- praisonai_code/cli/commands/checkpoint.py +170 -0
- praisonai_code/cli/commands/code.py +276 -0
- praisonai_code/cli/commands/command.py +114 -0
- praisonai_code/cli/commands/commit.py +47 -0
- praisonai_code/cli/commands/completion.py +333 -0
- praisonai_code/cli/commands/config.py +681 -0
- praisonai_code/cli/commands/context.py +414 -0
- praisonai_code/cli/commands/daemon.py +203 -0
- praisonai_code/cli/commands/debug.py +142 -0
- praisonai_code/cli/commands/deploy.py +71 -0
- praisonai_code/cli/commands/diag.py +55 -0
- praisonai_code/cli/commands/docs.py +1575 -0
- praisonai_code/cli/commands/doctor.py +332 -0
- praisonai_code/cli/commands/endpoints.py +51 -0
- praisonai_code/cli/commands/environment.py +179 -0
- praisonai_code/cli/commands/eval.py +131 -0
- praisonai_code/cli/commands/examples.py +953 -0
- praisonai_code/cli/commands/flow.py +436 -0
- praisonai_code/cli/commands/github.py +752 -0
- praisonai_code/cli/commands/hooks.py +74 -0
- praisonai_code/cli/commands/init.py +174 -0
- praisonai_code/cli/commands/knowledge.py +440 -0
- praisonai_code/cli/commands/langextract.py +120 -0
- praisonai_code/cli/commands/langfuse.py +984 -0
- praisonai_code/cli/commands/loop.py +211 -0
- praisonai_code/cli/commands/lsp.py +112 -0
- praisonai_code/cli/commands/managed.py +659 -0
- praisonai_code/cli/commands/mcp.py +763 -0
- praisonai_code/cli/commands/memory.py +298 -0
- praisonai_code/cli/commands/models.py +264 -0
- praisonai_code/cli/commands/n8n.py +326 -0
- praisonai_code/cli/commands/obs.py +19 -0
- praisonai_code/cli/commands/package.py +76 -0
- praisonai_code/cli/commands/paths.py +106 -0
- praisonai_code/cli/commands/permissions.py +272 -0
- praisonai_code/cli/commands/plugins.py +609 -0
- praisonai_code/cli/commands/port.py +530 -0
- praisonai_code/cli/commands/profile.py +466 -0
- praisonai_code/cli/commands/publish.py +193 -0
- praisonai_code/cli/commands/rag.py +913 -0
- praisonai_code/cli/commands/realtime.py +52 -0
- praisonai_code/cli/commands/recipe.py +684 -0
- praisonai_code/cli/commands/registry.py +59 -0
- praisonai_code/cli/commands/replay.py +830 -0
- praisonai_code/cli/commands/research.py +49 -0
- praisonai_code/cli/commands/retrieval.py +377 -0
- praisonai_code/cli/commands/rules.py +71 -0
- praisonai_code/cli/commands/run.py +1573 -0
- praisonai_code/cli/commands/sandbox.py +371 -0
- praisonai_code/cli/commands/schedule.py +529 -0
- praisonai_code/cli/commands/serve.py +690 -0
- praisonai_code/cli/commands/session.py +450 -0
- praisonai_code/cli/commands/setup.py +174 -0
- praisonai_code/cli/commands/skills.py +545 -0
- praisonai_code/cli/commands/standardise.py +711 -0
- praisonai_code/cli/commands/templates.py +54 -0
- praisonai_code/cli/commands/test.py +558 -0
- praisonai_code/cli/commands/todo.py +74 -0
- praisonai_code/cli/commands/tools.py +205 -0
- praisonai_code/cli/commands/traces.py +145 -0
- praisonai_code/cli/commands/tracker.py +852 -0
- praisonai_code/cli/commands/train.py +613 -0
- praisonai_code/cli/commands/ui.py +172 -0
- praisonai_code/cli/commands/up.py +354 -0
- praisonai_code/cli/commands/validate.py +291 -0
- praisonai_code/cli/commands/version.py +101 -0
- praisonai_code/cli/commands/workflow.py +97 -0
- praisonai_code/cli/config_loader.py +437 -0
- praisonai_code/cli/configuration/__init__.py +27 -0
- praisonai_code/cli/configuration/config.schema.json +57 -0
- praisonai_code/cli/configuration/credentials.py +446 -0
- praisonai_code/cli/configuration/loader.py +364 -0
- praisonai_code/cli/configuration/model_resolver.py +161 -0
- praisonai_code/cli/configuration/oauth.py +389 -0
- praisonai_code/cli/configuration/paths.py +224 -0
- praisonai_code/cli/configuration/resolver.py +687 -0
- praisonai_code/cli/configuration/schema.py +317 -0
- praisonai_code/cli/execution/__init__.py +99 -0
- praisonai_code/cli/execution/core.py +208 -0
- praisonai_code/cli/execution/profiler.py +898 -0
- praisonai_code/cli/execution/request.py +85 -0
- praisonai_code/cli/execution/result.py +74 -0
- praisonai_code/cli/fallback_schema.py +416 -0
- praisonai_code/cli/features/__init__.py +278 -0
- praisonai_code/cli/features/_endpoint_registry.py +64 -0
- praisonai_code/cli/features/_search_registry.py +43 -0
- praisonai_code/cli/features/acp.py +236 -0
- praisonai_code/cli/features/action_orchestrator.py +576 -0
- praisonai_code/cli/features/agent_scheduler.py +773 -0
- praisonai_code/cli/features/agent_tools.py +603 -0
- praisonai_code/cli/features/agents.py +397 -0
- praisonai_code/cli/features/at_mentions.py +471 -0
- praisonai_code/cli/features/audit_cli.py +270 -0
- praisonai_code/cli/features/auto_memory.py +182 -0
- praisonai_code/cli/features/auto_mode.py +552 -0
- praisonai_code/cli/features/autonomy_mode.py +546 -0
- praisonai_code/cli/features/background.py +356 -0
- praisonai_code/cli/features/base.py +168 -0
- praisonai_code/cli/features/benchmark.py +1462 -0
- praisonai_code/cli/features/capabilities.py +1326 -0
- praisonai_code/cli/features/checkpoints.py +345 -0
- praisonai_code/cli/features/cli_profiler.py +335 -0
- praisonai_code/cli/features/code_intelligence.py +666 -0
- praisonai_code/cli/features/compaction.py +294 -0
- praisonai_code/cli/features/compare.py +534 -0
- praisonai_code/cli/features/config_hierarchy.py +366 -0
- praisonai_code/cli/features/context_manager.py +597 -0
- praisonai_code/cli/features/cost_tracker.py +514 -0
- praisonai_code/cli/features/csv_test_runner.py +736 -0
- praisonai_code/cli/features/custom_definitions.py +790 -0
- praisonai_code/cli/features/debug.py +810 -0
- praisonai_code/cli/features/deploy.py +605 -0
- praisonai_code/cli/features/diag.py +289 -0
- praisonai_code/cli/features/display_jsonl.py +173 -0
- praisonai_code/cli/features/doctor/__init__.py +63 -0
- praisonai_code/cli/features/doctor/checks/__init__.py +29 -0
- praisonai_code/cli/features/doctor/checks/acp_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/bot_checks.py +340 -0
- praisonai_code/cli/features/doctor/checks/config_checks.py +373 -0
- praisonai_code/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai_code/cli/features/doctor/checks/env_checks.py +637 -0
- praisonai_code/cli/features/doctor/checks/gateway_checks.py +387 -0
- praisonai_code/cli/features/doctor/checks/lsp_checks.py +231 -0
- praisonai_code/cli/features/doctor/checks/mcp_checks.py +367 -0
- praisonai_code/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai_code/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai_code/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai_code/cli/features/doctor/checks/packaging_checks.py +422 -0
- praisonai_code/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai_code/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai_code/cli/features/doctor/checks/runtime_checks.py +650 -0
- praisonai_code/cli/features/doctor/checks/runtime_migration_checks.py +220 -0
- praisonai_code/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai_code/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai_code/cli/features/doctor/checks/skills_checks.py +327 -0
- praisonai_code/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai_code/cli/features/doctor/engine.py +266 -0
- praisonai_code/cli/features/doctor/formatters.py +377 -0
- praisonai_code/cli/features/doctor/handler.py +564 -0
- praisonai_code/cli/features/doctor/models.py +276 -0
- praisonai_code/cli/features/doctor/registry.py +239 -0
- praisonai_code/cli/features/endpoints.py +1016 -0
- praisonai_code/cli/features/eval.py +559 -0
- praisonai_code/cli/features/examples.py +707 -0
- praisonai_code/cli/features/external_agents.py +231 -0
- praisonai_code/cli/features/fast_context.py +410 -0
- praisonai_code/cli/features/file_history.py +320 -0
- praisonai_code/cli/features/flow_display.py +566 -0
- praisonai_code/cli/features/git_attribution.py +159 -0
- praisonai_code/cli/features/git_integration.py +651 -0
- praisonai_code/cli/features/guardrail.py +171 -0
- praisonai_code/cli/features/handoff.py +252 -0
- praisonai_code/cli/features/hooks.py +583 -0
- praisonai_code/cli/features/hybrid_workflow.py +391 -0
- praisonai_code/cli/features/image.py +384 -0
- praisonai_code/cli/features/interactive_core_headless.py +450 -0
- praisonai_code/cli/features/interactive_runtime.py +600 -0
- praisonai_code/cli/features/interactive_test_harness.py +537 -0
- praisonai_code/cli/features/interactive_tools.py +428 -0
- praisonai_code/cli/features/interactive_tui.py +603 -0
- praisonai_code/cli/features/job_workflow.py +906 -0
- praisonai_code/cli/features/jobs.py +632 -0
- praisonai_code/cli/features/knowledge.py +531 -0
- praisonai_code/cli/features/knowledge_cli.py +438 -0
- praisonai_code/cli/features/lite.py +244 -0
- praisonai_code/cli/features/logs.py +200 -0
- praisonai_code/cli/features/lsp_cli.py +225 -0
- praisonai_code/cli/features/lsp_diagnostics.py +185 -0
- praisonai_code/cli/features/mcp.py +344 -0
- praisonai_code/cli/features/message_queue.py +587 -0
- praisonai_code/cli/features/metrics.py +210 -0
- praisonai_code/cli/features/migrate.py +1329 -0
- praisonai_code/cli/features/migration_flow.py +463 -0
- praisonai_code/cli/features/migration_spec.py +276 -0
- praisonai_code/cli/features/n8n.py +703 -0
- praisonai_code/cli/features/observability.py +293 -0
- praisonai_code/cli/features/ollama.py +361 -0
- praisonai_code/cli/features/output_modes.py +155 -0
- praisonai_code/cli/features/output_style.py +273 -0
- praisonai_code/cli/features/package.py +631 -0
- praisonai_code/cli/features/performance.py +308 -0
- praisonai_code/cli/features/persistence.py +636 -0
- praisonai_code/cli/features/profiler/__init__.py +81 -0
- praisonai_code/cli/features/profiler/core.py +558 -0
- praisonai_code/cli/features/profiler/optimizations.py +652 -0
- praisonai_code/cli/features/profiler/suite.py +386 -0
- praisonai_code/cli/features/queue/__init__.py +73 -0
- praisonai_code/cli/features/queue/manager.py +435 -0
- praisonai_code/cli/features/queue/models.py +289 -0
- praisonai_code/cli/features/queue/persistence.py +564 -0
- praisonai_code/cli/features/queue/scheduler.py +529 -0
- praisonai_code/cli/features/queue/worker.py +400 -0
- praisonai_code/cli/features/recipe.py +2187 -0
- praisonai_code/cli/features/recipe_creator.py +996 -0
- praisonai_code/cli/features/recipe_optimizer.py +1364 -0
- praisonai_code/cli/features/recipe_prompts.py +226 -0
- praisonai_code/cli/features/registry.py +229 -0
- praisonai_code/cli/features/repo_map.py +860 -0
- praisonai_code/cli/features/router.py +466 -0
- praisonai_code/cli/features/safe_shell.py +427 -0
- praisonai_code/cli/features/sandbox_cli.py +283 -0
- praisonai_code/cli/features/sandbox_executor.py +536 -0
- praisonai_code/cli/features/sdk_knowledge.py +500 -0
- praisonai_code/cli/features/session.py +222 -0
- praisonai_code/cli/features/session_checkpoints.py +208 -0
- praisonai_code/cli/features/setup/__init__.py +9 -0
- praisonai_code/cli/features/setup/handler.py +355 -0
- praisonai_code/cli/features/setup/templates.py +62 -0
- praisonai_code/cli/features/skills.py +940 -0
- praisonai_code/cli/features/slash_commands.py +692 -0
- praisonai_code/cli/features/telemetry.py +179 -0
- praisonai_code/cli/features/templates.py +1390 -0
- praisonai_code/cli/features/thinking.py +343 -0
- praisonai_code/cli/features/todo.py +334 -0
- praisonai_code/cli/features/tools.py +680 -0
- praisonai_code/cli/features/tui/__init__.py +83 -0
- praisonai_code/cli/features/tui/app.py +871 -0
- praisonai_code/cli/features/tui/cli.py +580 -0
- praisonai_code/cli/features/tui/config.py +150 -0
- praisonai_code/cli/features/tui/debug.py +526 -0
- praisonai_code/cli/features/tui/events.py +99 -0
- praisonai_code/cli/features/tui/mock_provider.py +328 -0
- praisonai_code/cli/features/tui/orchestrator.py +652 -0
- praisonai_code/cli/features/tui/screens/__init__.py +50 -0
- praisonai_code/cli/features/tui/screens/help.py +157 -0
- praisonai_code/cli/features/tui/screens/main.py +568 -0
- praisonai_code/cli/features/tui/screens/queue.py +174 -0
- praisonai_code/cli/features/tui/screens/session.py +124 -0
- praisonai_code/cli/features/tui/screens/settings.py +148 -0
- praisonai_code/cli/features/tui/session_store.py +198 -0
- praisonai_code/cli/features/tui/widgets/__init__.py +56 -0
- praisonai_code/cli/features/tui/widgets/chat.py +263 -0
- praisonai_code/cli/features/tui/widgets/command_popup.py +258 -0
- praisonai_code/cli/features/tui/widgets/composer.py +292 -0
- praisonai_code/cli/features/tui/widgets/file_popup.py +207 -0
- praisonai_code/cli/features/tui/widgets/queue_panel.py +223 -0
- praisonai_code/cli/features/tui/widgets/status.py +181 -0
- praisonai_code/cli/features/tui/widgets/tool_panel.py +307 -0
- praisonai_code/cli/features/wizard.py +289 -0
- praisonai_code/cli/features/workflow.py +802 -0
- praisonai_code/cli/features/yaml_utils.py +321 -0
- praisonai_code/cli/interactive/__init__.py +48 -0
- praisonai_code/cli/interactive/async_tui.py +1218 -0
- praisonai_code/cli/interactive/config.py +139 -0
- praisonai_code/cli/interactive/core.py +618 -0
- praisonai_code/cli/interactive/events.py +131 -0
- praisonai_code/cli/interactive/frontends/__init__.py +31 -0
- praisonai_code/cli/interactive/frontends/rich_frontend.py +462 -0
- praisonai_code/cli/interactive/frontends/textual_frontend.py +157 -0
- praisonai_code/cli/interactive/praison_io.py +502 -0
- praisonai_code/cli/interactive/repl.py +297 -0
- praisonai_code/cli/interactive/split_tui.py +456 -0
- praisonai_code/cli/interactive/tui_app.py +457 -0
- praisonai_code/cli/langfuse_client.py +360 -0
- praisonai_code/cli/main.py +7421 -0
- praisonai_code/cli/output/__init__.py +25 -0
- praisonai_code/cli/output/console.py +456 -0
- praisonai_code/cli/output/event_bridge.py +191 -0
- praisonai_code/cli/schedule_cli.py +54 -0
- praisonai_code/cli/schema_provider.py +23 -0
- praisonai_code/cli/session/__init__.py +16 -0
- praisonai_code/cli/session/resume.py +148 -0
- praisonai_code/cli/session/unified.py +548 -0
- praisonai_code/cli/state/__init__.py +31 -0
- praisonai_code/cli/state/identifiers.py +161 -0
- praisonai_code/cli/state/project_sessions.py +383 -0
- praisonai_code/cli/state/sessions.py +390 -0
- praisonai_code/cli/ui/__init__.py +160 -0
- praisonai_code/cli/ui/config.py +46 -0
- praisonai_code/cli/ui/events.py +61 -0
- praisonai_code/cli/ui/mg_backend.py +342 -0
- praisonai_code/cli/ui/plain.py +133 -0
- praisonai_code/cli/ui/rich_backend.py +162 -0
- praisonai_code/cli/unified_schema.py +655 -0
- praisonai_code/cli/utils/env_utils.py +126 -0
- praisonai_code/cli/utils/project.py +131 -0
- praisonai_code/cli_backends/__init__.py +73 -0
- praisonai_code/cli_backends/claude.py +373 -0
- praisonai_code/cli_backends/registry.py +113 -0
- praisonai_code/runtime/__init__.py +36 -0
- praisonai_code/runtime/__main__.py +81 -0
- praisonai_code/runtime/client.py +131 -0
- praisonai_code/runtime/descriptor.py +209 -0
- praisonai_code/runtime/server.py +356 -0
- praisonai_code-0.0.1.dist-info/METADATA +80 -0
- praisonai_code-0.0.1.dist-info/RECORD +309 -0
- praisonai_code-0.0.1.dist-info/WHEEL +5 -0
- 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']
|