kubiya-control-plane-api 0.9.15__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.
- control_plane_api/LICENSE +676 -0
- control_plane_api/README.md +350 -0
- control_plane_api/__init__.py +4 -0
- control_plane_api/__version__.py +8 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +121 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/2613c65c3dbe_initial_database_setup.py +32 -0
- control_plane_api/alembic/versions/2df520d4927d_merge_heads.py +28 -0
- control_plane_api/alembic/versions/43abf98d6a01_add_paused_status_to_executions.py +73 -0
- control_plane_api/alembic/versions/6289854264cb_merge_multiple_heads.py +28 -0
- control_plane_api/alembic/versions/6a4d4dc3d8dc_generate_execution_transitions.py +50 -0
- control_plane_api/alembic/versions/87d11cf0a783_add_disconnected_status_to_worker_.py +44 -0
- control_plane_api/alembic/versions/add_ephemeral_queue_support.py +85 -0
- control_plane_api/alembic/versions/add_model_type_to_llm_models.py +31 -0
- control_plane_api/alembic/versions/add_plan_executions_table.py +114 -0
- control_plane_api/alembic/versions/add_trace_span_tables.py +154 -0
- control_plane_api/alembic/versions/add_user_info_to_traces.py +36 -0
- control_plane_api/alembic/versions/adjusting_foreign_keys.py +32 -0
- control_plane_api/alembic/versions/b4983d976db2_initial_tables.py +1128 -0
- control_plane_api/alembic/versions/d181a3b40e71_rename_custom_metadata_to_metadata_in_.py +50 -0
- control_plane_api/alembic/versions/df9117888e82_add_missing_columns.py +82 -0
- control_plane_api/alembic/versions/f25de6ad895a_missing_migrations.py +34 -0
- control_plane_api/alembic/versions/f71305fb69b9_fix_ephemeral_queue_deletion_foreign_key.py +54 -0
- control_plane_api/alembic/versions/mark_local_exec_queues_as_ephemeral.py +68 -0
- control_plane_api/alembic.ini +148 -0
- control_plane_api/api/index.py +12 -0
- control_plane_api/app/__init__.py +11 -0
- control_plane_api/app/activities/__init__.py +20 -0
- control_plane_api/app/activities/agent_activities.py +384 -0
- control_plane_api/app/activities/plan_generation_activities.py +499 -0
- control_plane_api/app/activities/team_activities.py +424 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +588 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +469 -0
- control_plane_api/app/config/config_loader.py +224 -0
- control_plane_api/app/config/model_pricing.py +323 -0
- control_plane_api/app/config/storage_config.py +159 -0
- control_plane_api/app/config.py +115 -0
- control_plane_api/app/controllers/__init__.py +0 -0
- control_plane_api/app/controllers/execution_environment_controller.py +1315 -0
- control_plane_api/app/database.py +135 -0
- control_plane_api/app/exceptions.py +408 -0
- control_plane_api/app/lib/__init__.py +11 -0
- control_plane_api/app/lib/environment.py +65 -0
- control_plane_api/app/lib/event_bus/__init__.py +17 -0
- control_plane_api/app/lib/event_bus/base.py +136 -0
- control_plane_api/app/lib/event_bus/manager.py +335 -0
- control_plane_api/app/lib/event_bus/providers/__init__.py +6 -0
- control_plane_api/app/lib/event_bus/providers/http_provider.py +166 -0
- control_plane_api/app/lib/event_bus/providers/nats_provider.py +324 -0
- control_plane_api/app/lib/event_bus/providers/redis_provider.py +233 -0
- control_plane_api/app/lib/event_bus/providers/websocket_provider.py +497 -0
- control_plane_api/app/lib/job_executor.py +330 -0
- control_plane_api/app/lib/kubiya_client.py +293 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/mcp_validation.py +163 -0
- control_plane_api/app/lib/nats/__init__.py +13 -0
- control_plane_api/app/lib/nats/credentials_manager.py +288 -0
- control_plane_api/app/lib/nats/listener.py +374 -0
- control_plane_api/app/lib/planning_prompt_builder.py +153 -0
- control_plane_api/app/lib/planning_tools/__init__.py +41 -0
- control_plane_api/app/lib/planning_tools/agents.py +409 -0
- control_plane_api/app/lib/planning_tools/agno_toolkit.py +836 -0
- control_plane_api/app/lib/planning_tools/base.py +119 -0
- control_plane_api/app/lib/planning_tools/cognitive_memory_tools.py +403 -0
- control_plane_api/app/lib/planning_tools/context_graph_tools.py +545 -0
- control_plane_api/app/lib/planning_tools/environments.py +218 -0
- control_plane_api/app/lib/planning_tools/knowledge.py +204 -0
- control_plane_api/app/lib/planning_tools/models.py +93 -0
- control_plane_api/app/lib/planning_tools/planning_service.py +646 -0
- control_plane_api/app/lib/planning_tools/resources.py +242 -0
- control_plane_api/app/lib/planning_tools/teams.py +334 -0
- control_plane_api/app/lib/policy_enforcer_client.py +1016 -0
- control_plane_api/app/lib/redis_client.py +803 -0
- control_plane_api/app/lib/sqlalchemy_utils.py +486 -0
- control_plane_api/app/lib/state_transition_tools/__init__.py +7 -0
- control_plane_api/app/lib/state_transition_tools/execution_context.py +388 -0
- control_plane_api/app/lib/storage/__init__.py +20 -0
- control_plane_api/app/lib/storage/base_provider.py +274 -0
- control_plane_api/app/lib/storage/provider_factory.py +157 -0
- control_plane_api/app/lib/storage/vercel_blob_provider.py +468 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/supabase_utils.py +138 -0
- control_plane_api/app/lib/task_planning/__init__.py +138 -0
- control_plane_api/app/lib/task_planning/agent_factory.py +308 -0
- control_plane_api/app/lib/task_planning/agents.py +389 -0
- control_plane_api/app/lib/task_planning/cache.py +218 -0
- control_plane_api/app/lib/task_planning/entity_resolver.py +273 -0
- control_plane_api/app/lib/task_planning/helpers.py +293 -0
- control_plane_api/app/lib/task_planning/hooks.py +474 -0
- control_plane_api/app/lib/task_planning/models.py +503 -0
- control_plane_api/app/lib/task_planning/plan_validator.py +166 -0
- control_plane_api/app/lib/task_planning/planning_workflow.py +2911 -0
- control_plane_api/app/lib/task_planning/runner.py +656 -0
- control_plane_api/app/lib/task_planning/streaming_hook.py +213 -0
- control_plane_api/app/lib/task_planning/workflow.py +424 -0
- control_plane_api/app/lib/templating/__init__.py +88 -0
- control_plane_api/app/lib/templating/compiler.py +278 -0
- control_plane_api/app/lib/templating/engine.py +178 -0
- control_plane_api/app/lib/templating/parsers/__init__.py +29 -0
- control_plane_api/app/lib/templating/parsers/base.py +96 -0
- control_plane_api/app/lib/templating/parsers/env.py +85 -0
- control_plane_api/app/lib/templating/parsers/graph.py +112 -0
- control_plane_api/app/lib/templating/parsers/secret.py +87 -0
- control_plane_api/app/lib/templating/parsers/simple.py +81 -0
- control_plane_api/app/lib/templating/resolver.py +366 -0
- control_plane_api/app/lib/templating/types.py +214 -0
- control_plane_api/app/lib/templating/validator.py +201 -0
- control_plane_api/app/lib/temporal_client.py +232 -0
- control_plane_api/app/lib/temporal_credentials_cache.py +178 -0
- control_plane_api/app/lib/temporal_credentials_service.py +203 -0
- control_plane_api/app/lib/validation/__init__.py +24 -0
- control_plane_api/app/lib/validation/runtime_validation.py +388 -0
- control_plane_api/app/main.py +531 -0
- control_plane_api/app/middleware/__init__.py +10 -0
- control_plane_api/app/middleware/auth.py +645 -0
- control_plane_api/app/middleware/exception_handler.py +267 -0
- control_plane_api/app/middleware/prometheus_middleware.py +173 -0
- control_plane_api/app/middleware/rate_limiting.py +384 -0
- control_plane_api/app/middleware/request_id.py +202 -0
- control_plane_api/app/models/__init__.py +40 -0
- control_plane_api/app/models/agent.py +90 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +107 -0
- control_plane_api/app/models/auth_user.py +73 -0
- control_plane_api/app/models/context.py +161 -0
- control_plane_api/app/models/custom_integration.py +99 -0
- control_plane_api/app/models/environment.py +64 -0
- control_plane_api/app/models/execution.py +125 -0
- control_plane_api/app/models/execution_transition.py +50 -0
- control_plane_api/app/models/job.py +159 -0
- control_plane_api/app/models/llm_model.py +78 -0
- control_plane_api/app/models/orchestration.py +66 -0
- control_plane_api/app/models/plan_execution.py +102 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +61 -0
- control_plane_api/app/models/project_management.py +85 -0
- control_plane_api/app/models/session.py +29 -0
- control_plane_api/app/models/skill.py +155 -0
- control_plane_api/app/models/system_tables.py +43 -0
- control_plane_api/app/models/task_planning.py +372 -0
- control_plane_api/app/models/team.py +86 -0
- control_plane_api/app/models/trace.py +257 -0
- control_plane_api/app/models/user_profile.py +54 -0
- control_plane_api/app/models/worker.py +221 -0
- control_plane_api/app/models/workflow.py +161 -0
- control_plane_api/app/models/workspace.py +50 -0
- control_plane_api/app/observability/__init__.py +177 -0
- control_plane_api/app/observability/context_logging.py +475 -0
- control_plane_api/app/observability/decorators.py +337 -0
- control_plane_api/app/observability/local_span_processor.py +702 -0
- control_plane_api/app/observability/metrics.py +303 -0
- control_plane_api/app/observability/middleware.py +246 -0
- control_plane_api/app/observability/optional.py +115 -0
- control_plane_api/app/observability/tracing.py +382 -0
- control_plane_api/app/policies/README.md +149 -0
- control_plane_api/app/policies/approved_users.rego +62 -0
- control_plane_api/app/policies/business_hours.rego +51 -0
- control_plane_api/app/policies/rate_limiting.rego +100 -0
- control_plane_api/app/policies/tool_enforcement/README.md +336 -0
- control_plane_api/app/policies/tool_enforcement/bash_command_validation.rego +71 -0
- control_plane_api/app/policies/tool_enforcement/business_hours_enforcement.rego +82 -0
- control_plane_api/app/policies/tool_enforcement/mcp_tool_allowlist.rego +58 -0
- control_plane_api/app/policies/tool_enforcement/production_safeguards.rego +80 -0
- control_plane_api/app/policies/tool_enforcement/role_based_tool_access.rego +44 -0
- control_plane_api/app/policies/tool_restrictions.rego +86 -0
- control_plane_api/app/routers/__init__.py +4 -0
- control_plane_api/app/routers/agents.py +382 -0
- control_plane_api/app/routers/agents_v2.py +1598 -0
- control_plane_api/app/routers/analytics.py +1310 -0
- control_plane_api/app/routers/auth.py +59 -0
- control_plane_api/app/routers/client_config.py +57 -0
- control_plane_api/app/routers/context_graph.py +561 -0
- control_plane_api/app/routers/context_manager.py +577 -0
- control_plane_api/app/routers/custom_integrations.py +490 -0
- control_plane_api/app/routers/enforcer.py +132 -0
- control_plane_api/app/routers/environment_context.py +252 -0
- control_plane_api/app/routers/environments.py +761 -0
- control_plane_api/app/routers/execution_environment.py +847 -0
- control_plane_api/app/routers/executions/__init__.py +28 -0
- control_plane_api/app/routers/executions/router.py +286 -0
- control_plane_api/app/routers/executions/services/__init__.py +22 -0
- control_plane_api/app/routers/executions/services/demo_worker_health.py +156 -0
- control_plane_api/app/routers/executions/services/status_service.py +420 -0
- control_plane_api/app/routers/executions/services/test_worker_health.py +480 -0
- control_plane_api/app/routers/executions/services/worker_health.py +514 -0
- control_plane_api/app/routers/executions/streaming/__init__.py +22 -0
- control_plane_api/app/routers/executions/streaming/deduplication.py +352 -0
- control_plane_api/app/routers/executions/streaming/event_buffer.py +353 -0
- control_plane_api/app/routers/executions/streaming/event_formatter.py +964 -0
- control_plane_api/app/routers/executions/streaming/history_loader.py +588 -0
- control_plane_api/app/routers/executions/streaming/live_source.py +693 -0
- control_plane_api/app/routers/executions/streaming/streamer.py +849 -0
- control_plane_api/app/routers/executions.py +4888 -0
- control_plane_api/app/routers/health.py +165 -0
- control_plane_api/app/routers/health_v2.py +394 -0
- control_plane_api/app/routers/integration_templates.py +496 -0
- control_plane_api/app/routers/integrations.py +287 -0
- control_plane_api/app/routers/jobs.py +1809 -0
- control_plane_api/app/routers/metrics.py +517 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +628 -0
- control_plane_api/app/routers/plan_executions.py +1481 -0
- control_plane_api/app/routers/plan_generation_async.py +304 -0
- control_plane_api/app/routers/policies.py +669 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +987 -0
- control_plane_api/app/routers/runners.py +379 -0
- control_plane_api/app/routers/runtimes.py +172 -0
- control_plane_api/app/routers/secrets.py +171 -0
- control_plane_api/app/routers/skills.py +1010 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/storage.py +456 -0
- control_plane_api/app/routers/task_planning.py +611 -0
- control_plane_api/app/routers/task_queues.py +650 -0
- control_plane_api/app/routers/team_context.py +274 -0
- control_plane_api/app/routers/teams.py +1747 -0
- control_plane_api/app/routers/templates.py +248 -0
- control_plane_api/app/routers/traces.py +571 -0
- control_plane_api/app/routers/websocket_client.py +479 -0
- control_plane_api/app/routers/websocket_executions_status.py +437 -0
- control_plane_api/app/routers/websocket_gateway.py +323 -0
- control_plane_api/app/routers/websocket_traces.py +576 -0
- control_plane_api/app/routers/worker_queues.py +2555 -0
- control_plane_api/app/routers/worker_websocket.py +419 -0
- control_plane_api/app/routers/workers.py +1004 -0
- control_plane_api/app/routers/workflows.py +204 -0
- control_plane_api/app/runtimes/__init__.py +6 -0
- control_plane_api/app/runtimes/validation.py +344 -0
- control_plane_api/app/schemas/__init__.py +1 -0
- control_plane_api/app/schemas/job_schemas.py +302 -0
- control_plane_api/app/schemas/mcp_schemas.py +311 -0
- control_plane_api/app/schemas/template_schemas.py +133 -0
- control_plane_api/app/schemas/trace_schemas.py +168 -0
- control_plane_api/app/schemas/worker_queue_observability_schemas.py +165 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_planning_strategy.py +233 -0
- control_plane_api/app/services/agno_service.py +838 -0
- control_plane_api/app/services/claude_code_planning_service.py +203 -0
- control_plane_api/app/services/context_graph_client.py +224 -0
- control_plane_api/app/services/custom_integration_service.py +415 -0
- control_plane_api/app/services/integration_resolution_service.py +345 -0
- control_plane_api/app/services/litellm_service.py +394 -0
- control_plane_api/app/services/plan_generator.py +79 -0
- control_plane_api/app/services/planning_strategy.py +66 -0
- control_plane_api/app/services/planning_strategy_factory.py +118 -0
- control_plane_api/app/services/policy_service.py +615 -0
- control_plane_api/app/services/state_transition_service.py +755 -0
- control_plane_api/app/services/storage_service.py +593 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/services/toolsets/context_graph_skill.py +432 -0
- control_plane_api/app/services/trace_retention.py +354 -0
- control_plane_api/app/services/worker_queue_metrics_service.py +190 -0
- control_plane_api/app/services/workflow_cancellation_manager.py +135 -0
- control_plane_api/app/services/workflow_operations_service.py +611 -0
- control_plane_api/app/skills/__init__.py +100 -0
- control_plane_api/app/skills/base.py +239 -0
- control_plane_api/app/skills/builtin/__init__.py +37 -0
- control_plane_api/app/skills/builtin/agent_communication/__init__.py +8 -0
- control_plane_api/app/skills/builtin/agent_communication/skill.py +246 -0
- control_plane_api/app/skills/builtin/code_ingestion/__init__.py +4 -0
- control_plane_api/app/skills/builtin/code_ingestion/skill.py +267 -0
- control_plane_api/app/skills/builtin/cognitive_memory/__init__.py +4 -0
- control_plane_api/app/skills/builtin/cognitive_memory/skill.py +174 -0
- control_plane_api/app/skills/builtin/contextual_awareness/__init__.py +4 -0
- control_plane_api/app/skills/builtin/contextual_awareness/skill.py +387 -0
- control_plane_api/app/skills/builtin/data_visualization/__init__.py +4 -0
- control_plane_api/app/skills/builtin/data_visualization/skill.py +154 -0
- control_plane_api/app/skills/builtin/docker/__init__.py +4 -0
- control_plane_api/app/skills/builtin/docker/skill.py +104 -0
- control_plane_api/app/skills/builtin/file_generation/__init__.py +4 -0
- control_plane_api/app/skills/builtin/file_generation/skill.py +94 -0
- control_plane_api/app/skills/builtin/file_system/__init__.py +4 -0
- control_plane_api/app/skills/builtin/file_system/skill.py +110 -0
- control_plane_api/app/skills/builtin/knowledge_api/__init__.py +5 -0
- control_plane_api/app/skills/builtin/knowledge_api/skill.py +124 -0
- control_plane_api/app/skills/builtin/python/__init__.py +4 -0
- control_plane_api/app/skills/builtin/python/skill.py +92 -0
- control_plane_api/app/skills/builtin/remote_filesystem/__init__.py +5 -0
- control_plane_api/app/skills/builtin/remote_filesystem/skill.py +170 -0
- control_plane_api/app/skills/builtin/shell/__init__.py +4 -0
- control_plane_api/app/skills/builtin/shell/skill.py +161 -0
- control_plane_api/app/skills/builtin/slack/__init__.py +3 -0
- control_plane_api/app/skills/builtin/slack/skill.py +302 -0
- control_plane_api/app/skills/builtin/workflow_executor/__init__.py +4 -0
- control_plane_api/app/skills/builtin/workflow_executor/skill.py +469 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/config.py +63 -0
- control_plane_api/app/skills/loaders/__init__.py +14 -0
- control_plane_api/app/skills/loaders/base.py +73 -0
- control_plane_api/app/skills/loaders/filesystem_loader.py +199 -0
- control_plane_api/app/skills/registry.py +125 -0
- control_plane_api/app/utils/helpers.py +12 -0
- control_plane_api/app/utils/workflow_executor.py +354 -0
- control_plane_api/app/workflows/__init__.py +11 -0
- control_plane_api/app/workflows/agent_execution.py +520 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +223 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/plan_generation.py +254 -0
- control_plane_api/app/workflows/team_execution.py +442 -0
- control_plane_api/scripts/seed_models.py +240 -0
- control_plane_api/scripts/validate_existing_tool_names.py +492 -0
- control_plane_api/shared/__init__.py +8 -0
- control_plane_api/shared/version.py +17 -0
- control_plane_api/test_deduplication.py +274 -0
- control_plane_api/test_executor_deduplication_e2e.py +309 -0
- control_plane_api/test_job_execution_e2e.py +283 -0
- control_plane_api/test_real_integration.py +193 -0
- control_plane_api/version.py +38 -0
- control_plane_api/worker/__init__.py +0 -0
- control_plane_api/worker/activities/__init__.py +0 -0
- control_plane_api/worker/activities/agent_activities.py +1585 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/job_activities.py +199 -0
- control_plane_api/worker/activities/runtime_activities.py +1167 -0
- control_plane_api/worker/activities/skill_activities.py +282 -0
- control_plane_api/worker/activities/team_activities.py +479 -0
- control_plane_api/worker/agent_runtime_server.py +370 -0
- control_plane_api/worker/binary_manager.py +333 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +273 -0
- control_plane_api/worker/control_plane_client.py +1491 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/health_monitor.py +159 -0
- control_plane_api/worker/metrics.py +237 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/error_events.py +105 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +35 -0
- control_plane_api/worker/runtimes/agent_runtime/runtime.py +485 -0
- control_plane_api/worker/runtimes/agno/__init__.py +34 -0
- control_plane_api/worker/runtimes/agno/config.py +248 -0
- control_plane_api/worker/runtimes/agno/hooks.py +385 -0
- control_plane_api/worker/runtimes/agno/mcp_builder.py +195 -0
- control_plane_api/worker/runtimes/agno/runtime.py +1063 -0
- control_plane_api/worker/runtimes/agno/utils.py +163 -0
- control_plane_api/worker/runtimes/base.py +979 -0
- control_plane_api/worker/runtimes/claude_code/__init__.py +38 -0
- control_plane_api/worker/runtimes/claude_code/cleanup.py +184 -0
- control_plane_api/worker/runtimes/claude_code/client_pool.py +529 -0
- control_plane_api/worker/runtimes/claude_code/config.py +829 -0
- control_plane_api/worker/runtimes/claude_code/hooks.py +482 -0
- control_plane_api/worker/runtimes/claude_code/litellm_proxy.py +1702 -0
- control_plane_api/worker/runtimes/claude_code/mcp_builder.py +467 -0
- control_plane_api/worker/runtimes/claude_code/mcp_discovery.py +558 -0
- control_plane_api/worker/runtimes/claude_code/runtime.py +1546 -0
- control_plane_api/worker/runtimes/claude_code/tool_mapper.py +403 -0
- control_plane_api/worker/runtimes/claude_code/utils.py +149 -0
- control_plane_api/worker/runtimes/factory.py +173 -0
- control_plane_api/worker/runtimes/model_utils.py +107 -0
- control_plane_api/worker/runtimes/validation.py +93 -0
- control_plane_api/worker/services/__init__.py +1 -0
- control_plane_api/worker/services/agent_communication_tools.py +908 -0
- control_plane_api/worker/services/agent_executor.py +485 -0
- control_plane_api/worker/services/agent_executor_v2.py +793 -0
- control_plane_api/worker/services/analytics_collector.py +457 -0
- control_plane_api/worker/services/analytics_service.py +464 -0
- control_plane_api/worker/services/approval_tools.py +310 -0
- control_plane_api/worker/services/approval_tools_agno.py +207 -0
- control_plane_api/worker/services/cancellation_manager.py +177 -0
- control_plane_api/worker/services/code_ingestion_tools.py +465 -0
- control_plane_api/worker/services/contextual_awareness_tools.py +405 -0
- control_plane_api/worker/services/data_visualization.py +834 -0
- control_plane_api/worker/services/event_publisher.py +531 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/remote_filesystem_tools.py +498 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +365 -0
- control_plane_api/worker/services/skill_context_enhancement.py +181 -0
- control_plane_api/worker/services/skill_factory.py +471 -0
- control_plane_api/worker/services/system_prompt_enhancement.py +410 -0
- control_plane_api/worker/services/team_executor.py +715 -0
- control_plane_api/worker/services/team_executor_v2.py +1866 -0
- control_plane_api/worker/services/tool_enforcement.py +254 -0
- control_plane_api/worker/services/workflow_executor/__init__.py +52 -0
- control_plane_api/worker/services/workflow_executor/event_processor.py +287 -0
- control_plane_api/worker/services/workflow_executor/event_publisher.py +210 -0
- control_plane_api/worker/services/workflow_executor/executors/__init__.py +15 -0
- control_plane_api/worker/services/workflow_executor/executors/base.py +270 -0
- control_plane_api/worker/services/workflow_executor/executors/json_executor.py +50 -0
- control_plane_api/worker/services/workflow_executor/executors/python_executor.py +50 -0
- control_plane_api/worker/services/workflow_executor/models.py +142 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1748 -0
- control_plane_api/worker/skills/__init__.py +12 -0
- control_plane_api/worker/skills/builtin/context_graph_search/README.md +213 -0
- control_plane_api/worker/skills/builtin/context_graph_search/__init__.py +5 -0
- control_plane_api/worker/skills/builtin/context_graph_search/agno_impl.py +808 -0
- control_plane_api/worker/skills/builtin/context_graph_search/skill.yaml +67 -0
- control_plane_api/worker/skills/builtin/contextual_awareness/__init__.py +4 -0
- control_plane_api/worker/skills/builtin/contextual_awareness/agno_impl.py +62 -0
- control_plane_api/worker/skills/builtin/data_visualization/agno_impl.py +18 -0
- control_plane_api/worker/skills/builtin/data_visualization/skill.yaml +84 -0
- control_plane_api/worker/skills/builtin/docker/agno_impl.py +65 -0
- control_plane_api/worker/skills/builtin/docker/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/file_generation/agno_impl.py +47 -0
- control_plane_api/worker/skills/builtin/file_generation/skill.yaml +64 -0
- control_plane_api/worker/skills/builtin/file_system/agno_impl.py +32 -0
- control_plane_api/worker/skills/builtin/file_system/skill.yaml +54 -0
- control_plane_api/worker/skills/builtin/knowledge_api/__init__.py +4 -0
- control_plane_api/worker/skills/builtin/knowledge_api/agno_impl.py +50 -0
- control_plane_api/worker/skills/builtin/knowledge_api/skill.yaml +66 -0
- control_plane_api/worker/skills/builtin/python/agno_impl.py +25 -0
- control_plane_api/worker/skills/builtin/python/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/schema_fix_mixin.py +260 -0
- control_plane_api/worker/skills/builtin/shell/agno_impl.py +31 -0
- control_plane_api/worker/skills/builtin/shell/skill.yaml +60 -0
- control_plane_api/worker/skills/builtin/slack/__init__.py +3 -0
- control_plane_api/worker/skills/builtin/slack/agno_impl.py +1282 -0
- control_plane_api/worker/skills/builtin/slack/skill.yaml +276 -0
- control_plane_api/worker/skills/builtin/workflow_executor/agno_impl.py +62 -0
- control_plane_api/worker/skills/builtin/workflow_executor/skill.yaml +79 -0
- control_plane_api/worker/skills/loaders/__init__.py +5 -0
- control_plane_api/worker/skills/loaders/base.py +23 -0
- control_plane_api/worker/skills/loaders/filesystem_loader.py +357 -0
- control_plane_api/worker/skills/registry.py +208 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/conftest.py +12 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_context_graph_real_api.py +338 -0
- control_plane_api/worker/tests/e2e/test_context_graph_templates_e2e.py +523 -0
- control_plane_api/worker/tests/e2e/test_enforcement_e2e.py +344 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/e2e/test_single_execution_mode.py +656 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_builtin_skills_fixes.py +245 -0
- control_plane_api/worker/tests/integration/test_context_graph_search_integration.py +365 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/integration/test_hook_enforcement_integration.py +579 -0
- control_plane_api/worker/tests/integration/test_scheduled_job_workflow.py +237 -0
- control_plane_api/worker/tests/integration/test_system_prompt_enhancement_integration.py +343 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_builtin_skill_autoload.py +396 -0
- control_plane_api/worker/tests/unit/test_context_graph_search.py +450 -0
- control_plane_api/worker/tests/unit/test_context_graph_templates.py +403 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/tests/unit/test_control_plane_client_jobs.py +345 -0
- control_plane_api/worker/tests/unit/test_job_activities.py +353 -0
- control_plane_api/worker/tests/unit/test_skill_context_enhancement.py +321 -0
- control_plane_api/worker/tests/unit/test_system_prompt_enhancement.py +415 -0
- control_plane_api/worker/tests/unit/test_tool_enforcement.py +324 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +330 -0
- control_plane_api/worker/utils/environment.py +65 -0
- control_plane_api/worker/utils/error_publisher.py +260 -0
- control_plane_api/worker/utils/event_batcher.py +256 -0
- control_plane_api/worker/utils/logging_config.py +335 -0
- control_plane_api/worker/utils/logging_helper.py +326 -0
- control_plane_api/worker/utils/parameter_validator.py +120 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +665 -0
- control_plane_api/worker/utils/tool_validation.py +332 -0
- control_plane_api/worker/utils/workspace_manager.py +163 -0
- control_plane_api/worker/websocket_client.py +393 -0
- control_plane_api/worker/worker.py +1297 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +909 -0
- control_plane_api/worker/workflows/scheduled_job_wrapper.py +332 -0
- control_plane_api/worker/workflows/team_execution.py +611 -0
- kubiya_control_plane_api-0.9.15.dist-info/METADATA +354 -0
- kubiya_control_plane_api-0.9.15.dist-info/RECORD +479 -0
- kubiya_control_plane_api-0.9.15.dist-info/WHEEL +5 -0
- kubiya_control_plane_api-0.9.15.dist-info/entry_points.txt +5 -0
- kubiya_control_plane_api-0.9.15.dist-info/licenses/LICENSE +676 -0
- kubiya_control_plane_api-0.9.15.dist-info/top_level.txt +3 -0
- scripts/__init__.py +1 -0
- scripts/migrations.py +39 -0
- scripts/seed_worker_queues.py +128 -0
- scripts/setup_agent_runtime.py +142 -0
- worker_internal/__init__.py +1 -0
- worker_internal/planner/__init__.py +1 -0
- worker_internal/planner/activities.py +1499 -0
- worker_internal/planner/agent_tools.py +197 -0
- worker_internal/planner/event_models.py +148 -0
- worker_internal/planner/event_publisher.py +67 -0
- worker_internal/planner/models.py +199 -0
- worker_internal/planner/retry_logic.py +134 -0
- worker_internal/planner/worker.py +300 -0
- worker_internal/planner/workflows.py +970 -0
|
@@ -0,0 +1,1063 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Agno runtime implementation.
|
|
3
|
+
|
|
4
|
+
This is the main runtime class that provides Agno framework integration
|
|
5
|
+
for the Agent Control Plane.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import queue
|
|
10
|
+
import threading
|
|
11
|
+
import time
|
|
12
|
+
import structlog
|
|
13
|
+
from typing import Dict, Any, Optional, AsyncIterator, Callable, TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from ..base import (
|
|
16
|
+
RuntimeType,
|
|
17
|
+
RuntimeExecutionResult,
|
|
18
|
+
RuntimeExecutionContext,
|
|
19
|
+
RuntimeCapabilities,
|
|
20
|
+
BaseRuntime,
|
|
21
|
+
RuntimeRegistry,
|
|
22
|
+
)
|
|
23
|
+
from control_plane_api.worker.services.event_publisher import (
|
|
24
|
+
EventPublisher,
|
|
25
|
+
EventPublisherConfig,
|
|
26
|
+
EventPriority,
|
|
27
|
+
)
|
|
28
|
+
from control_plane_api.worker.utils.tool_validation import (
|
|
29
|
+
validate_and_sanitize_tools,
|
|
30
|
+
sanitize_tool_name,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
from .config import build_agno_agent_config
|
|
34
|
+
from .hooks import create_tool_hook_for_streaming, create_tool_hook_with_callback
|
|
35
|
+
from .mcp_builder import build_agno_mcp_tools
|
|
36
|
+
from .utils import (
|
|
37
|
+
build_conversation_messages,
|
|
38
|
+
extract_usage,
|
|
39
|
+
extract_tool_messages,
|
|
40
|
+
extract_response_content,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from control_plane_client import ControlPlaneClient
|
|
45
|
+
from services.cancellation_manager import CancellationManager
|
|
46
|
+
|
|
47
|
+
logger = structlog.get_logger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@RuntimeRegistry.register(RuntimeType.DEFAULT)
|
|
51
|
+
class AgnoRuntime(BaseRuntime):
|
|
52
|
+
"""
|
|
53
|
+
Runtime implementation using Agno framework.
|
|
54
|
+
|
|
55
|
+
This runtime wraps the Agno-based agent execution logic,
|
|
56
|
+
providing a clean interface that conforms to the AgentRuntime protocol.
|
|
57
|
+
|
|
58
|
+
Features:
|
|
59
|
+
- LiteLLM-based model execution
|
|
60
|
+
- Real-time streaming with event batching
|
|
61
|
+
- Tool execution hooks
|
|
62
|
+
- Conversation history support
|
|
63
|
+
- Comprehensive usage tracking
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
control_plane_client: "ControlPlaneClient",
|
|
69
|
+
cancellation_manager: "CancellationManager",
|
|
70
|
+
**kwargs,
|
|
71
|
+
):
|
|
72
|
+
"""
|
|
73
|
+
Initialize the Agno runtime.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
control_plane_client: Client for Control Plane API
|
|
77
|
+
cancellation_manager: Manager for execution cancellation
|
|
78
|
+
**kwargs: Additional configuration options
|
|
79
|
+
"""
|
|
80
|
+
super().__init__(control_plane_client, cancellation_manager, **kwargs)
|
|
81
|
+
self._custom_tools: Dict[str, Any] = {} # tool_id -> tool instance
|
|
82
|
+
|
|
83
|
+
def get_runtime_type(self) -> RuntimeType:
|
|
84
|
+
"""Return RuntimeType.DEFAULT."""
|
|
85
|
+
return RuntimeType.DEFAULT
|
|
86
|
+
|
|
87
|
+
def get_capabilities(self) -> RuntimeCapabilities:
|
|
88
|
+
"""Return Agno runtime capabilities."""
|
|
89
|
+
return RuntimeCapabilities(
|
|
90
|
+
streaming=True,
|
|
91
|
+
tools=True,
|
|
92
|
+
mcp=True, # Agno supports MCP via MCPTools
|
|
93
|
+
hooks=True,
|
|
94
|
+
cancellation=True,
|
|
95
|
+
conversation_history=True,
|
|
96
|
+
custom_tools=True # Agno supports custom Python tools
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _validate_mcp_tool_names(self, mcp_tool: Any, execution_id: str) -> Any:
|
|
100
|
+
"""
|
|
101
|
+
Validate and sanitize MCP tool function names.
|
|
102
|
+
|
|
103
|
+
This ensures MCP tools from external servers meet universal LLM provider requirements.
|
|
104
|
+
Agno's MCPTools.functions property contains the actual tool definitions.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
mcp_tool: Connected MCPTools instance
|
|
108
|
+
execution_id: Execution ID for logging
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
The MCP tool instance with validated function names
|
|
112
|
+
"""
|
|
113
|
+
if not hasattr(mcp_tool, 'functions') or not mcp_tool.functions:
|
|
114
|
+
return mcp_tool
|
|
115
|
+
|
|
116
|
+
server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
|
|
117
|
+
|
|
118
|
+
# Convert functions dict to list of Function objects for validation
|
|
119
|
+
# mcp_tool.functions is a Dict[str, Function], we need to validate the Function objects
|
|
120
|
+
functions_list = list(mcp_tool.functions.values())
|
|
121
|
+
|
|
122
|
+
# Validate and sanitize the function names
|
|
123
|
+
validated_functions, validation_report = validate_and_sanitize_tools(
|
|
124
|
+
functions_list,
|
|
125
|
+
tool_name_getter=lambda f: getattr(f, 'name', str(f)),
|
|
126
|
+
auto_fix=True,
|
|
127
|
+
provider_context=f"agno_mcp_{server_name}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Log validation results
|
|
131
|
+
sanitized_count = sum(1 for r in validation_report if r['action'] == 'sanitized')
|
|
132
|
+
filtered_count = sum(1 for r in validation_report if r['action'] == 'filtered')
|
|
133
|
+
|
|
134
|
+
if sanitized_count > 0:
|
|
135
|
+
self.logger.warning(
|
|
136
|
+
"mcp_tool_names_sanitized",
|
|
137
|
+
server_name=server_name,
|
|
138
|
+
execution_id=execution_id,
|
|
139
|
+
sanitized_count=sanitized_count,
|
|
140
|
+
total_functions=len(mcp_tool.functions),
|
|
141
|
+
details=[r for r in validation_report if r['action'] == 'sanitized'][:5] # Limit details
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if filtered_count > 0:
|
|
145
|
+
self.logger.error(
|
|
146
|
+
"mcp_tool_names_filtered",
|
|
147
|
+
server_name=server_name,
|
|
148
|
+
execution_id=execution_id,
|
|
149
|
+
filtered_count=filtered_count,
|
|
150
|
+
total_functions=len(mcp_tool.functions),
|
|
151
|
+
details=[r for r in validation_report if r['action'] == 'filtered']
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Reconstruct the functions dict from validated Function objects
|
|
155
|
+
# Preserve the Dict[str, Function] structure that Agno expects
|
|
156
|
+
from collections import OrderedDict
|
|
157
|
+
validated_functions_dict = OrderedDict()
|
|
158
|
+
for func in validated_functions:
|
|
159
|
+
func_name = getattr(func, 'name', str(func))
|
|
160
|
+
validated_functions_dict[func_name] = func
|
|
161
|
+
|
|
162
|
+
# Update the MCP tool with validated functions dict
|
|
163
|
+
mcp_tool.functions = validated_functions_dict
|
|
164
|
+
|
|
165
|
+
return mcp_tool
|
|
166
|
+
|
|
167
|
+
async def _execute_impl(
|
|
168
|
+
self, context: RuntimeExecutionContext
|
|
169
|
+
) -> RuntimeExecutionResult:
|
|
170
|
+
"""
|
|
171
|
+
Execute agent using Agno framework without streaming.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
context: Execution context with prompt, history, config
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
RuntimeExecutionResult with response and metadata
|
|
178
|
+
"""
|
|
179
|
+
mcp_tools_instances = []
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
# Build MCP tools from context
|
|
183
|
+
mcp_tools_instances = build_agno_mcp_tools(context.mcp_servers)
|
|
184
|
+
|
|
185
|
+
# Connect MCP tools
|
|
186
|
+
connected_mcp_tools = []
|
|
187
|
+
for mcp_tool in mcp_tools_instances:
|
|
188
|
+
try:
|
|
189
|
+
await mcp_tool.connect()
|
|
190
|
+
|
|
191
|
+
# Verify the tool is actually initialized (agno doesn't raise on failure)
|
|
192
|
+
if not mcp_tool.initialized:
|
|
193
|
+
server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
|
|
194
|
+
error_msg = f"Failed to initialize MCP tool: {server_name}"
|
|
195
|
+
self.logger.error(
|
|
196
|
+
"mcp_tool_initialization_failed",
|
|
197
|
+
server_name=server_name,
|
|
198
|
+
execution_id=context.execution_id,
|
|
199
|
+
error=error_msg,
|
|
200
|
+
)
|
|
201
|
+
raise RuntimeError(error_msg)
|
|
202
|
+
|
|
203
|
+
# Verify it has tools available
|
|
204
|
+
if not mcp_tool.functions:
|
|
205
|
+
server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
|
|
206
|
+
error_msg = f"MCP tool {server_name} has no functions available"
|
|
207
|
+
self.logger.error(
|
|
208
|
+
"mcp_tool_has_no_functions",
|
|
209
|
+
server_name=server_name,
|
|
210
|
+
execution_id=context.execution_id,
|
|
211
|
+
error=error_msg,
|
|
212
|
+
)
|
|
213
|
+
raise RuntimeError(error_msg)
|
|
214
|
+
|
|
215
|
+
self.logger.info(
|
|
216
|
+
"mcp_tool_connected",
|
|
217
|
+
execution_id=context.execution_id,
|
|
218
|
+
server_name=getattr(mcp_tool, '_server_name', mcp_tool.name),
|
|
219
|
+
function_count=len(mcp_tool.functions),
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# UNIVERSAL VALIDATION: Validate MCP tool names
|
|
223
|
+
validated_mcp_tool = self._validate_mcp_tool_names(mcp_tool, context.execution_id)
|
|
224
|
+
connected_mcp_tools.append(validated_mcp_tool)
|
|
225
|
+
|
|
226
|
+
except Exception as e:
|
|
227
|
+
server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
|
|
228
|
+
self.logger.error(
|
|
229
|
+
"mcp_tool_connection_failed",
|
|
230
|
+
error=str(e),
|
|
231
|
+
error_type=type(e).__name__,
|
|
232
|
+
server_name=server_name,
|
|
233
|
+
execution_id=context.execution_id,
|
|
234
|
+
)
|
|
235
|
+
import traceback
|
|
236
|
+
self.logger.debug(
|
|
237
|
+
"mcp_tool_connection_error_traceback",
|
|
238
|
+
traceback=traceback.format_exc(),
|
|
239
|
+
execution_id=context.execution_id,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Publish MCP connection error event
|
|
243
|
+
try:
|
|
244
|
+
from control_plane_api.worker.utils.error_publisher import (
|
|
245
|
+
ErrorEventPublisher, ErrorSeverity, ErrorCategory
|
|
246
|
+
)
|
|
247
|
+
error_publisher = ErrorEventPublisher(self.control_plane)
|
|
248
|
+
await error_publisher.publish_error(
|
|
249
|
+
execution_id=context.execution_id,
|
|
250
|
+
exception=e,
|
|
251
|
+
severity=ErrorSeverity.WARNING,
|
|
252
|
+
category=ErrorCategory.MCP_CONNECTION,
|
|
253
|
+
stage="initialization",
|
|
254
|
+
component="mcp_server",
|
|
255
|
+
operation=f"connect_{server_name}",
|
|
256
|
+
metadata={"server_name": server_name},
|
|
257
|
+
recovery_actions=[
|
|
258
|
+
"Verify MCP server is running and accessible",
|
|
259
|
+
"Check MCP server configuration",
|
|
260
|
+
"Review network connectivity",
|
|
261
|
+
],
|
|
262
|
+
)
|
|
263
|
+
except Exception as publish_error:
|
|
264
|
+
# Log warning but don't block execution
|
|
265
|
+
self.logger.warning(
|
|
266
|
+
f"Failed to publish MCP connection error: {publish_error}",
|
|
267
|
+
execution_id=context.execution_id,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Continue with other MCP tools even if one fails
|
|
271
|
+
|
|
272
|
+
# Use only successfully connected tools
|
|
273
|
+
mcp_tools_instances = connected_mcp_tools
|
|
274
|
+
|
|
275
|
+
# Merge regular skills with custom tools
|
|
276
|
+
# IMPORTANT: Deep copy skills to isolate Function objects between executions
|
|
277
|
+
# This prevents schema corruption from shared mutable state in Function.parameters
|
|
278
|
+
from copy import deepcopy
|
|
279
|
+
|
|
280
|
+
all_skills = []
|
|
281
|
+
if context.skills:
|
|
282
|
+
for skill in context.skills:
|
|
283
|
+
try:
|
|
284
|
+
# Deep copy the skill to ensure Function objects are isolated
|
|
285
|
+
# This prevents process_entrypoint() from modifying shared state
|
|
286
|
+
if hasattr(skill, 'functions') and hasattr(skill.functions, 'items'):
|
|
287
|
+
copied_skill = deepcopy(skill)
|
|
288
|
+
all_skills.append(copied_skill)
|
|
289
|
+
self.logger.debug(
|
|
290
|
+
"skill_deep_copied",
|
|
291
|
+
skill_name=getattr(skill, 'name', 'unknown'),
|
|
292
|
+
function_count=len(skill.functions) if hasattr(skill, 'functions') else 0,
|
|
293
|
+
execution_id=context.execution_id,
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
# For non-Toolkit skills, use as-is
|
|
297
|
+
all_skills.append(skill)
|
|
298
|
+
except Exception as e:
|
|
299
|
+
# If deep copy fails, fall back to original skill and log warning
|
|
300
|
+
self.logger.warning(
|
|
301
|
+
"skill_deep_copy_failed_using_original",
|
|
302
|
+
skill_name=getattr(skill, 'name', 'unknown'),
|
|
303
|
+
error=str(e),
|
|
304
|
+
error_type=type(e).__name__,
|
|
305
|
+
execution_id=context.execution_id,
|
|
306
|
+
)
|
|
307
|
+
all_skills.append(skill)
|
|
308
|
+
|
|
309
|
+
# Add custom tools
|
|
310
|
+
if self._custom_tools:
|
|
311
|
+
for tool_id, custom_tool in self._custom_tools.items():
|
|
312
|
+
try:
|
|
313
|
+
# Get toolkit from custom tool
|
|
314
|
+
toolkit = custom_tool.get_tools()
|
|
315
|
+
|
|
316
|
+
# Extract tools - handle both Toolkit objects and iterables
|
|
317
|
+
if hasattr(toolkit, 'tools'):
|
|
318
|
+
all_skills.extend(toolkit.tools)
|
|
319
|
+
elif hasattr(toolkit, '__iter__'):
|
|
320
|
+
all_skills.extend(toolkit)
|
|
321
|
+
else:
|
|
322
|
+
all_skills.append(toolkit)
|
|
323
|
+
|
|
324
|
+
self.logger.debug(
|
|
325
|
+
"custom_tool_loaded",
|
|
326
|
+
tool_id=tool_id,
|
|
327
|
+
execution_id=context.execution_id
|
|
328
|
+
)
|
|
329
|
+
except Exception as e:
|
|
330
|
+
self.logger.error(
|
|
331
|
+
"custom_tool_load_failed",
|
|
332
|
+
tool_id=tool_id,
|
|
333
|
+
error=str(e),
|
|
334
|
+
execution_id=context.execution_id
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
# Extract metadata for Langfuse tracking
|
|
338
|
+
user_id = None
|
|
339
|
+
session_id = None
|
|
340
|
+
agent_name = None
|
|
341
|
+
|
|
342
|
+
if context.user_metadata:
|
|
343
|
+
user_id = context.user_metadata.get("user_email") or context.user_metadata.get("user_id")
|
|
344
|
+
session_id = context.user_metadata.get("session_id") or context.execution_id
|
|
345
|
+
agent_name = context.user_metadata.get("agent_name") or context.agent_id
|
|
346
|
+
|
|
347
|
+
# DEBUG: Log metadata extraction
|
|
348
|
+
self.logger.warning(
|
|
349
|
+
"🔍 DEBUG: AGNO RUNTIME (_execute_impl) - METADATA EXTRACTION",
|
|
350
|
+
context_user_metadata=context.user_metadata,
|
|
351
|
+
extracted_user_id=user_id,
|
|
352
|
+
extracted_session_id=session_id,
|
|
353
|
+
extracted_agent_name=agent_name,
|
|
354
|
+
organization_id=context.organization_id,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
# Create Agno agent with all tools (skills + MCP tools) and metadata
|
|
358
|
+
agent = build_agno_agent_config(
|
|
359
|
+
agent_id=context.agent_id,
|
|
360
|
+
system_prompt=context.system_prompt,
|
|
361
|
+
model_id=context.model_id,
|
|
362
|
+
skills=all_skills,
|
|
363
|
+
mcp_tools=mcp_tools_instances,
|
|
364
|
+
tool_hooks=None,
|
|
365
|
+
user_id=user_id,
|
|
366
|
+
session_id=session_id,
|
|
367
|
+
organization_id=context.organization_id,
|
|
368
|
+
agent_name=agent_name,
|
|
369
|
+
skill_configs=context.skill_configs,
|
|
370
|
+
user_metadata=context.user_metadata,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Register for cancellation
|
|
374
|
+
self.cancellation_manager.register(
|
|
375
|
+
execution_id=context.execution_id,
|
|
376
|
+
instance=agent,
|
|
377
|
+
instance_type="agent",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Log tool schema snapshots for debugging
|
|
381
|
+
# This helps detect schema inconsistencies and parameter mismatches
|
|
382
|
+
if context.execution_id:
|
|
383
|
+
for skill in all_skills:
|
|
384
|
+
if hasattr(skill, 'functions') and hasattr(skill.functions, 'items'):
|
|
385
|
+
skill_name = getattr(skill, 'name', 'unknown')
|
|
386
|
+
for func_name, func_obj in skill.functions.items():
|
|
387
|
+
if hasattr(func_obj, 'parameters') and isinstance(func_obj.parameters, dict):
|
|
388
|
+
param_names = list(func_obj.parameters.get('properties', {}).keys())
|
|
389
|
+
self.logger.debug(
|
|
390
|
+
"tool_schema_snapshot",
|
|
391
|
+
execution_id=context.execution_id,
|
|
392
|
+
skill_name=skill_name,
|
|
393
|
+
tool_name=func_name,
|
|
394
|
+
parameters=param_names,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
# Build conversation context
|
|
398
|
+
messages = build_conversation_messages(context.conversation_history)
|
|
399
|
+
|
|
400
|
+
# Determine if we need async execution (when MCP tools are present)
|
|
401
|
+
has_async_tools = len(mcp_tools_instances) > 0
|
|
402
|
+
|
|
403
|
+
# Execute without streaming
|
|
404
|
+
if has_async_tools:
|
|
405
|
+
# Use async agent.arun() for MCP tools
|
|
406
|
+
if messages:
|
|
407
|
+
result = await agent.arun(context.prompt, stream=False, messages=messages)
|
|
408
|
+
else:
|
|
409
|
+
result = await agent.arun(context.prompt, stream=False)
|
|
410
|
+
else:
|
|
411
|
+
# Use sync agent.run() for non-MCP tools
|
|
412
|
+
def run_agent():
|
|
413
|
+
if messages:
|
|
414
|
+
return agent.run(context.prompt, stream=False, messages=messages)
|
|
415
|
+
else:
|
|
416
|
+
return agent.run(context.prompt, stream=False)
|
|
417
|
+
|
|
418
|
+
# Run in thread pool to avoid blocking
|
|
419
|
+
result = await asyncio.to_thread(run_agent)
|
|
420
|
+
|
|
421
|
+
# Cleanup
|
|
422
|
+
self.cancellation_manager.unregister(context.execution_id)
|
|
423
|
+
|
|
424
|
+
# Extract response and metadata
|
|
425
|
+
response_content = extract_response_content(result)
|
|
426
|
+
usage = extract_usage(result)
|
|
427
|
+
tool_messages = extract_tool_messages(result)
|
|
428
|
+
|
|
429
|
+
return RuntimeExecutionResult(
|
|
430
|
+
response=response_content,
|
|
431
|
+
usage=usage,
|
|
432
|
+
success=True,
|
|
433
|
+
finish_reason="stop",
|
|
434
|
+
run_id=getattr(result, "run_id", None),
|
|
435
|
+
model=context.model_id,
|
|
436
|
+
tool_messages=tool_messages,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
except asyncio.CancelledError:
|
|
440
|
+
# Handle cancellation
|
|
441
|
+
self.cancellation_manager.cancel(context.execution_id)
|
|
442
|
+
self.cancellation_manager.unregister(context.execution_id)
|
|
443
|
+
raise
|
|
444
|
+
|
|
445
|
+
except Exception as e:
|
|
446
|
+
self.logger.error(
|
|
447
|
+
"agno_execution_failed",
|
|
448
|
+
execution_id=context.execution_id,
|
|
449
|
+
error=str(e),
|
|
450
|
+
)
|
|
451
|
+
self.cancellation_manager.unregister(context.execution_id)
|
|
452
|
+
|
|
453
|
+
# Publish error event
|
|
454
|
+
try:
|
|
455
|
+
from control_plane_api.worker.utils.error_publisher import (
|
|
456
|
+
ErrorEventPublisher, ErrorSeverity, ErrorCategory
|
|
457
|
+
)
|
|
458
|
+
error_publisher = ErrorEventPublisher(self.control_plane)
|
|
459
|
+
await error_publisher.publish_error(
|
|
460
|
+
execution_id=context.execution_id,
|
|
461
|
+
exception=e,
|
|
462
|
+
severity=ErrorSeverity.CRITICAL,
|
|
463
|
+
category=ErrorCategory.UNKNOWN,
|
|
464
|
+
stage="execution",
|
|
465
|
+
component="agno_runtime",
|
|
466
|
+
operation="agent_execution",
|
|
467
|
+
include_stack_trace=True,
|
|
468
|
+
)
|
|
469
|
+
except Exception:
|
|
470
|
+
pass # Never break execution flow
|
|
471
|
+
|
|
472
|
+
return RuntimeExecutionResult(
|
|
473
|
+
response="",
|
|
474
|
+
usage={},
|
|
475
|
+
success=False,
|
|
476
|
+
error=str(e),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
finally:
|
|
480
|
+
# Close MCP tool connections
|
|
481
|
+
for mcp_tool in mcp_tools_instances:
|
|
482
|
+
try:
|
|
483
|
+
await mcp_tool.close()
|
|
484
|
+
self.logger.debug(
|
|
485
|
+
"mcp_tool_closed",
|
|
486
|
+
execution_id=context.execution_id,
|
|
487
|
+
)
|
|
488
|
+
except Exception as e:
|
|
489
|
+
self.logger.error(
|
|
490
|
+
"mcp_tool_close_failed",
|
|
491
|
+
error=str(e),
|
|
492
|
+
execution_id=context.execution_id,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
async def _stream_execute_impl(
|
|
496
|
+
self,
|
|
497
|
+
context: RuntimeExecutionContext,
|
|
498
|
+
event_callback: Optional[Callable[[Dict], None]] = None,
|
|
499
|
+
) -> AsyncIterator[RuntimeExecutionResult]:
|
|
500
|
+
"""
|
|
501
|
+
Execute agent with streaming using Agno framework with efficient event batching.
|
|
502
|
+
|
|
503
|
+
This implementation uses the EventPublisher service to batch message chunks,
|
|
504
|
+
reducing HTTP requests by 90-96% while keeping tool events immediate.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
context: Execution context
|
|
508
|
+
event_callback: Optional callback for real-time events
|
|
509
|
+
|
|
510
|
+
Yields:
|
|
511
|
+
RuntimeExecutionResult chunks as they arrive in real-time
|
|
512
|
+
"""
|
|
513
|
+
# Create event publisher with batching
|
|
514
|
+
event_publisher = EventPublisher(
|
|
515
|
+
control_plane=self.control_plane,
|
|
516
|
+
execution_id=context.execution_id,
|
|
517
|
+
config=EventPublisherConfig.from_env(),
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
mcp_tools_instances = []
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
# Build MCP tools from context
|
|
524
|
+
mcp_tools_instances = build_agno_mcp_tools(context.mcp_servers)
|
|
525
|
+
|
|
526
|
+
# Connect MCP tools
|
|
527
|
+
connected_mcp_tools = []
|
|
528
|
+
for mcp_tool in mcp_tools_instances:
|
|
529
|
+
try:
|
|
530
|
+
await mcp_tool.connect()
|
|
531
|
+
|
|
532
|
+
# Verify the tool is actually initialized (agno doesn't raise on failure)
|
|
533
|
+
if not mcp_tool.initialized:
|
|
534
|
+
server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
|
|
535
|
+
error_msg = f"Failed to initialize MCP tool: {server_name}"
|
|
536
|
+
self.logger.error(
|
|
537
|
+
"mcp_tool_initialization_failed",
|
|
538
|
+
server_name=server_name,
|
|
539
|
+
execution_id=context.execution_id,
|
|
540
|
+
error=error_msg,
|
|
541
|
+
)
|
|
542
|
+
raise RuntimeError(error_msg)
|
|
543
|
+
|
|
544
|
+
# Verify it has tools available
|
|
545
|
+
if not mcp_tool.functions:
|
|
546
|
+
server_name = getattr(mcp_tool, '_server_name', mcp_tool.name)
|
|
547
|
+
error_msg = f"MCP tool {server_name} has no functions available"
|
|
548
|
+
self.logger.error(
|
|
549
|
+
"mcp_tool_has_no_functions",
|
|
550
|
+
server_name=server_name,
|
|
551
|
+
execution_id=context.execution_id,
|
|
552
|
+
error=error_msg,
|
|
553
|
+
)
|
|
554
|
+
raise RuntimeError(error_msg)
|
|
555
|
+
|
|
556
|
+
self.logger.info(
|
|
557
|
+
"mcp_tool_connected_streaming",
|
|
558
|
+
execution_id=context.execution_id,
|
|
559
|
+
server_name=getattr(mcp_tool, '_server_name', mcp_tool.name),
|
|
560
|
+
function_count=len(mcp_tool.functions),
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
# UNIVERSAL VALIDATION: Validate MCP tool names
|
|
564
|
+
validated_mcp_tool = self._validate_mcp_tool_names(mcp_tool, context.execution_id)
|
|
565
|
+
connected_mcp_tools.append(validated_mcp_tool)
|
|
566
|
+
|
|
567
|
+
except Exception as e:
|
|
568
|
+
self.logger.error(
|
|
569
|
+
"mcp_tool_connection_failed_streaming",
|
|
570
|
+
error=str(e),
|
|
571
|
+
error_type=type(e).__name__,
|
|
572
|
+
server_name=getattr(mcp_tool, '_server_name', mcp_tool.name),
|
|
573
|
+
execution_id=context.execution_id,
|
|
574
|
+
)
|
|
575
|
+
import traceback
|
|
576
|
+
self.logger.debug(
|
|
577
|
+
"mcp_tool_connection_error_traceback",
|
|
578
|
+
traceback=traceback.format_exc(),
|
|
579
|
+
execution_id=context.execution_id,
|
|
580
|
+
)
|
|
581
|
+
# Continue with other MCP tools even if one fails
|
|
582
|
+
|
|
583
|
+
# Use only successfully connected tools
|
|
584
|
+
mcp_tools_instances = connected_mcp_tools
|
|
585
|
+
|
|
586
|
+
# Build conversation context
|
|
587
|
+
messages = build_conversation_messages(context.conversation_history)
|
|
588
|
+
|
|
589
|
+
# Determine if we need async execution (when MCP tools are present)
|
|
590
|
+
has_async_tools = len(mcp_tools_instances) > 0
|
|
591
|
+
|
|
592
|
+
# Stream execution - publish events INSIDE the thread (like old code)
|
|
593
|
+
accumulated_response = ""
|
|
594
|
+
run_result = None
|
|
595
|
+
|
|
596
|
+
# Create queue for streaming chunks from thread to async
|
|
597
|
+
chunk_queue = queue.Queue()
|
|
598
|
+
|
|
599
|
+
# Generate unique message ID
|
|
600
|
+
message_id = f"{context.execution_id}_msg_{int(time.time() * 1000000)}"
|
|
601
|
+
|
|
602
|
+
# Merge regular skills with custom tools
|
|
603
|
+
all_skills = list(context.skills) if context.skills else []
|
|
604
|
+
|
|
605
|
+
# Add custom tools
|
|
606
|
+
if self._custom_tools:
|
|
607
|
+
for tool_id, custom_tool in self._custom_tools.items():
|
|
608
|
+
try:
|
|
609
|
+
# Get toolkit from custom tool
|
|
610
|
+
toolkit = custom_tool.get_tools()
|
|
611
|
+
|
|
612
|
+
# Extract tools - handle both Toolkit objects and iterables
|
|
613
|
+
if hasattr(toolkit, 'tools'):
|
|
614
|
+
all_skills.extend(toolkit.tools)
|
|
615
|
+
elif hasattr(toolkit, '__iter__'):
|
|
616
|
+
all_skills.extend(toolkit)
|
|
617
|
+
else:
|
|
618
|
+
all_skills.append(toolkit)
|
|
619
|
+
|
|
620
|
+
self.logger.debug(
|
|
621
|
+
"custom_tool_loaded_streaming",
|
|
622
|
+
tool_id=tool_id,
|
|
623
|
+
execution_id=context.execution_id
|
|
624
|
+
)
|
|
625
|
+
except Exception as e:
|
|
626
|
+
self.logger.error(
|
|
627
|
+
"custom_tool_load_failed_streaming",
|
|
628
|
+
tool_id=tool_id,
|
|
629
|
+
error=str(e),
|
|
630
|
+
execution_id=context.execution_id
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
# Initialize enforcement service
|
|
634
|
+
enforcement_context = {
|
|
635
|
+
"organization_id": context.organization_id,
|
|
636
|
+
"user_email": context.user_email,
|
|
637
|
+
"user_id": context.user_id,
|
|
638
|
+
"user_roles": context.user_roles or [],
|
|
639
|
+
"team_id": context.team_id,
|
|
640
|
+
"team_name": context.team_name,
|
|
641
|
+
"agent_id": context.agent_id,
|
|
642
|
+
"environment": context.environment,
|
|
643
|
+
"model_id": context.model_id,
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
# Import enforcement dependencies
|
|
647
|
+
from control_plane_api.app.lib.policy_enforcer_client import create_policy_enforcer_client
|
|
648
|
+
from control_plane_api.worker.services.tool_enforcement import ToolEnforcementService
|
|
649
|
+
import os
|
|
650
|
+
|
|
651
|
+
# Get enforcer client (using the same token as the control plane)
|
|
652
|
+
enforcer_client = None
|
|
653
|
+
enforcement_service = None
|
|
654
|
+
|
|
655
|
+
# Check if enforcement is enabled (opt-in via environment variable)
|
|
656
|
+
enforcement_enabled = os.environ.get("KUBIYA_ENFORCE_ENABLED", "").lower() in ("true", "1", "yes")
|
|
657
|
+
|
|
658
|
+
if not enforcement_enabled:
|
|
659
|
+
self.logger.info(
|
|
660
|
+
"policy_enforcement_disabled",
|
|
661
|
+
reason="KUBIYA_ENFORCE_ENABLED not set",
|
|
662
|
+
execution_id=context.execution_id[:8],
|
|
663
|
+
note="Set KUBIYA_ENFORCE_ENABLED=true to enable policy enforcement"
|
|
664
|
+
)
|
|
665
|
+
else:
|
|
666
|
+
try:
|
|
667
|
+
# Get enforcer URL - default to control plane enforcer proxy
|
|
668
|
+
enforcer_url = os.environ.get("ENFORCER_SERVICE_URL")
|
|
669
|
+
if not enforcer_url:
|
|
670
|
+
# Use control plane's enforcer proxy as default
|
|
671
|
+
control_plane_url = os.environ.get("CONTROL_PLANE_URL", "http://localhost:8000")
|
|
672
|
+
enforcer_url = f"{control_plane_url.rstrip('/')}/api/v1/enforcer"
|
|
673
|
+
self.logger.debug(
|
|
674
|
+
"using_control_plane_enforcer_proxy",
|
|
675
|
+
enforcer_url=enforcer_url,
|
|
676
|
+
execution_id=context.execution_id[:8],
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
# Use async context manager properly (we're in an async function)
|
|
680
|
+
enforcer_client_context = create_policy_enforcer_client(
|
|
681
|
+
enforcer_url=enforcer_url,
|
|
682
|
+
api_key=self.control_plane.api_key,
|
|
683
|
+
auth_type="UserKey"
|
|
684
|
+
)
|
|
685
|
+
enforcer_client = await enforcer_client_context.__aenter__()
|
|
686
|
+
if enforcer_client:
|
|
687
|
+
enforcement_service = ToolEnforcementService(enforcer_client)
|
|
688
|
+
self.logger.info(
|
|
689
|
+
"policy_enforcement_enabled",
|
|
690
|
+
enforcer_url=enforcer_url,
|
|
691
|
+
execution_id=context.execution_id[:8],
|
|
692
|
+
)
|
|
693
|
+
except Exception as e:
|
|
694
|
+
self.logger.warning(
|
|
695
|
+
"enforcement_service_init_failed",
|
|
696
|
+
error=str(e),
|
|
697
|
+
execution_id=context.execution_id[:8],
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
# Create tool hook that publishes directly to Control Plane with enforcement
|
|
701
|
+
tool_hook = create_tool_hook_for_streaming(
|
|
702
|
+
control_plane=self.control_plane,
|
|
703
|
+
execution_id=context.execution_id,
|
|
704
|
+
message_id=message_id, # Link tools to this assistant message turn
|
|
705
|
+
enforcement_context=enforcement_context,
|
|
706
|
+
enforcement_service=enforcement_service,
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
# Extract metadata for Langfuse tracking
|
|
710
|
+
user_id = None
|
|
711
|
+
session_id = None
|
|
712
|
+
agent_name = None
|
|
713
|
+
|
|
714
|
+
if context.user_metadata:
|
|
715
|
+
user_id = context.user_metadata.get("user_email") or context.user_metadata.get("user_id")
|
|
716
|
+
session_id = context.user_metadata.get("session_id") or context.execution_id
|
|
717
|
+
agent_name = context.user_metadata.get("agent_name") or context.agent_id
|
|
718
|
+
|
|
719
|
+
# DEBUG: Log metadata extraction
|
|
720
|
+
self.logger.warning(
|
|
721
|
+
"🔍 DEBUG: AGNO RUNTIME (_stream_execute_impl) - METADATA EXTRACTION",
|
|
722
|
+
context_user_metadata=context.user_metadata,
|
|
723
|
+
extracted_user_id=user_id,
|
|
724
|
+
extracted_session_id=session_id,
|
|
725
|
+
extracted_agent_name=agent_name,
|
|
726
|
+
organization_id=context.organization_id,
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
# Create Agno agent with all tools (skills + MCP tools), tool hooks, and metadata
|
|
730
|
+
agent = build_agno_agent_config(
|
|
731
|
+
agent_id=context.agent_id,
|
|
732
|
+
system_prompt=context.system_prompt,
|
|
733
|
+
model_id=context.model_id,
|
|
734
|
+
skills=all_skills,
|
|
735
|
+
mcp_tools=mcp_tools_instances,
|
|
736
|
+
tool_hooks=[tool_hook],
|
|
737
|
+
user_id=user_id,
|
|
738
|
+
session_id=session_id,
|
|
739
|
+
organization_id=context.organization_id,
|
|
740
|
+
agent_name=agent_name,
|
|
741
|
+
skill_configs=context.skill_configs,
|
|
742
|
+
user_metadata=context.user_metadata,
|
|
743
|
+
)
|
|
744
|
+
|
|
745
|
+
# Register for cancellation
|
|
746
|
+
self.cancellation_manager.register(
|
|
747
|
+
execution_id=context.execution_id,
|
|
748
|
+
instance=agent,
|
|
749
|
+
instance_type="agent",
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
# Cache execution metadata
|
|
753
|
+
self.control_plane.cache_metadata(context.execution_id, "AGENT")
|
|
754
|
+
|
|
755
|
+
def stream_agent_run():
|
|
756
|
+
"""
|
|
757
|
+
Run agent with streaming and publish events directly to Control Plane.
|
|
758
|
+
This runs in a thread pool, so blocking HTTP calls are OK here.
|
|
759
|
+
Put chunks in queue for async iterator to yield in real-time.
|
|
760
|
+
"""
|
|
761
|
+
nonlocal accumulated_response, run_result
|
|
762
|
+
run_id_published = False
|
|
763
|
+
|
|
764
|
+
# Use thread-local event loop from control_plane client
|
|
765
|
+
# This ensures all async operations (tool hooks, event publishing) share the same loop
|
|
766
|
+
# and it persists until explicitly cleaned up at the end of execution
|
|
767
|
+
thread_loop = self.control_plane._get_thread_event_loop()
|
|
768
|
+
|
|
769
|
+
try:
|
|
770
|
+
# Use async streaming for MCP tools, sync for others
|
|
771
|
+
if has_async_tools:
|
|
772
|
+
# For async tools (MCP), we need to use agent.arun() in an async context
|
|
773
|
+
# Use the thread-local event loop instead of creating a new one
|
|
774
|
+
if messages:
|
|
775
|
+
stream_response = thread_loop.run_until_complete(
|
|
776
|
+
agent.arun(context.prompt, stream=True, messages=messages)
|
|
777
|
+
)
|
|
778
|
+
else:
|
|
779
|
+
stream_response = thread_loop.run_until_complete(
|
|
780
|
+
agent.arun(context.prompt, stream=True)
|
|
781
|
+
)
|
|
782
|
+
else:
|
|
783
|
+
# Use sync agent.run() for non-MCP tools
|
|
784
|
+
if messages:
|
|
785
|
+
stream_response = agent.run(
|
|
786
|
+
context.prompt,
|
|
787
|
+
stream=True,
|
|
788
|
+
messages=messages,
|
|
789
|
+
)
|
|
790
|
+
else:
|
|
791
|
+
stream_response = agent.run(context.prompt, stream=True)
|
|
792
|
+
|
|
793
|
+
# Iterate over streaming chunks and publish IMMEDIATELY
|
|
794
|
+
for chunk in stream_response:
|
|
795
|
+
# Capture run_id for cancellation (first chunk)
|
|
796
|
+
if not run_id_published and hasattr(chunk, "run_id") and chunk.run_id:
|
|
797
|
+
self.cancellation_manager.set_run_id(
|
|
798
|
+
context.execution_id, chunk.run_id
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
# Publish run_id event
|
|
802
|
+
self.control_plane.publish_event(
|
|
803
|
+
execution_id=context.execution_id,
|
|
804
|
+
event_type="run_started",
|
|
805
|
+
data={
|
|
806
|
+
"run_id": chunk.run_id,
|
|
807
|
+
"execution_id": context.execution_id,
|
|
808
|
+
"cancellable": True,
|
|
809
|
+
}
|
|
810
|
+
)
|
|
811
|
+
run_id_published = True
|
|
812
|
+
|
|
813
|
+
# Extract content
|
|
814
|
+
chunk_content = ""
|
|
815
|
+
if hasattr(chunk, "content") and chunk.content:
|
|
816
|
+
if isinstance(chunk.content, str):
|
|
817
|
+
chunk_content = chunk.content
|
|
818
|
+
elif hasattr(chunk.content, "text"):
|
|
819
|
+
chunk_content = chunk.content.text
|
|
820
|
+
|
|
821
|
+
# Filter out whitespace-only chunks to prevent "(no content)" in UI
|
|
822
|
+
if chunk_content and chunk_content.strip():
|
|
823
|
+
accumulated_response += chunk_content
|
|
824
|
+
|
|
825
|
+
# Queue chunk for batched publishing (via EventPublisher in async context)
|
|
826
|
+
# This reduces 300 HTTP requests → 12 requests (96% reduction)
|
|
827
|
+
chunk_queue.put(("chunk", chunk_content, message_id))
|
|
828
|
+
|
|
829
|
+
# Store final result
|
|
830
|
+
run_result = stream_response
|
|
831
|
+
|
|
832
|
+
# Signal completion
|
|
833
|
+
chunk_queue.put(("done", run_result))
|
|
834
|
+
|
|
835
|
+
except Exception as e:
|
|
836
|
+
self.logger.error("streaming_error", error=str(e))
|
|
837
|
+
chunk_queue.put(("error", e))
|
|
838
|
+
raise
|
|
839
|
+
|
|
840
|
+
# Start streaming in background thread
|
|
841
|
+
stream_thread = threading.Thread(target=stream_agent_run, daemon=True)
|
|
842
|
+
stream_thread.start()
|
|
843
|
+
|
|
844
|
+
# Yield chunks as they arrive in the queue and publish via EventPublisher
|
|
845
|
+
while True:
|
|
846
|
+
try:
|
|
847
|
+
# Non-blocking get with short timeout for responsiveness
|
|
848
|
+
queue_item = await asyncio.to_thread(chunk_queue.get, timeout=0.1)
|
|
849
|
+
|
|
850
|
+
if queue_item[0] == "chunk":
|
|
851
|
+
# Unpack chunk data
|
|
852
|
+
_, chunk_content, msg_id = queue_item
|
|
853
|
+
|
|
854
|
+
# Publish chunk via EventPublisher (batched, non-blocking)
|
|
855
|
+
await event_publisher.publish(
|
|
856
|
+
event_type="message_chunk",
|
|
857
|
+
data={
|
|
858
|
+
"role": "assistant",
|
|
859
|
+
"content": chunk_content,
|
|
860
|
+
"is_chunk": True,
|
|
861
|
+
"message_id": msg_id,
|
|
862
|
+
},
|
|
863
|
+
priority=EventPriority.NORMAL, # Batched
|
|
864
|
+
)
|
|
865
|
+
|
|
866
|
+
# Yield chunk immediately to iterator
|
|
867
|
+
yield RuntimeExecutionResult(
|
|
868
|
+
response=chunk_content,
|
|
869
|
+
usage={},
|
|
870
|
+
success=True,
|
|
871
|
+
)
|
|
872
|
+
elif queue_item[0] == "done":
|
|
873
|
+
# Final result - extract metadata and break
|
|
874
|
+
run_result = queue_item[1]
|
|
875
|
+
break
|
|
876
|
+
elif queue_item[0] == "error":
|
|
877
|
+
# Error occurred in thread
|
|
878
|
+
raise queue_item[1]
|
|
879
|
+
|
|
880
|
+
except queue.Empty:
|
|
881
|
+
# Queue empty, check if thread is still alive
|
|
882
|
+
if not stream_thread.is_alive():
|
|
883
|
+
# Thread died without putting "done" - something went wrong
|
|
884
|
+
break
|
|
885
|
+
# Thread still running, continue waiting
|
|
886
|
+
continue
|
|
887
|
+
|
|
888
|
+
# Wait for thread to complete
|
|
889
|
+
await asyncio.to_thread(stream_thread.join, timeout=5.0)
|
|
890
|
+
|
|
891
|
+
# Yield final result with complete metadata
|
|
892
|
+
usage = extract_usage(run_result) if run_result else {}
|
|
893
|
+
tool_messages = extract_tool_messages(run_result) if run_result else []
|
|
894
|
+
|
|
895
|
+
yield RuntimeExecutionResult(
|
|
896
|
+
response=accumulated_response, # Full accumulated response
|
|
897
|
+
usage=usage,
|
|
898
|
+
success=True,
|
|
899
|
+
finish_reason="stop",
|
|
900
|
+
run_id=getattr(run_result, "run_id", None) if run_result else None,
|
|
901
|
+
model=context.model_id,
|
|
902
|
+
tool_messages=tool_messages,
|
|
903
|
+
metadata={"accumulated_response": accumulated_response},
|
|
904
|
+
)
|
|
905
|
+
|
|
906
|
+
finally:
|
|
907
|
+
# Flush and close event publisher to ensure all batched events are sent
|
|
908
|
+
await event_publisher.flush()
|
|
909
|
+
await event_publisher.close()
|
|
910
|
+
|
|
911
|
+
# Clean up thread-local event loop used by tool hooks
|
|
912
|
+
# This prevents resource leaks and "await wasn't used with future" errors
|
|
913
|
+
self.control_plane.close_thread_event_loop()
|
|
914
|
+
|
|
915
|
+
# Close MCP tool connections
|
|
916
|
+
for mcp_tool in mcp_tools_instances:
|
|
917
|
+
try:
|
|
918
|
+
await mcp_tool.close()
|
|
919
|
+
self.logger.debug(
|
|
920
|
+
"mcp_tool_closed_streaming",
|
|
921
|
+
execution_id=context.execution_id,
|
|
922
|
+
)
|
|
923
|
+
except Exception as e:
|
|
924
|
+
self.logger.error(
|
|
925
|
+
"mcp_tool_close_failed_streaming",
|
|
926
|
+
error=str(e),
|
|
927
|
+
execution_id=context.execution_id,
|
|
928
|
+
)
|
|
929
|
+
|
|
930
|
+
# Close enforcer client context manager (fix resource leak)
|
|
931
|
+
if 'enforcer_client_context' in locals() and enforcer_client_context is not None:
|
|
932
|
+
try:
|
|
933
|
+
await enforcer_client_context.__aexit__(None, None, None)
|
|
934
|
+
self.logger.debug(
|
|
935
|
+
"enforcer_client_closed",
|
|
936
|
+
execution_id=context.execution_id[:8],
|
|
937
|
+
)
|
|
938
|
+
except Exception as e:
|
|
939
|
+
self.logger.warning(
|
|
940
|
+
"enforcer_client_close_failed",
|
|
941
|
+
error=str(e),
|
|
942
|
+
execution_id=context.execution_id[:8],
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
# Cleanup
|
|
946
|
+
self.cancellation_manager.unregister(context.execution_id)
|
|
947
|
+
|
|
948
|
+
# ==================== Custom Tool Extension API ====================
|
|
949
|
+
|
|
950
|
+
def get_custom_tool_requirements(self) -> Dict[str, Any]:
|
|
951
|
+
"""
|
|
952
|
+
Get requirements for creating custom tools for Agno runtime.
|
|
953
|
+
|
|
954
|
+
Returns:
|
|
955
|
+
Dictionary with format, examples, and documentation for Agno custom tools
|
|
956
|
+
"""
|
|
957
|
+
return {
|
|
958
|
+
"format": "python_class",
|
|
959
|
+
"description": "Python class with get_tools() method returning Agno Toolkit",
|
|
960
|
+
"example_code": '''
|
|
961
|
+
from agno.tools import Toolkit
|
|
962
|
+
|
|
963
|
+
class MyCustomTool:
|
|
964
|
+
"""Custom tool for Agno runtime."""
|
|
965
|
+
|
|
966
|
+
def get_tools(self) -> Toolkit:
|
|
967
|
+
"""Return Agno toolkit with custom functions."""
|
|
968
|
+
return Toolkit(
|
|
969
|
+
name="my_tool",
|
|
970
|
+
tools=[self.my_function]
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
def my_function(self, arg: str) -> str:
|
|
974
|
+
"""Tool function description."""
|
|
975
|
+
return f"Result: {arg}"
|
|
976
|
+
''',
|
|
977
|
+
"documentation_url": "https://docs.agno.ai/custom-tools",
|
|
978
|
+
"required_methods": ["get_tools"],
|
|
979
|
+
"schema": {
|
|
980
|
+
"type": "object",
|
|
981
|
+
"required": ["get_tools"],
|
|
982
|
+
"properties": {
|
|
983
|
+
"get_tools": {
|
|
984
|
+
"type": "method",
|
|
985
|
+
"returns": "Toolkit"
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
def validate_custom_tool(self, tool: Any) -> tuple[bool, Optional[str]]:
|
|
992
|
+
"""
|
|
993
|
+
Validate a custom tool for Agno runtime.
|
|
994
|
+
|
|
995
|
+
Args:
|
|
996
|
+
tool: Tool instance to validate
|
|
997
|
+
|
|
998
|
+
Returns:
|
|
999
|
+
Tuple of (is_valid, error_message)
|
|
1000
|
+
"""
|
|
1001
|
+
# Check for get_tools method
|
|
1002
|
+
if not hasattr(tool, 'get_tools'):
|
|
1003
|
+
return False, "Tool must have get_tools() method"
|
|
1004
|
+
|
|
1005
|
+
if not callable(getattr(tool, 'get_tools')):
|
|
1006
|
+
return False, "get_tools must be callable"
|
|
1007
|
+
|
|
1008
|
+
# Try calling to validate return type
|
|
1009
|
+
try:
|
|
1010
|
+
toolkit = tool.get_tools()
|
|
1011
|
+
|
|
1012
|
+
# Check if it's a Toolkit-like object (has tools attribute or is iterable)
|
|
1013
|
+
if not (hasattr(toolkit, 'tools') or hasattr(toolkit, '__iter__')):
|
|
1014
|
+
return False, f"get_tools() must return Toolkit or iterable, got {type(toolkit)}"
|
|
1015
|
+
|
|
1016
|
+
except Exception as e:
|
|
1017
|
+
return False, f"get_tools() failed: {str(e)}"
|
|
1018
|
+
|
|
1019
|
+
return True, None
|
|
1020
|
+
|
|
1021
|
+
def register_custom_tool(self, tool: Any, metadata: Optional[Dict] = None) -> str:
|
|
1022
|
+
"""
|
|
1023
|
+
Register a custom tool with Agno runtime.
|
|
1024
|
+
|
|
1025
|
+
Args:
|
|
1026
|
+
tool: Tool instance with get_tools() method
|
|
1027
|
+
metadata: Optional metadata (name, description, etc.)
|
|
1028
|
+
|
|
1029
|
+
Returns:
|
|
1030
|
+
Tool identifier for this registered tool
|
|
1031
|
+
|
|
1032
|
+
Raises:
|
|
1033
|
+
ValueError: If tool validation fails
|
|
1034
|
+
"""
|
|
1035
|
+
# Validate first
|
|
1036
|
+
is_valid, error = self.validate_custom_tool(tool)
|
|
1037
|
+
if not is_valid:
|
|
1038
|
+
raise ValueError(f"Invalid custom tool: {error}")
|
|
1039
|
+
|
|
1040
|
+
# Generate tool ID
|
|
1041
|
+
tool_name = metadata.get("name") if metadata else tool.__class__.__name__
|
|
1042
|
+
tool_id = f"custom_{tool_name}_{id(tool)}"
|
|
1043
|
+
|
|
1044
|
+
# Store tool instance
|
|
1045
|
+
self._custom_tools[tool_id] = tool
|
|
1046
|
+
|
|
1047
|
+
self.logger.info(
|
|
1048
|
+
"custom_tool_registered",
|
|
1049
|
+
tool_id=tool_id,
|
|
1050
|
+
tool_class=tool.__class__.__name__,
|
|
1051
|
+
tool_name=tool_name
|
|
1052
|
+
)
|
|
1053
|
+
|
|
1054
|
+
return tool_id
|
|
1055
|
+
|
|
1056
|
+
def get_registered_custom_tools(self) -> list[str]:
|
|
1057
|
+
"""
|
|
1058
|
+
Get list of registered custom tool identifiers.
|
|
1059
|
+
|
|
1060
|
+
Returns:
|
|
1061
|
+
List of tool IDs
|
|
1062
|
+
"""
|
|
1063
|
+
return list(self._custom_tools.keys())
|