PraisonAI 3.0.0__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/__init__.py +54 -0
- praisonai/__main__.py +15 -0
- praisonai/acp/__init__.py +54 -0
- praisonai/acp/config.py +159 -0
- praisonai/acp/server.py +587 -0
- praisonai/acp/session.py +219 -0
- praisonai/adapters/__init__.py +50 -0
- praisonai/adapters/readers.py +395 -0
- praisonai/adapters/rerankers.py +315 -0
- praisonai/adapters/retrievers.py +394 -0
- praisonai/adapters/vector_stores.py +409 -0
- praisonai/agent_scheduler.py +337 -0
- praisonai/agents_generator.py +903 -0
- praisonai/api/call.py +292 -0
- praisonai/auto.py +1197 -0
- praisonai/capabilities/__init__.py +275 -0
- praisonai/capabilities/a2a.py +140 -0
- praisonai/capabilities/assistants.py +283 -0
- praisonai/capabilities/audio.py +320 -0
- praisonai/capabilities/batches.py +469 -0
- praisonai/capabilities/completions.py +336 -0
- praisonai/capabilities/container_files.py +155 -0
- praisonai/capabilities/containers.py +93 -0
- praisonai/capabilities/embeddings.py +158 -0
- praisonai/capabilities/files.py +467 -0
- praisonai/capabilities/fine_tuning.py +293 -0
- praisonai/capabilities/guardrails.py +182 -0
- praisonai/capabilities/images.py +330 -0
- praisonai/capabilities/mcp.py +190 -0
- praisonai/capabilities/messages.py +270 -0
- praisonai/capabilities/moderations.py +154 -0
- praisonai/capabilities/ocr.py +217 -0
- praisonai/capabilities/passthrough.py +204 -0
- praisonai/capabilities/rag.py +207 -0
- praisonai/capabilities/realtime.py +160 -0
- praisonai/capabilities/rerank.py +165 -0
- praisonai/capabilities/responses.py +266 -0
- praisonai/capabilities/search.py +109 -0
- praisonai/capabilities/skills.py +133 -0
- praisonai/capabilities/vector_store_files.py +334 -0
- praisonai/capabilities/vector_stores.py +304 -0
- praisonai/capabilities/videos.py +141 -0
- praisonai/chainlit_ui.py +304 -0
- praisonai/chat/__init__.py +106 -0
- praisonai/chat/app.py +125 -0
- praisonai/cli/__init__.py +26 -0
- praisonai/cli/app.py +213 -0
- praisonai/cli/commands/__init__.py +75 -0
- praisonai/cli/commands/acp.py +70 -0
- praisonai/cli/commands/completion.py +333 -0
- praisonai/cli/commands/config.py +166 -0
- praisonai/cli/commands/debug.py +142 -0
- praisonai/cli/commands/diag.py +55 -0
- praisonai/cli/commands/doctor.py +166 -0
- praisonai/cli/commands/environment.py +179 -0
- praisonai/cli/commands/lsp.py +112 -0
- praisonai/cli/commands/mcp.py +210 -0
- praisonai/cli/commands/profile.py +457 -0
- praisonai/cli/commands/run.py +228 -0
- praisonai/cli/commands/schedule.py +150 -0
- praisonai/cli/commands/serve.py +97 -0
- praisonai/cli/commands/session.py +212 -0
- praisonai/cli/commands/traces.py +145 -0
- praisonai/cli/commands/version.py +101 -0
- praisonai/cli/configuration/__init__.py +18 -0
- praisonai/cli/configuration/loader.py +353 -0
- praisonai/cli/configuration/paths.py +114 -0
- praisonai/cli/configuration/schema.py +164 -0
- praisonai/cli/features/__init__.py +268 -0
- praisonai/cli/features/acp.py +236 -0
- praisonai/cli/features/action_orchestrator.py +546 -0
- praisonai/cli/features/agent_scheduler.py +773 -0
- praisonai/cli/features/agent_tools.py +474 -0
- praisonai/cli/features/agents.py +375 -0
- praisonai/cli/features/at_mentions.py +471 -0
- praisonai/cli/features/auto_memory.py +182 -0
- praisonai/cli/features/autonomy_mode.py +490 -0
- praisonai/cli/features/background.py +356 -0
- praisonai/cli/features/base.py +168 -0
- praisonai/cli/features/capabilities.py +1326 -0
- praisonai/cli/features/checkpoints.py +338 -0
- praisonai/cli/features/code_intelligence.py +652 -0
- praisonai/cli/features/compaction.py +294 -0
- praisonai/cli/features/compare.py +534 -0
- praisonai/cli/features/cost_tracker.py +514 -0
- praisonai/cli/features/debug.py +810 -0
- praisonai/cli/features/deploy.py +517 -0
- praisonai/cli/features/diag.py +289 -0
- praisonai/cli/features/doctor/__init__.py +63 -0
- praisonai/cli/features/doctor/checks/__init__.py +24 -0
- praisonai/cli/features/doctor/checks/acp_checks.py +240 -0
- praisonai/cli/features/doctor/checks/config_checks.py +366 -0
- praisonai/cli/features/doctor/checks/db_checks.py +366 -0
- praisonai/cli/features/doctor/checks/env_checks.py +543 -0
- praisonai/cli/features/doctor/checks/lsp_checks.py +199 -0
- praisonai/cli/features/doctor/checks/mcp_checks.py +349 -0
- praisonai/cli/features/doctor/checks/memory_checks.py +268 -0
- praisonai/cli/features/doctor/checks/network_checks.py +251 -0
- praisonai/cli/features/doctor/checks/obs_checks.py +328 -0
- praisonai/cli/features/doctor/checks/performance_checks.py +235 -0
- praisonai/cli/features/doctor/checks/permissions_checks.py +259 -0
- praisonai/cli/features/doctor/checks/selftest_checks.py +322 -0
- praisonai/cli/features/doctor/checks/serve_checks.py +426 -0
- praisonai/cli/features/doctor/checks/skills_checks.py +231 -0
- praisonai/cli/features/doctor/checks/tools_checks.py +371 -0
- praisonai/cli/features/doctor/engine.py +266 -0
- praisonai/cli/features/doctor/formatters.py +310 -0
- praisonai/cli/features/doctor/handler.py +397 -0
- praisonai/cli/features/doctor/models.py +264 -0
- praisonai/cli/features/doctor/registry.py +239 -0
- praisonai/cli/features/endpoints.py +1019 -0
- praisonai/cli/features/eval.py +560 -0
- praisonai/cli/features/external_agents.py +231 -0
- praisonai/cli/features/fast_context.py +410 -0
- praisonai/cli/features/flow_display.py +566 -0
- praisonai/cli/features/git_integration.py +651 -0
- praisonai/cli/features/guardrail.py +171 -0
- praisonai/cli/features/handoff.py +185 -0
- praisonai/cli/features/hooks.py +583 -0
- praisonai/cli/features/image.py +384 -0
- praisonai/cli/features/interactive_runtime.py +585 -0
- praisonai/cli/features/interactive_tools.py +380 -0
- praisonai/cli/features/interactive_tui.py +603 -0
- praisonai/cli/features/jobs.py +632 -0
- praisonai/cli/features/knowledge.py +531 -0
- praisonai/cli/features/lite.py +244 -0
- praisonai/cli/features/lsp_cli.py +225 -0
- praisonai/cli/features/mcp.py +169 -0
- praisonai/cli/features/message_queue.py +587 -0
- praisonai/cli/features/metrics.py +211 -0
- praisonai/cli/features/n8n.py +673 -0
- praisonai/cli/features/observability.py +293 -0
- praisonai/cli/features/ollama.py +361 -0
- praisonai/cli/features/output_style.py +273 -0
- praisonai/cli/features/package.py +631 -0
- praisonai/cli/features/performance.py +308 -0
- praisonai/cli/features/persistence.py +636 -0
- praisonai/cli/features/profile.py +226 -0
- praisonai/cli/features/profiler/__init__.py +81 -0
- praisonai/cli/features/profiler/core.py +558 -0
- praisonai/cli/features/profiler/optimizations.py +652 -0
- praisonai/cli/features/profiler/suite.py +386 -0
- praisonai/cli/features/profiling.py +350 -0
- praisonai/cli/features/queue/__init__.py +73 -0
- praisonai/cli/features/queue/manager.py +395 -0
- praisonai/cli/features/queue/models.py +286 -0
- praisonai/cli/features/queue/persistence.py +564 -0
- praisonai/cli/features/queue/scheduler.py +484 -0
- praisonai/cli/features/queue/worker.py +372 -0
- praisonai/cli/features/recipe.py +1723 -0
- praisonai/cli/features/recipes.py +449 -0
- praisonai/cli/features/registry.py +229 -0
- praisonai/cli/features/repo_map.py +860 -0
- praisonai/cli/features/router.py +466 -0
- praisonai/cli/features/sandbox_executor.py +515 -0
- praisonai/cli/features/serve.py +829 -0
- praisonai/cli/features/session.py +222 -0
- praisonai/cli/features/skills.py +856 -0
- praisonai/cli/features/slash_commands.py +650 -0
- praisonai/cli/features/telemetry.py +179 -0
- praisonai/cli/features/templates.py +1384 -0
- praisonai/cli/features/thinking.py +305 -0
- praisonai/cli/features/todo.py +334 -0
- praisonai/cli/features/tools.py +680 -0
- praisonai/cli/features/tui/__init__.py +83 -0
- praisonai/cli/features/tui/app.py +580 -0
- praisonai/cli/features/tui/cli.py +566 -0
- praisonai/cli/features/tui/debug.py +511 -0
- praisonai/cli/features/tui/events.py +99 -0
- praisonai/cli/features/tui/mock_provider.py +328 -0
- praisonai/cli/features/tui/orchestrator.py +652 -0
- praisonai/cli/features/tui/screens/__init__.py +50 -0
- praisonai/cli/features/tui/screens/main.py +245 -0
- praisonai/cli/features/tui/screens/queue.py +174 -0
- praisonai/cli/features/tui/screens/session.py +124 -0
- praisonai/cli/features/tui/screens/settings.py +148 -0
- praisonai/cli/features/tui/widgets/__init__.py +56 -0
- praisonai/cli/features/tui/widgets/chat.py +261 -0
- praisonai/cli/features/tui/widgets/composer.py +224 -0
- praisonai/cli/features/tui/widgets/queue_panel.py +200 -0
- praisonai/cli/features/tui/widgets/status.py +167 -0
- praisonai/cli/features/tui/widgets/tool_panel.py +248 -0
- praisonai/cli/features/workflow.py +720 -0
- praisonai/cli/legacy.py +236 -0
- praisonai/cli/main.py +5559 -0
- praisonai/cli/schedule_cli.py +54 -0
- praisonai/cli/state/__init__.py +31 -0
- praisonai/cli/state/identifiers.py +161 -0
- praisonai/cli/state/sessions.py +313 -0
- praisonai/code/__init__.py +93 -0
- praisonai/code/agent_tools.py +344 -0
- praisonai/code/diff/__init__.py +21 -0
- praisonai/code/diff/diff_strategy.py +432 -0
- praisonai/code/tools/__init__.py +27 -0
- praisonai/code/tools/apply_diff.py +221 -0
- praisonai/code/tools/execute_command.py +275 -0
- praisonai/code/tools/list_files.py +274 -0
- praisonai/code/tools/read_file.py +206 -0
- praisonai/code/tools/search_replace.py +248 -0
- praisonai/code/tools/write_file.py +217 -0
- praisonai/code/utils/__init__.py +46 -0
- praisonai/code/utils/file_utils.py +307 -0
- praisonai/code/utils/ignore_utils.py +308 -0
- praisonai/code/utils/text_utils.py +276 -0
- praisonai/db/__init__.py +64 -0
- praisonai/db/adapter.py +531 -0
- praisonai/deploy/__init__.py +62 -0
- praisonai/deploy/api.py +231 -0
- praisonai/deploy/docker.py +454 -0
- praisonai/deploy/doctor.py +367 -0
- praisonai/deploy/main.py +327 -0
- praisonai/deploy/models.py +179 -0
- praisonai/deploy/providers/__init__.py +33 -0
- praisonai/deploy/providers/aws.py +331 -0
- praisonai/deploy/providers/azure.py +358 -0
- praisonai/deploy/providers/base.py +101 -0
- praisonai/deploy/providers/gcp.py +314 -0
- praisonai/deploy/schema.py +208 -0
- praisonai/deploy.py +185 -0
- praisonai/endpoints/__init__.py +53 -0
- praisonai/endpoints/a2u_server.py +410 -0
- praisonai/endpoints/discovery.py +165 -0
- praisonai/endpoints/providers/__init__.py +28 -0
- praisonai/endpoints/providers/a2a.py +253 -0
- praisonai/endpoints/providers/a2u.py +208 -0
- praisonai/endpoints/providers/agents_api.py +171 -0
- praisonai/endpoints/providers/base.py +231 -0
- praisonai/endpoints/providers/mcp.py +263 -0
- praisonai/endpoints/providers/recipe.py +206 -0
- praisonai/endpoints/providers/tools_mcp.py +150 -0
- praisonai/endpoints/registry.py +131 -0
- praisonai/endpoints/server.py +161 -0
- praisonai/inbuilt_tools/__init__.py +24 -0
- praisonai/inbuilt_tools/autogen_tools.py +117 -0
- praisonai/inc/__init__.py +2 -0
- praisonai/inc/config.py +96 -0
- praisonai/inc/models.py +155 -0
- praisonai/integrations/__init__.py +56 -0
- praisonai/integrations/base.py +303 -0
- praisonai/integrations/claude_code.py +270 -0
- praisonai/integrations/codex_cli.py +255 -0
- praisonai/integrations/cursor_cli.py +195 -0
- praisonai/integrations/gemini_cli.py +222 -0
- praisonai/jobs/__init__.py +67 -0
- praisonai/jobs/executor.py +425 -0
- praisonai/jobs/models.py +230 -0
- praisonai/jobs/router.py +314 -0
- praisonai/jobs/server.py +186 -0
- praisonai/jobs/store.py +203 -0
- praisonai/llm/__init__.py +66 -0
- praisonai/llm/registry.py +382 -0
- praisonai/mcp_server/__init__.py +152 -0
- praisonai/mcp_server/adapters/__init__.py +74 -0
- praisonai/mcp_server/adapters/agents.py +128 -0
- praisonai/mcp_server/adapters/capabilities.py +168 -0
- praisonai/mcp_server/adapters/cli_tools.py +568 -0
- praisonai/mcp_server/adapters/extended_capabilities.py +462 -0
- praisonai/mcp_server/adapters/knowledge.py +93 -0
- praisonai/mcp_server/adapters/memory.py +104 -0
- praisonai/mcp_server/adapters/prompts.py +306 -0
- praisonai/mcp_server/adapters/resources.py +124 -0
- praisonai/mcp_server/adapters/tools_bridge.py +280 -0
- praisonai/mcp_server/auth/__init__.py +48 -0
- praisonai/mcp_server/auth/api_key.py +291 -0
- praisonai/mcp_server/auth/oauth.py +460 -0
- praisonai/mcp_server/auth/oidc.py +289 -0
- praisonai/mcp_server/auth/scopes.py +260 -0
- praisonai/mcp_server/cli.py +852 -0
- praisonai/mcp_server/elicitation.py +445 -0
- praisonai/mcp_server/icons.py +302 -0
- praisonai/mcp_server/recipe_adapter.py +573 -0
- praisonai/mcp_server/recipe_cli.py +824 -0
- praisonai/mcp_server/registry.py +703 -0
- praisonai/mcp_server/sampling.py +422 -0
- praisonai/mcp_server/server.py +490 -0
- praisonai/mcp_server/tasks.py +443 -0
- praisonai/mcp_server/transports/__init__.py +18 -0
- praisonai/mcp_server/transports/http_stream.py +376 -0
- praisonai/mcp_server/transports/stdio.py +132 -0
- praisonai/persistence/__init__.py +84 -0
- praisonai/persistence/config.py +238 -0
- praisonai/persistence/conversation/__init__.py +25 -0
- praisonai/persistence/conversation/async_mysql.py +427 -0
- praisonai/persistence/conversation/async_postgres.py +410 -0
- praisonai/persistence/conversation/async_sqlite.py +371 -0
- praisonai/persistence/conversation/base.py +151 -0
- praisonai/persistence/conversation/json_store.py +250 -0
- praisonai/persistence/conversation/mysql.py +387 -0
- praisonai/persistence/conversation/postgres.py +401 -0
- praisonai/persistence/conversation/singlestore.py +240 -0
- praisonai/persistence/conversation/sqlite.py +341 -0
- praisonai/persistence/conversation/supabase.py +203 -0
- praisonai/persistence/conversation/surrealdb.py +287 -0
- praisonai/persistence/factory.py +301 -0
- praisonai/persistence/hooks/__init__.py +18 -0
- praisonai/persistence/hooks/agent_hooks.py +297 -0
- praisonai/persistence/knowledge/__init__.py +26 -0
- praisonai/persistence/knowledge/base.py +144 -0
- praisonai/persistence/knowledge/cassandra.py +232 -0
- praisonai/persistence/knowledge/chroma.py +295 -0
- praisonai/persistence/knowledge/clickhouse.py +242 -0
- praisonai/persistence/knowledge/cosmosdb_vector.py +438 -0
- praisonai/persistence/knowledge/couchbase.py +286 -0
- praisonai/persistence/knowledge/lancedb.py +216 -0
- praisonai/persistence/knowledge/langchain_adapter.py +291 -0
- praisonai/persistence/knowledge/lightrag_adapter.py +212 -0
- praisonai/persistence/knowledge/llamaindex_adapter.py +256 -0
- praisonai/persistence/knowledge/milvus.py +277 -0
- praisonai/persistence/knowledge/mongodb_vector.py +306 -0
- praisonai/persistence/knowledge/pgvector.py +335 -0
- praisonai/persistence/knowledge/pinecone.py +253 -0
- praisonai/persistence/knowledge/qdrant.py +301 -0
- praisonai/persistence/knowledge/redis_vector.py +291 -0
- praisonai/persistence/knowledge/singlestore_vector.py +299 -0
- praisonai/persistence/knowledge/surrealdb_vector.py +309 -0
- praisonai/persistence/knowledge/upstash_vector.py +266 -0
- praisonai/persistence/knowledge/weaviate.py +223 -0
- praisonai/persistence/migrations/__init__.py +10 -0
- praisonai/persistence/migrations/manager.py +251 -0
- praisonai/persistence/orchestrator.py +406 -0
- praisonai/persistence/state/__init__.py +21 -0
- praisonai/persistence/state/async_mongodb.py +200 -0
- praisonai/persistence/state/base.py +107 -0
- praisonai/persistence/state/dynamodb.py +226 -0
- praisonai/persistence/state/firestore.py +175 -0
- praisonai/persistence/state/gcs.py +155 -0
- praisonai/persistence/state/memory.py +245 -0
- praisonai/persistence/state/mongodb.py +158 -0
- praisonai/persistence/state/redis.py +190 -0
- praisonai/persistence/state/upstash.py +144 -0
- praisonai/persistence/tests/__init__.py +3 -0
- praisonai/persistence/tests/test_all_backends.py +633 -0
- praisonai/profiler.py +1214 -0
- praisonai/recipe/__init__.py +134 -0
- praisonai/recipe/bridge.py +278 -0
- praisonai/recipe/core.py +893 -0
- praisonai/recipe/exceptions.py +54 -0
- praisonai/recipe/history.py +402 -0
- praisonai/recipe/models.py +266 -0
- praisonai/recipe/operations.py +440 -0
- praisonai/recipe/policy.py +422 -0
- praisonai/recipe/registry.py +849 -0
- praisonai/recipe/runtime.py +214 -0
- praisonai/recipe/security.py +711 -0
- praisonai/recipe/serve.py +859 -0
- praisonai/recipe/server.py +613 -0
- praisonai/scheduler/__init__.py +45 -0
- praisonai/scheduler/agent_scheduler.py +552 -0
- praisonai/scheduler/base.py +124 -0
- praisonai/scheduler/daemon_manager.py +225 -0
- praisonai/scheduler/state_manager.py +155 -0
- praisonai/scheduler/yaml_loader.py +193 -0
- praisonai/scheduler.py +194 -0
- praisonai/setup/__init__.py +1 -0
- praisonai/setup/build.py +21 -0
- praisonai/setup/post_install.py +23 -0
- praisonai/setup/setup_conda_env.py +25 -0
- praisonai/setup.py +16 -0
- praisonai/templates/__init__.py +116 -0
- praisonai/templates/cache.py +364 -0
- praisonai/templates/dependency_checker.py +358 -0
- praisonai/templates/discovery.py +391 -0
- praisonai/templates/loader.py +564 -0
- praisonai/templates/registry.py +511 -0
- praisonai/templates/resolver.py +206 -0
- praisonai/templates/security.py +327 -0
- praisonai/templates/tool_override.py +498 -0
- praisonai/templates/tools_doctor.py +256 -0
- praisonai/test.py +105 -0
- praisonai/train.py +562 -0
- praisonai/train_vision.py +306 -0
- praisonai/ui/agents.py +824 -0
- praisonai/ui/callbacks.py +57 -0
- praisonai/ui/chainlit_compat.py +246 -0
- praisonai/ui/chat.py +532 -0
- praisonai/ui/code.py +717 -0
- praisonai/ui/colab.py +474 -0
- praisonai/ui/colab_chainlit.py +81 -0
- praisonai/ui/components/aicoder.py +284 -0
- praisonai/ui/context.py +283 -0
- praisonai/ui/database_config.py +56 -0
- praisonai/ui/db.py +294 -0
- praisonai/ui/realtime.py +488 -0
- praisonai/ui/realtimeclient/__init__.py +756 -0
- praisonai/ui/realtimeclient/tools.py +242 -0
- praisonai/ui/sql_alchemy.py +710 -0
- praisonai/upload_vision.py +140 -0
- praisonai/version.py +1 -0
- praisonai-3.0.0.dist-info/METADATA +3493 -0
- praisonai-3.0.0.dist-info/RECORD +393 -0
- praisonai-3.0.0.dist-info/WHEEL +5 -0
- praisonai-3.0.0.dist-info/entry_points.txt +4 -0
- praisonai-3.0.0.dist-info/top_level.txt +1 -0
praisonai/recipe/core.py
ADDED
|
@@ -0,0 +1,893 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Recipe Core API
|
|
3
|
+
|
|
4
|
+
Provides the main recipe execution functions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
import uuid
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, Iterator, List, Optional, Union
|
|
13
|
+
|
|
14
|
+
from .models import (
|
|
15
|
+
RecipeResult,
|
|
16
|
+
RecipeEvent,
|
|
17
|
+
RecipeConfig,
|
|
18
|
+
RecipeInfo,
|
|
19
|
+
RecipeStatus,
|
|
20
|
+
ValidationResult,
|
|
21
|
+
)
|
|
22
|
+
from .exceptions import (
|
|
23
|
+
RecipeError,
|
|
24
|
+
RecipeNotFoundError,
|
|
25
|
+
RecipeDependencyError,
|
|
26
|
+
RecipePolicyError,
|
|
27
|
+
RecipeTimeoutError,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# Default denied tools (dangerous by default)
|
|
32
|
+
DEFAULT_DENIED_TOOLS = [
|
|
33
|
+
"shell.exec",
|
|
34
|
+
"shell.run",
|
|
35
|
+
"shell_tool",
|
|
36
|
+
"file.write",
|
|
37
|
+
"file.delete",
|
|
38
|
+
"fs.write",
|
|
39
|
+
"fs.delete",
|
|
40
|
+
"network.unrestricted",
|
|
41
|
+
"db.write",
|
|
42
|
+
"db.delete",
|
|
43
|
+
"execute_command",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _generate_run_id() -> str:
|
|
48
|
+
"""Generate a unique run ID."""
|
|
49
|
+
return f"run-{uuid.uuid4().hex[:12]}"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _generate_trace_id() -> str:
|
|
53
|
+
"""Generate a trace ID for distributed tracing."""
|
|
54
|
+
return uuid.uuid4().hex
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _get_timestamp() -> str:
|
|
58
|
+
"""Get current timestamp in ISO format."""
|
|
59
|
+
return datetime.now(timezone.utc).isoformat()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def get_template_search_paths() -> List[Path]:
|
|
63
|
+
"""
|
|
64
|
+
Get list of paths to search for recipe templates.
|
|
65
|
+
|
|
66
|
+
Precedence order:
|
|
67
|
+
1. PRAISONAI_RECIPE_PATH environment variable (colon-separated)
|
|
68
|
+
2. Current working directory ./recipes
|
|
69
|
+
3. User home ~/.praison/recipes
|
|
70
|
+
4. Agent-Recipes package templates (if installed)
|
|
71
|
+
5. Built-in templates
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of Path objects to search for templates
|
|
75
|
+
"""
|
|
76
|
+
paths = []
|
|
77
|
+
|
|
78
|
+
# 1. Environment variable
|
|
79
|
+
env_path = os.environ.get("PRAISONAI_RECIPE_PATH")
|
|
80
|
+
if env_path:
|
|
81
|
+
for p in env_path.split(os.pathsep):
|
|
82
|
+
path = Path(p).expanduser()
|
|
83
|
+
if path.exists():
|
|
84
|
+
paths.append(path)
|
|
85
|
+
|
|
86
|
+
# 2. Current working directory
|
|
87
|
+
cwd_recipes = Path.cwd() / "recipes"
|
|
88
|
+
if cwd_recipes.exists():
|
|
89
|
+
paths.append(cwd_recipes)
|
|
90
|
+
|
|
91
|
+
# 3. User home
|
|
92
|
+
home_recipes = Path.home() / ".praison" / "recipes"
|
|
93
|
+
if home_recipes.exists():
|
|
94
|
+
paths.append(home_recipes)
|
|
95
|
+
|
|
96
|
+
# 4. Agent-Recipes package (lazy import)
|
|
97
|
+
try:
|
|
98
|
+
import agent_recipes
|
|
99
|
+
if hasattr(agent_recipes, 'get_template_path'):
|
|
100
|
+
agent_path = Path(agent_recipes.get_template_path(""))
|
|
101
|
+
if agent_path.parent.exists():
|
|
102
|
+
paths.append(agent_path.parent)
|
|
103
|
+
elif hasattr(agent_recipes, '__file__'):
|
|
104
|
+
agent_templates = Path(agent_recipes.__file__).parent / "templates"
|
|
105
|
+
if agent_templates.exists():
|
|
106
|
+
paths.append(agent_templates)
|
|
107
|
+
except ImportError:
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# 5. Built-in templates (relative to this package)
|
|
111
|
+
builtin = Path(__file__).parent.parent / "templates"
|
|
112
|
+
if builtin.exists():
|
|
113
|
+
paths.append(builtin)
|
|
114
|
+
|
|
115
|
+
return paths
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def reload_registry():
|
|
119
|
+
"""
|
|
120
|
+
Reload the recipe registry, clearing any cached templates.
|
|
121
|
+
|
|
122
|
+
This is useful for hot-reloading recipes during development
|
|
123
|
+
or when using the /admin/reload endpoint.
|
|
124
|
+
"""
|
|
125
|
+
global _recipe_cache
|
|
126
|
+
if '_recipe_cache' in globals():
|
|
127
|
+
_recipe_cache.clear()
|
|
128
|
+
|
|
129
|
+
# Also clear any module-level caches
|
|
130
|
+
import importlib
|
|
131
|
+
try:
|
|
132
|
+
from praisonai import recipe as recipe_module
|
|
133
|
+
importlib.reload(recipe_module)
|
|
134
|
+
except Exception:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Recipe cache for performance
|
|
139
|
+
_recipe_cache: Dict[str, Any] = {}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def run(
|
|
143
|
+
name: str,
|
|
144
|
+
input: Union[str, Dict[str, Any]] = None,
|
|
145
|
+
config: Optional[Dict[str, Any]] = None,
|
|
146
|
+
session_id: Optional[str] = None,
|
|
147
|
+
options: Optional[Dict[str, Any]] = None,
|
|
148
|
+
) -> RecipeResult:
|
|
149
|
+
"""
|
|
150
|
+
Run a recipe by name.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
name: Recipe name or URI (e.g., "support-reply", "github:user/repo/recipe")
|
|
154
|
+
input: Input data (string path or dict)
|
|
155
|
+
config: Optional config overrides
|
|
156
|
+
session_id: Optional session ID for state grouping
|
|
157
|
+
options: Execution options:
|
|
158
|
+
- timeout_sec: Timeout in seconds (default: 300)
|
|
159
|
+
- dry_run: Validate without executing (default: False)
|
|
160
|
+
- verbose: Enable verbose output (default: False)
|
|
161
|
+
- force: Force execution even with missing deps (default: False)
|
|
162
|
+
- offline: Use only cached templates (default: False)
|
|
163
|
+
- mode: Execution mode - "dev" or "prod" (default: "dev")
|
|
164
|
+
- allow_dangerous_tools: Allow dangerous tools (default: False)
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
RecipeResult with run_id, status, output, metrics, trace
|
|
168
|
+
|
|
169
|
+
Example:
|
|
170
|
+
>>> from praisonai import recipe
|
|
171
|
+
>>> result = recipe.run("support-reply", input={"ticket_id": "T-123"})
|
|
172
|
+
>>> if result.ok:
|
|
173
|
+
... print(result.output["reply"])
|
|
174
|
+
"""
|
|
175
|
+
options = options or {}
|
|
176
|
+
config = config or {}
|
|
177
|
+
input = input or {}
|
|
178
|
+
|
|
179
|
+
# Generate identifiers
|
|
180
|
+
run_id = _generate_run_id()
|
|
181
|
+
trace_id = _generate_trace_id()
|
|
182
|
+
session_id = session_id or f"session-{uuid.uuid4().hex[:8]}"
|
|
183
|
+
|
|
184
|
+
start_time = time.time()
|
|
185
|
+
|
|
186
|
+
trace = {
|
|
187
|
+
"run_id": run_id,
|
|
188
|
+
"trace_id": trace_id,
|
|
189
|
+
"session_id": session_id,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try:
|
|
193
|
+
# Load template
|
|
194
|
+
recipe_config = _load_recipe(name, offline=options.get("offline", False))
|
|
195
|
+
|
|
196
|
+
if recipe_config is None:
|
|
197
|
+
return RecipeResult(
|
|
198
|
+
run_id=run_id,
|
|
199
|
+
recipe=name,
|
|
200
|
+
version="unknown",
|
|
201
|
+
status=RecipeStatus.FAILED,
|
|
202
|
+
error=f"Recipe not found: {name}",
|
|
203
|
+
metrics={"duration_sec": time.time() - start_time},
|
|
204
|
+
trace=trace,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Check dependencies
|
|
208
|
+
if not options.get("force", False):
|
|
209
|
+
dep_result = _check_dependencies(recipe_config)
|
|
210
|
+
if not dep_result["all_satisfied"]:
|
|
211
|
+
missing = _format_missing_deps(dep_result)
|
|
212
|
+
return RecipeResult(
|
|
213
|
+
run_id=run_id,
|
|
214
|
+
recipe=recipe_config.name,
|
|
215
|
+
version=recipe_config.version,
|
|
216
|
+
status=RecipeStatus.MISSING_DEPS,
|
|
217
|
+
error=f"Missing dependencies: {', '.join(missing)}",
|
|
218
|
+
metrics={"duration_sec": time.time() - start_time},
|
|
219
|
+
trace=trace,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Check tool permissions
|
|
223
|
+
if not options.get("allow_dangerous_tools", False):
|
|
224
|
+
policy_error = _check_tool_policy(recipe_config)
|
|
225
|
+
if policy_error:
|
|
226
|
+
return RecipeResult(
|
|
227
|
+
run_id=run_id,
|
|
228
|
+
recipe=recipe_config.name,
|
|
229
|
+
version=recipe_config.version,
|
|
230
|
+
status=RecipeStatus.POLICY_DENIED,
|
|
231
|
+
error=policy_error,
|
|
232
|
+
metrics={"duration_sec": time.time() - start_time},
|
|
233
|
+
trace=trace,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Dry run mode
|
|
237
|
+
if options.get("dry_run", False):
|
|
238
|
+
return RecipeResult(
|
|
239
|
+
run_id=run_id,
|
|
240
|
+
recipe=recipe_config.name,
|
|
241
|
+
version=recipe_config.version,
|
|
242
|
+
status=RecipeStatus.DRY_RUN,
|
|
243
|
+
output={
|
|
244
|
+
"plan": "Would execute recipe with provided config",
|
|
245
|
+
"recipe": recipe_config.name,
|
|
246
|
+
"config": {**recipe_config.defaults, **config},
|
|
247
|
+
},
|
|
248
|
+
metrics={"duration_sec": time.time() - start_time},
|
|
249
|
+
trace=trace,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Merge input and config
|
|
253
|
+
if isinstance(input, str):
|
|
254
|
+
merged_config = {**recipe_config.defaults, "input": input, **config}
|
|
255
|
+
else:
|
|
256
|
+
merged_config = {**recipe_config.defaults, **input, **config}
|
|
257
|
+
|
|
258
|
+
# Execute recipe
|
|
259
|
+
output = _execute_recipe(
|
|
260
|
+
recipe_config,
|
|
261
|
+
merged_config,
|
|
262
|
+
session_id,
|
|
263
|
+
options,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
duration = time.time() - start_time
|
|
267
|
+
|
|
268
|
+
return RecipeResult(
|
|
269
|
+
run_id=run_id,
|
|
270
|
+
recipe=recipe_config.name,
|
|
271
|
+
version=recipe_config.version,
|
|
272
|
+
status=RecipeStatus.SUCCESS,
|
|
273
|
+
output=output,
|
|
274
|
+
metrics={"duration_sec": round(duration, 2)},
|
|
275
|
+
trace=trace,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
except RecipeNotFoundError as e:
|
|
279
|
+
return RecipeResult(
|
|
280
|
+
run_id=run_id,
|
|
281
|
+
recipe=name,
|
|
282
|
+
version="unknown",
|
|
283
|
+
status=RecipeStatus.FAILED,
|
|
284
|
+
error=str(e),
|
|
285
|
+
metrics={"duration_sec": time.time() - start_time},
|
|
286
|
+
trace=trace,
|
|
287
|
+
)
|
|
288
|
+
except RecipeDependencyError as e:
|
|
289
|
+
return RecipeResult(
|
|
290
|
+
run_id=run_id,
|
|
291
|
+
recipe=e.recipe or name,
|
|
292
|
+
version="unknown",
|
|
293
|
+
status=RecipeStatus.MISSING_DEPS,
|
|
294
|
+
error=str(e),
|
|
295
|
+
metrics={"duration_sec": time.time() - start_time},
|
|
296
|
+
trace=trace,
|
|
297
|
+
)
|
|
298
|
+
except RecipePolicyError as e:
|
|
299
|
+
return RecipeResult(
|
|
300
|
+
run_id=run_id,
|
|
301
|
+
recipe=e.recipe or name,
|
|
302
|
+
version="unknown",
|
|
303
|
+
status=RecipeStatus.POLICY_DENIED,
|
|
304
|
+
error=str(e),
|
|
305
|
+
metrics={"duration_sec": time.time() - start_time},
|
|
306
|
+
trace=trace,
|
|
307
|
+
)
|
|
308
|
+
except RecipeTimeoutError as e:
|
|
309
|
+
return RecipeResult(
|
|
310
|
+
run_id=run_id,
|
|
311
|
+
recipe=name,
|
|
312
|
+
version="unknown",
|
|
313
|
+
status=RecipeStatus.TIMEOUT,
|
|
314
|
+
error=str(e),
|
|
315
|
+
metrics={"duration_sec": time.time() - start_time},
|
|
316
|
+
trace=trace,
|
|
317
|
+
)
|
|
318
|
+
except Exception as e:
|
|
319
|
+
return RecipeResult(
|
|
320
|
+
run_id=run_id,
|
|
321
|
+
recipe=name,
|
|
322
|
+
version="unknown",
|
|
323
|
+
status=RecipeStatus.FAILED,
|
|
324
|
+
error=str(e),
|
|
325
|
+
metrics={"duration_sec": time.time() - start_time},
|
|
326
|
+
trace=trace,
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def run_stream(
|
|
331
|
+
name: str,
|
|
332
|
+
input: Union[str, Dict[str, Any]] = None,
|
|
333
|
+
config: Optional[Dict[str, Any]] = None,
|
|
334
|
+
session_id: Optional[str] = None,
|
|
335
|
+
options: Optional[Dict[str, Any]] = None,
|
|
336
|
+
) -> Iterator[RecipeEvent]:
|
|
337
|
+
"""
|
|
338
|
+
Run a recipe with streaming events.
|
|
339
|
+
|
|
340
|
+
Yields RecipeEvent objects for progress tracking.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
name: Recipe name or URI
|
|
344
|
+
input: Input data
|
|
345
|
+
config: Optional config overrides
|
|
346
|
+
session_id: Optional session ID
|
|
347
|
+
options: Execution options
|
|
348
|
+
|
|
349
|
+
Yields:
|
|
350
|
+
RecipeEvent objects with event_type and data
|
|
351
|
+
|
|
352
|
+
Example:
|
|
353
|
+
>>> for event in recipe.run_stream("transcript-generator", input="audio.mp3"):
|
|
354
|
+
... print(f"[{event.event_type}] {event.data}")
|
|
355
|
+
"""
|
|
356
|
+
options = options or {}
|
|
357
|
+
config = config or {}
|
|
358
|
+
input = input or {}
|
|
359
|
+
|
|
360
|
+
run_id = _generate_run_id()
|
|
361
|
+
trace_id = _generate_trace_id()
|
|
362
|
+
session_id = session_id or f"session-{uuid.uuid4().hex[:8]}"
|
|
363
|
+
|
|
364
|
+
# Started event
|
|
365
|
+
yield RecipeEvent(
|
|
366
|
+
event_type="started",
|
|
367
|
+
data={
|
|
368
|
+
"run_id": run_id,
|
|
369
|
+
"recipe": name,
|
|
370
|
+
"trace_id": trace_id,
|
|
371
|
+
"session_id": session_id,
|
|
372
|
+
},
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
# Load recipe
|
|
377
|
+
yield RecipeEvent(
|
|
378
|
+
event_type="progress",
|
|
379
|
+
data={"step": "loading", "message": f"Loading recipe: {name}"},
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
recipe_config = _load_recipe(name, offline=options.get("offline", False))
|
|
383
|
+
|
|
384
|
+
if recipe_config is None:
|
|
385
|
+
yield RecipeEvent(
|
|
386
|
+
event_type="error",
|
|
387
|
+
data={"code": "not_found", "message": f"Recipe not found: {name}"},
|
|
388
|
+
)
|
|
389
|
+
return
|
|
390
|
+
|
|
391
|
+
# Check dependencies
|
|
392
|
+
yield RecipeEvent(
|
|
393
|
+
event_type="progress",
|
|
394
|
+
data={"step": "checking_deps", "message": "Checking dependencies"},
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
if not options.get("force", False):
|
|
398
|
+
dep_result = _check_dependencies(recipe_config)
|
|
399
|
+
if not dep_result["all_satisfied"]:
|
|
400
|
+
missing = _format_missing_deps(dep_result)
|
|
401
|
+
yield RecipeEvent(
|
|
402
|
+
event_type="error",
|
|
403
|
+
data={
|
|
404
|
+
"code": "missing_deps",
|
|
405
|
+
"message": f"Missing dependencies: {', '.join(missing)}",
|
|
406
|
+
"missing": missing,
|
|
407
|
+
},
|
|
408
|
+
)
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
# Dry run
|
|
412
|
+
if options.get("dry_run", False):
|
|
413
|
+
yield RecipeEvent(
|
|
414
|
+
event_type="completed",
|
|
415
|
+
data={
|
|
416
|
+
"run_id": run_id,
|
|
417
|
+
"status": RecipeStatus.DRY_RUN,
|
|
418
|
+
"message": "Dry run completed",
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
return
|
|
422
|
+
|
|
423
|
+
# Execute
|
|
424
|
+
yield RecipeEvent(
|
|
425
|
+
event_type="progress",
|
|
426
|
+
data={"step": "executing", "message": "Executing recipe"},
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Merge config
|
|
430
|
+
if isinstance(input, str):
|
|
431
|
+
merged_config = {**recipe_config.defaults, "input": input, **config}
|
|
432
|
+
else:
|
|
433
|
+
merged_config = {**recipe_config.defaults, **input, **config}
|
|
434
|
+
|
|
435
|
+
start_time = time.time()
|
|
436
|
+
output = _execute_recipe(recipe_config, merged_config, session_id, options)
|
|
437
|
+
duration = time.time() - start_time
|
|
438
|
+
|
|
439
|
+
# Output event
|
|
440
|
+
yield RecipeEvent(
|
|
441
|
+
event_type="output",
|
|
442
|
+
data={"output": output},
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
# Completed event
|
|
446
|
+
yield RecipeEvent(
|
|
447
|
+
event_type="completed",
|
|
448
|
+
data={
|
|
449
|
+
"run_id": run_id,
|
|
450
|
+
"status": RecipeStatus.SUCCESS,
|
|
451
|
+
"duration_sec": round(duration, 2),
|
|
452
|
+
},
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
except Exception as e:
|
|
456
|
+
yield RecipeEvent(
|
|
457
|
+
event_type="error",
|
|
458
|
+
data={"code": "execution_error", "message": str(e)},
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def validate(name: str, offline: bool = False) -> ValidationResult:
|
|
463
|
+
"""
|
|
464
|
+
Validate a recipe and check dependencies.
|
|
465
|
+
|
|
466
|
+
Args:
|
|
467
|
+
name: Recipe name or URI
|
|
468
|
+
offline: Use only cached templates
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
ValidationResult with valid status, errors, warnings, dependencies
|
|
472
|
+
|
|
473
|
+
Example:
|
|
474
|
+
>>> result = recipe.validate("support-reply")
|
|
475
|
+
>>> if result.valid:
|
|
476
|
+
... print("Recipe is valid")
|
|
477
|
+
>>> else:
|
|
478
|
+
... print(f"Errors: {result.errors}")
|
|
479
|
+
"""
|
|
480
|
+
errors = []
|
|
481
|
+
warnings = []
|
|
482
|
+
|
|
483
|
+
try:
|
|
484
|
+
recipe_config = _load_recipe(name, offline=offline)
|
|
485
|
+
|
|
486
|
+
if recipe_config is None:
|
|
487
|
+
return ValidationResult(
|
|
488
|
+
valid=False,
|
|
489
|
+
recipe=name,
|
|
490
|
+
version="unknown",
|
|
491
|
+
errors=[f"Recipe not found: {name}"],
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
# Check dependencies
|
|
495
|
+
dep_result = _check_dependencies(recipe_config)
|
|
496
|
+
|
|
497
|
+
# Check for missing required deps
|
|
498
|
+
for pkg in dep_result.get("packages", []):
|
|
499
|
+
if not pkg.get("available", False):
|
|
500
|
+
errors.append(f"Missing package: {pkg['name']}")
|
|
501
|
+
|
|
502
|
+
for env in dep_result.get("env", []):
|
|
503
|
+
if not env.get("available", False):
|
|
504
|
+
errors.append(f"Missing env var: ${env['name']}")
|
|
505
|
+
|
|
506
|
+
for ext in dep_result.get("external", []):
|
|
507
|
+
if not ext.get("available", False):
|
|
508
|
+
warnings.append(f"Missing external tool: {ext['name']}")
|
|
509
|
+
|
|
510
|
+
# Check tool policy
|
|
511
|
+
policy_error = _check_tool_policy(recipe_config)
|
|
512
|
+
if policy_error:
|
|
513
|
+
warnings.append(f"Tool policy: {policy_error}")
|
|
514
|
+
|
|
515
|
+
return ValidationResult(
|
|
516
|
+
valid=len(errors) == 0,
|
|
517
|
+
recipe=recipe_config.name,
|
|
518
|
+
version=recipe_config.version,
|
|
519
|
+
errors=errors,
|
|
520
|
+
warnings=warnings,
|
|
521
|
+
dependencies=dep_result,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
except Exception as e:
|
|
525
|
+
return ValidationResult(
|
|
526
|
+
valid=False,
|
|
527
|
+
recipe=name,
|
|
528
|
+
version="unknown",
|
|
529
|
+
errors=[str(e)],
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def list_recipes(
|
|
534
|
+
source_filter: Optional[str] = None,
|
|
535
|
+
tags: Optional[List[str]] = None,
|
|
536
|
+
offline: bool = False,
|
|
537
|
+
) -> List[RecipeInfo]:
|
|
538
|
+
"""
|
|
539
|
+
List available recipes.
|
|
540
|
+
|
|
541
|
+
Args:
|
|
542
|
+
source_filter: Filter by source (local, package, github)
|
|
543
|
+
tags: Filter by tags
|
|
544
|
+
offline: Use only cached templates
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
List of RecipeInfo objects
|
|
548
|
+
|
|
549
|
+
Example:
|
|
550
|
+
>>> recipes = recipe.list_recipes(tags=["video"])
|
|
551
|
+
>>> for r in recipes:
|
|
552
|
+
... print(f"{r.name}: {r.description}")
|
|
553
|
+
"""
|
|
554
|
+
try:
|
|
555
|
+
from praisonai.templates import TemplateDiscovery
|
|
556
|
+
|
|
557
|
+
discovery = TemplateDiscovery()
|
|
558
|
+
templates = discovery.list_templates(source_filter=source_filter)
|
|
559
|
+
|
|
560
|
+
recipes = []
|
|
561
|
+
for t in templates:
|
|
562
|
+
# Filter by tags if specified
|
|
563
|
+
if tags:
|
|
564
|
+
template_tags = getattr(t, 'tags', []) or []
|
|
565
|
+
if not any(tag in template_tags for tag in tags):
|
|
566
|
+
continue
|
|
567
|
+
|
|
568
|
+
recipes.append(RecipeInfo(
|
|
569
|
+
name=t.name,
|
|
570
|
+
version=getattr(t, 'version', '1.0.0'),
|
|
571
|
+
description=getattr(t, 'description', ''),
|
|
572
|
+
tags=getattr(t, 'tags', []) or [],
|
|
573
|
+
path=str(t.path) if hasattr(t, 'path') else '',
|
|
574
|
+
source=getattr(t, 'source', 'local'),
|
|
575
|
+
))
|
|
576
|
+
|
|
577
|
+
return recipes
|
|
578
|
+
|
|
579
|
+
except Exception:
|
|
580
|
+
return []
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def describe(name: str, offline: bool = False) -> Optional[RecipeConfig]:
|
|
584
|
+
"""
|
|
585
|
+
Get detailed information about a recipe.
|
|
586
|
+
|
|
587
|
+
Args:
|
|
588
|
+
name: Recipe name or URI
|
|
589
|
+
offline: Use only cached templates
|
|
590
|
+
|
|
591
|
+
Returns:
|
|
592
|
+
RecipeConfig with full recipe details, or None if not found
|
|
593
|
+
|
|
594
|
+
Example:
|
|
595
|
+
>>> info = recipe.describe("support-reply")
|
|
596
|
+
>>> print(f"Required env vars: {info.get_required_env()}")
|
|
597
|
+
"""
|
|
598
|
+
return _load_recipe(name, offline=offline)
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
# --- Internal Functions ---
|
|
602
|
+
|
|
603
|
+
def _load_recipe(name: str, offline: bool = False) -> Optional[RecipeConfig]:
|
|
604
|
+
"""Load a recipe by name or URI."""
|
|
605
|
+
try:
|
|
606
|
+
from praisonai.templates import TemplateDiscovery, TemplateLoader
|
|
607
|
+
|
|
608
|
+
discovery = TemplateDiscovery()
|
|
609
|
+
discovered = discovery.find_template(name)
|
|
610
|
+
|
|
611
|
+
if discovered:
|
|
612
|
+
loader = TemplateLoader(offline=offline)
|
|
613
|
+
template = loader.load(str(discovered.path))
|
|
614
|
+
|
|
615
|
+
return RecipeConfig(
|
|
616
|
+
name=template.name,
|
|
617
|
+
version=template.version,
|
|
618
|
+
description=template.description,
|
|
619
|
+
author=template.author,
|
|
620
|
+
license=template.license,
|
|
621
|
+
tags=template.tags,
|
|
622
|
+
requires=template.requires,
|
|
623
|
+
tools=template.raw.get("tools", {}),
|
|
624
|
+
config_schema=template.config_schema,
|
|
625
|
+
defaults=template.defaults,
|
|
626
|
+
outputs=template.raw.get("outputs", []),
|
|
627
|
+
governance=template.raw.get("governance", {}),
|
|
628
|
+
data_policy=template.raw.get("data_policy", {}),
|
|
629
|
+
path=str(template.path) if template.path else None,
|
|
630
|
+
raw=template.raw,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Try loading directly as URI
|
|
634
|
+
loader = TemplateLoader(offline=offline)
|
|
635
|
+
try:
|
|
636
|
+
template = loader.load(name)
|
|
637
|
+
return RecipeConfig(
|
|
638
|
+
name=template.name,
|
|
639
|
+
version=template.version,
|
|
640
|
+
description=template.description,
|
|
641
|
+
author=template.author,
|
|
642
|
+
license=template.license,
|
|
643
|
+
tags=template.tags,
|
|
644
|
+
requires=template.requires,
|
|
645
|
+
tools=template.raw.get("tools", {}),
|
|
646
|
+
config_schema=template.config_schema,
|
|
647
|
+
defaults=template.defaults,
|
|
648
|
+
outputs=template.raw.get("outputs", []),
|
|
649
|
+
governance=template.raw.get("governance", {}),
|
|
650
|
+
data_policy=template.raw.get("data_policy", {}),
|
|
651
|
+
path=str(template.path) if template.path else None,
|
|
652
|
+
raw=template.raw,
|
|
653
|
+
)
|
|
654
|
+
except Exception:
|
|
655
|
+
pass
|
|
656
|
+
|
|
657
|
+
return None
|
|
658
|
+
|
|
659
|
+
except Exception:
|
|
660
|
+
return None
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _check_dependencies(recipe_config: RecipeConfig) -> Dict[str, Any]:
|
|
664
|
+
"""Check if recipe dependencies are satisfied."""
|
|
665
|
+
result = {
|
|
666
|
+
"all_satisfied": True,
|
|
667
|
+
"packages": [],
|
|
668
|
+
"env": [],
|
|
669
|
+
"tools": [],
|
|
670
|
+
"external": [],
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
# Check Python packages
|
|
674
|
+
for pkg in recipe_config.get_required_packages():
|
|
675
|
+
try:
|
|
676
|
+
__import__(pkg.replace("-", "_"))
|
|
677
|
+
result["packages"].append({"name": pkg, "available": True})
|
|
678
|
+
except ImportError:
|
|
679
|
+
result["packages"].append({"name": pkg, "available": False})
|
|
680
|
+
result["all_satisfied"] = False
|
|
681
|
+
|
|
682
|
+
# Check environment variables
|
|
683
|
+
for env_var in recipe_config.get_required_env():
|
|
684
|
+
available = env_var in os.environ
|
|
685
|
+
result["env"].append({"name": env_var, "available": available})
|
|
686
|
+
if not available:
|
|
687
|
+
result["all_satisfied"] = False
|
|
688
|
+
|
|
689
|
+
# Check external tools
|
|
690
|
+
import shutil
|
|
691
|
+
for ext in recipe_config.get_external_deps():
|
|
692
|
+
ext_name = ext.get("name", ext) if isinstance(ext, dict) else ext
|
|
693
|
+
available = shutil.which(ext_name) is not None
|
|
694
|
+
result["external"].append({"name": ext_name, "available": available})
|
|
695
|
+
# External tools are warnings, not errors
|
|
696
|
+
|
|
697
|
+
return result
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _format_missing_deps(dep_result: Dict[str, Any]) -> List[str]:
|
|
701
|
+
"""Format missing dependencies as a list of strings."""
|
|
702
|
+
missing = []
|
|
703
|
+
|
|
704
|
+
for pkg in dep_result.get("packages", []):
|
|
705
|
+
if not pkg.get("available", False):
|
|
706
|
+
missing.append(pkg["name"])
|
|
707
|
+
|
|
708
|
+
for env in dep_result.get("env", []):
|
|
709
|
+
if not env.get("available", False):
|
|
710
|
+
missing.append(f"${env['name']}")
|
|
711
|
+
|
|
712
|
+
return missing
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _check_tool_policy(recipe_config: RecipeConfig) -> Optional[str]:
|
|
716
|
+
"""Check if recipe uses denied tools. Returns error message or None."""
|
|
717
|
+
allowed = set(recipe_config.get_allowed_tools())
|
|
718
|
+
denied = set(recipe_config.get_denied_tools())
|
|
719
|
+
required = set(recipe_config.get_required_tools())
|
|
720
|
+
|
|
721
|
+
# Check if any required tools are in default denied list
|
|
722
|
+
for tool in required:
|
|
723
|
+
if tool in DEFAULT_DENIED_TOOLS and tool not in allowed:
|
|
724
|
+
return f"Tool '{tool}' is denied by default. Use allow_dangerous_tools=True to override."
|
|
725
|
+
|
|
726
|
+
# Check explicit denials
|
|
727
|
+
for tool in required:
|
|
728
|
+
if tool in denied:
|
|
729
|
+
return f"Tool '{tool}' is explicitly denied by recipe policy."
|
|
730
|
+
|
|
731
|
+
return None
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _execute_recipe(
|
|
735
|
+
recipe_config: RecipeConfig,
|
|
736
|
+
merged_config: Dict[str, Any],
|
|
737
|
+
session_id: str,
|
|
738
|
+
options: Dict[str, Any],
|
|
739
|
+
) -> Any:
|
|
740
|
+
"""Execute the recipe workflow."""
|
|
741
|
+
try:
|
|
742
|
+
from praisonai.templates import TemplateLoader
|
|
743
|
+
from praisonai.templates.tool_override import create_tool_registry_with_overrides
|
|
744
|
+
|
|
745
|
+
loader = TemplateLoader()
|
|
746
|
+
|
|
747
|
+
# Load workflow config
|
|
748
|
+
from praisonai.templates.loader import TemplateConfig as LoaderTemplateConfig
|
|
749
|
+
|
|
750
|
+
# Create a TemplateConfig compatible with loader
|
|
751
|
+
template_path = Path(recipe_config.path) if recipe_config.path else None
|
|
752
|
+
|
|
753
|
+
loader_config = LoaderTemplateConfig(
|
|
754
|
+
name=recipe_config.name,
|
|
755
|
+
description=recipe_config.description,
|
|
756
|
+
version=recipe_config.version,
|
|
757
|
+
author=recipe_config.author,
|
|
758
|
+
license=recipe_config.license,
|
|
759
|
+
tags=recipe_config.tags,
|
|
760
|
+
requires=recipe_config.requires,
|
|
761
|
+
workflow_file=recipe_config.raw.get("workflow", "workflow.yaml"),
|
|
762
|
+
agents_file=recipe_config.raw.get("agents", "agents.yaml"),
|
|
763
|
+
config_schema=recipe_config.config_schema,
|
|
764
|
+
defaults=merged_config,
|
|
765
|
+
skills=recipe_config.raw.get("skills", []),
|
|
766
|
+
cli=recipe_config.raw.get("cli", {}),
|
|
767
|
+
raw=recipe_config.raw,
|
|
768
|
+
path=template_path,
|
|
769
|
+
)
|
|
770
|
+
|
|
771
|
+
workflow_config = loader.load_workflow_config(loader_config)
|
|
772
|
+
|
|
773
|
+
# Build tool registry
|
|
774
|
+
tool_registry = create_tool_registry_with_overrides(
|
|
775
|
+
include_defaults=True,
|
|
776
|
+
template_dir=recipe_config.path,
|
|
777
|
+
)
|
|
778
|
+
|
|
779
|
+
# Execute based on workflow type
|
|
780
|
+
if "agents" in workflow_config and "tasks" in workflow_config:
|
|
781
|
+
return _execute_praisonai_workflow(
|
|
782
|
+
workflow_config, merged_config, tool_registry, options
|
|
783
|
+
)
|
|
784
|
+
elif "steps" in workflow_config:
|
|
785
|
+
return _execute_steps_workflow(
|
|
786
|
+
workflow_config, merged_config, tool_registry, options
|
|
787
|
+
)
|
|
788
|
+
else:
|
|
789
|
+
# Simple agent execution
|
|
790
|
+
return _execute_simple_agent(
|
|
791
|
+
workflow_config, merged_config, tool_registry, options
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
except ImportError as e:
|
|
795
|
+
raise RecipeError(f"Missing dependency for recipe execution: {e}")
|
|
796
|
+
except Exception as e:
|
|
797
|
+
raise RecipeError(f"Recipe execution failed: {e}")
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def _execute_praisonai_workflow(
|
|
801
|
+
workflow_config: Dict[str, Any],
|
|
802
|
+
config: Dict[str, Any],
|
|
803
|
+
tool_registry: Any,
|
|
804
|
+
options: Dict[str, Any],
|
|
805
|
+
) -> Any:
|
|
806
|
+
"""Execute a PraisonAI agents/tasks workflow."""
|
|
807
|
+
from praisonaiagents import Agent, Task, PraisonAIAgents
|
|
808
|
+
from praisonai.templates.tool_override import resolve_tools
|
|
809
|
+
|
|
810
|
+
agents = []
|
|
811
|
+
agent_map = {}
|
|
812
|
+
|
|
813
|
+
for agent_cfg in workflow_config.get("agents", []):
|
|
814
|
+
agent_tools = resolve_tools(
|
|
815
|
+
agent_cfg.get("tools", []),
|
|
816
|
+
registry=tool_registry,
|
|
817
|
+
)
|
|
818
|
+
|
|
819
|
+
agent = Agent(
|
|
820
|
+
name=agent_cfg.get("name", "Agent"),
|
|
821
|
+
role=agent_cfg.get("role", ""),
|
|
822
|
+
goal=agent_cfg.get("goal", ""),
|
|
823
|
+
backstory=agent_cfg.get("backstory", ""),
|
|
824
|
+
tools=agent_tools if agent_tools else None,
|
|
825
|
+
llm=agent_cfg.get("llm"),
|
|
826
|
+
verbose=options.get("verbose", False),
|
|
827
|
+
)
|
|
828
|
+
agents.append(agent)
|
|
829
|
+
agent_map[agent_cfg.get("name")] = agent
|
|
830
|
+
|
|
831
|
+
tasks = []
|
|
832
|
+
for task_cfg in workflow_config.get("tasks", []):
|
|
833
|
+
agent_name = task_cfg.get("agent")
|
|
834
|
+
agent = agent_map.get(agent_name, agents[0] if agents else None)
|
|
835
|
+
|
|
836
|
+
# Substitute config values in description
|
|
837
|
+
description = task_cfg.get("description", "")
|
|
838
|
+
try:
|
|
839
|
+
description = description.format(**config)
|
|
840
|
+
except KeyError:
|
|
841
|
+
pass
|
|
842
|
+
|
|
843
|
+
task = Task(
|
|
844
|
+
name=task_cfg.get("name", "Task"),
|
|
845
|
+
description=description,
|
|
846
|
+
expected_output=task_cfg.get("expected_output", ""),
|
|
847
|
+
agent=agent,
|
|
848
|
+
)
|
|
849
|
+
tasks.append(task)
|
|
850
|
+
|
|
851
|
+
praison = PraisonAIAgents(
|
|
852
|
+
agents=agents,
|
|
853
|
+
tasks=tasks,
|
|
854
|
+
process=workflow_config.get("process", "sequential"),
|
|
855
|
+
verbose=options.get("verbose", 1) if options.get("verbose") else 0,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
return praison.start()
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _execute_steps_workflow(
|
|
862
|
+
workflow_config: Dict[str, Any],
|
|
863
|
+
config: Dict[str, Any],
|
|
864
|
+
tool_registry: Any,
|
|
865
|
+
options: Dict[str, Any],
|
|
866
|
+
) -> Any:
|
|
867
|
+
"""Execute a steps-based workflow."""
|
|
868
|
+
# For now, convert to simple execution
|
|
869
|
+
return {"message": "Steps workflow executed", "config": config}
|
|
870
|
+
|
|
871
|
+
|
|
872
|
+
def _execute_simple_agent(
|
|
873
|
+
workflow_config: Dict[str, Any],
|
|
874
|
+
config: Dict[str, Any],
|
|
875
|
+
tool_registry: Any,
|
|
876
|
+
options: Dict[str, Any],
|
|
877
|
+
) -> Any:
|
|
878
|
+
"""Execute a simple single-agent workflow."""
|
|
879
|
+
from praisonaiagents import Agent
|
|
880
|
+
|
|
881
|
+
agent = Agent(
|
|
882
|
+
name=workflow_config.get("name", "RecipeAgent"),
|
|
883
|
+
role=workflow_config.get("role", "AI Assistant"),
|
|
884
|
+
goal=workflow_config.get("goal", "Complete the task"),
|
|
885
|
+
backstory=workflow_config.get("backstory", ""),
|
|
886
|
+
verbose=options.get("verbose", False),
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
prompt = config.get("input", config.get("prompt", ""))
|
|
890
|
+
if prompt:
|
|
891
|
+
return agent.chat(prompt)
|
|
892
|
+
|
|
893
|
+
return {"message": "Recipe executed", "config": config}
|