kubiya-control-plane-api 0.1.0__py3-none-any.whl → 0.3.4__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.
Potentially problematic release.
This version of kubiya-control-plane-api might be problematic. Click here for more details.
- control_plane_api/README.md +266 -0
- control_plane_api/__init__.py +0 -0
- control_plane_api/__version__.py +1 -0
- control_plane_api/alembic/README +1 -0
- control_plane_api/alembic/env.py +98 -0
- control_plane_api/alembic/script.py.mako +28 -0
- control_plane_api/alembic/versions/1382bec74309_initial_migration_with_all_models.py +251 -0
- control_plane_api/alembic/versions/1f54bc2a37e3_add_analytics_tables.py +162 -0
- control_plane_api/alembic/versions/2e4cb136dc10_rename_toolset_ids_to_skill_ids_in_teams.py +30 -0
- control_plane_api/alembic/versions/31cd69a644ce_add_skill_templates_table.py +28 -0
- control_plane_api/alembic/versions/89e127caa47d_add_jobs_and_job_executions_tables.py +161 -0
- control_plane_api/alembic/versions/add_llm_models_table.py +51 -0
- control_plane_api/alembic/versions/b0e10697f212_add_runtime_column_to_teams_simple.py +42 -0
- control_plane_api/alembic/versions/ce43b24b63bf_add_execution_trigger_source_and_fix_.py +155 -0
- control_plane_api/alembic/versions/d4eaf16e3f8d_rename_toolsets_to_skills.py +84 -0
- control_plane_api/alembic/versions/efa2dc427da1_rename_metadata_to_custom_metadata.py +32 -0
- control_plane_api/alembic/versions/f973b431d1ce_add_workflow_executor_to_skill_types.py +44 -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 +379 -0
- control_plane_api/app/activities/team_activities.py +410 -0
- control_plane_api/app/activities/temporal_cloud_activities.py +577 -0
- control_plane_api/app/config/__init__.py +35 -0
- control_plane_api/app/config/api_config.py +354 -0
- control_plane_api/app/config/model_pricing.py +318 -0
- control_plane_api/app/config.py +95 -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/job_executor.py +312 -0
- control_plane_api/app/lib/kubiya_client.py +235 -0
- control_plane_api/app/lib/litellm_pricing.py +166 -0
- control_plane_api/app/lib/planning_tools/__init__.py +22 -0
- control_plane_api/app/lib/planning_tools/agents.py +155 -0
- control_plane_api/app/lib/planning_tools/base.py +189 -0
- control_plane_api/app/lib/planning_tools/environments.py +214 -0
- control_plane_api/app/lib/planning_tools/resources.py +240 -0
- control_plane_api/app/lib/planning_tools/teams.py +198 -0
- control_plane_api/app/lib/policy_enforcer_client.py +939 -0
- control_plane_api/app/lib/redis_client.py +436 -0
- control_plane_api/app/lib/supabase.py +71 -0
- control_plane_api/app/lib/temporal_client.py +138 -0
- control_plane_api/app/lib/validation/__init__.py +20 -0
- control_plane_api/app/lib/validation/runtime_validation.py +287 -0
- control_plane_api/app/main.py +128 -0
- control_plane_api/app/middleware/__init__.py +8 -0
- control_plane_api/app/middleware/auth.py +513 -0
- control_plane_api/app/middleware/exception_handler.py +267 -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 +27 -0
- control_plane_api/app/models/agent.py +79 -0
- control_plane_api/app/models/analytics.py +206 -0
- control_plane_api/app/models/associations.py +81 -0
- control_plane_api/app/models/environment.py +63 -0
- control_plane_api/app/models/execution.py +93 -0
- control_plane_api/app/models/job.py +179 -0
- control_plane_api/app/models/llm_model.py +75 -0
- control_plane_api/app/models/presence.py +49 -0
- control_plane_api/app/models/project.py +47 -0
- control_plane_api/app/models/session.py +38 -0
- control_plane_api/app/models/team.py +66 -0
- control_plane_api/app/models/workflow.py +55 -0
- control_plane_api/app/policies/README.md +121 -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_restrictions.rego +86 -0
- control_plane_api/app/routers/__init__.py +4 -0
- control_plane_api/app/routers/agents.py +364 -0
- control_plane_api/app/routers/agents_v2.py +1260 -0
- control_plane_api/app/routers/analytics.py +1014 -0
- control_plane_api/app/routers/context_manager.py +562 -0
- control_plane_api/app/routers/environment_context.py +270 -0
- control_plane_api/app/routers/environments.py +715 -0
- control_plane_api/app/routers/execution_environment.py +517 -0
- control_plane_api/app/routers/executions.py +1911 -0
- control_plane_api/app/routers/health.py +92 -0
- control_plane_api/app/routers/health_v2.py +326 -0
- control_plane_api/app/routers/integrations.py +274 -0
- control_plane_api/app/routers/jobs.py +1344 -0
- control_plane_api/app/routers/models.py +82 -0
- control_plane_api/app/routers/models_v2.py +361 -0
- control_plane_api/app/routers/policies.py +639 -0
- control_plane_api/app/routers/presence.py +234 -0
- control_plane_api/app/routers/projects.py +902 -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 +155 -0
- control_plane_api/app/routers/skills.py +1001 -0
- control_plane_api/app/routers/skills_definitions.py +140 -0
- control_plane_api/app/routers/task_planning.py +1256 -0
- control_plane_api/app/routers/task_queues.py +654 -0
- control_plane_api/app/routers/team_context.py +270 -0
- control_plane_api/app/routers/teams.py +1400 -0
- control_plane_api/app/routers/worker_queues.py +1545 -0
- control_plane_api/app/routers/workers.py +935 -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/job_schemas.py +295 -0
- control_plane_api/app/services/__init__.py +1 -0
- control_plane_api/app/services/agno_service.py +619 -0
- control_plane_api/app/services/litellm_service.py +190 -0
- control_plane_api/app/services/policy_service.py +525 -0
- control_plane_api/app/services/temporal_cloud_provisioning.py +150 -0
- control_plane_api/app/skills/__init__.py +44 -0
- control_plane_api/app/skills/base.py +229 -0
- control_plane_api/app/skills/business_intelligence.py +189 -0
- control_plane_api/app/skills/data_visualization.py +154 -0
- control_plane_api/app/skills/docker.py +104 -0
- control_plane_api/app/skills/file_generation.py +94 -0
- control_plane_api/app/skills/file_system.py +110 -0
- control_plane_api/app/skills/python.py +92 -0
- control_plane_api/app/skills/registry.py +65 -0
- control_plane_api/app/skills/shell.py +102 -0
- control_plane_api/app/skills/workflow_executor.py +469 -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 +507 -0
- control_plane_api/app/workflows/agent_execution_with_skills.py +222 -0
- control_plane_api/app/workflows/namespace_provisioning.py +326 -0
- control_plane_api/app/workflows/team_execution.py +399 -0
- control_plane_api/scripts/seed_models.py +239 -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 +1241 -0
- control_plane_api/worker/activities/approval_activities.py +234 -0
- control_plane_api/worker/activities/runtime_activities.py +388 -0
- control_plane_api/worker/activities/skill_activities.py +267 -0
- control_plane_api/worker/activities/team_activities.py +1217 -0
- control_plane_api/worker/config/__init__.py +31 -0
- control_plane_api/worker/config/worker_config.py +275 -0
- control_plane_api/worker/control_plane_client.py +529 -0
- control_plane_api/worker/examples/analytics_integration_example.py +362 -0
- control_plane_api/worker/models/__init__.py +1 -0
- control_plane_api/worker/models/inputs.py +89 -0
- control_plane_api/worker/runtimes/__init__.py +31 -0
- control_plane_api/worker/runtimes/base.py +789 -0
- control_plane_api/worker/runtimes/claude_code_runtime.py +1443 -0
- control_plane_api/worker/runtimes/default_runtime.py +617 -0
- control_plane_api/worker/runtimes/factory.py +173 -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_executor.py +422 -0
- control_plane_api/worker/services/agent_executor_v2.py +383 -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/data_visualization.py +827 -0
- control_plane_api/worker/services/jira_tools.py +257 -0
- control_plane_api/worker/services/runtime_analytics.py +328 -0
- control_plane_api/worker/services/session_service.py +194 -0
- control_plane_api/worker/services/skill_factory.py +175 -0
- control_plane_api/worker/services/team_executor.py +574 -0
- control_plane_api/worker/services/team_executor_v2.py +465 -0
- control_plane_api/worker/services/workflow_executor_tools.py +1418 -0
- control_plane_api/worker/tests/__init__.py +1 -0
- control_plane_api/worker/tests/e2e/__init__.py +0 -0
- control_plane_api/worker/tests/e2e/test_execution_flow.py +571 -0
- control_plane_api/worker/tests/integration/__init__.py +0 -0
- control_plane_api/worker/tests/integration/test_control_plane_integration.py +308 -0
- control_plane_api/worker/tests/unit/__init__.py +0 -0
- control_plane_api/worker/tests/unit/test_control_plane_client.py +401 -0
- control_plane_api/worker/utils/__init__.py +1 -0
- control_plane_api/worker/utils/chunk_batcher.py +305 -0
- control_plane_api/worker/utils/retry_utils.py +60 -0
- control_plane_api/worker/utils/streaming_utils.py +373 -0
- control_plane_api/worker/worker.py +753 -0
- control_plane_api/worker/workflows/__init__.py +0 -0
- control_plane_api/worker/workflows/agent_execution.py +589 -0
- control_plane_api/worker/workflows/team_execution.py +429 -0
- kubiya_control_plane_api-0.3.4.dist-info/METADATA +229 -0
- kubiya_control_plane_api-0.3.4.dist-info/RECORD +182 -0
- kubiya_control_plane_api-0.3.4.dist-info/entry_points.txt +2 -0
- kubiya_control_plane_api-0.3.4.dist-info/top_level.txt +1 -0
- kubiya_control_plane_api-0.1.0.dist-info/METADATA +0 -66
- kubiya_control_plane_api-0.1.0.dist-info/RECORD +0 -5
- kubiya_control_plane_api-0.1.0.dist-info/top_level.txt +0 -1
- {kubiya_control_plane_api-0.1.0.dist-info/licenses → control_plane_api}/LICENSE +0 -0
- {kubiya_control_plane_api-0.1.0.dist-info → kubiya_control_plane_api-0.3.4.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,617 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Default runtime implementation using Agno framework.
|
|
3
|
+
|
|
4
|
+
This runtime adapter wraps the existing Agno-based agent execution logic,
|
|
5
|
+
providing a clean interface that conforms to the AgentRuntime protocol.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Dict, Any, Optional, AsyncIterator, Callable, TYPE_CHECKING
|
|
9
|
+
import structlog
|
|
10
|
+
import asyncio
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
from agno.agent import Agent
|
|
14
|
+
from agno.models.litellm import LiteLLM
|
|
15
|
+
|
|
16
|
+
from .base import (
|
|
17
|
+
RuntimeType,
|
|
18
|
+
RuntimeExecutionResult,
|
|
19
|
+
RuntimeExecutionContext,
|
|
20
|
+
RuntimeCapabilities,
|
|
21
|
+
BaseRuntime,
|
|
22
|
+
RuntimeRegistry,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from control_plane_client import ControlPlaneClient
|
|
27
|
+
from services.cancellation_manager import CancellationManager
|
|
28
|
+
|
|
29
|
+
logger = structlog.get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@RuntimeRegistry.register(RuntimeType.DEFAULT)
|
|
33
|
+
class DefaultRuntime(BaseRuntime):
|
|
34
|
+
"""
|
|
35
|
+
Runtime implementation using Agno framework.
|
|
36
|
+
|
|
37
|
+
This is the default runtime that wraps the existing Agno-based
|
|
38
|
+
agent execution logic. It maintains backward compatibility while
|
|
39
|
+
conforming to the new AgentRuntime interface.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
control_plane_client: "ControlPlaneClient",
|
|
45
|
+
cancellation_manager: "CancellationManager",
|
|
46
|
+
**kwargs,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Initialize the Agno runtime.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
control_plane_client: Client for Control Plane API
|
|
53
|
+
cancellation_manager: Manager for execution cancellation
|
|
54
|
+
**kwargs: Additional configuration options
|
|
55
|
+
"""
|
|
56
|
+
super().__init__(control_plane_client, cancellation_manager, **kwargs)
|
|
57
|
+
|
|
58
|
+
def get_runtime_type(self) -> RuntimeType:
|
|
59
|
+
"""Return RuntimeType.DEFAULT."""
|
|
60
|
+
return RuntimeType.DEFAULT
|
|
61
|
+
|
|
62
|
+
def get_capabilities(self) -> RuntimeCapabilities:
|
|
63
|
+
"""Return Agno runtime capabilities."""
|
|
64
|
+
return RuntimeCapabilities(
|
|
65
|
+
streaming=True,
|
|
66
|
+
tools=True,
|
|
67
|
+
mcp=False,
|
|
68
|
+
hooks=True,
|
|
69
|
+
cancellation=True,
|
|
70
|
+
conversation_history=True,
|
|
71
|
+
custom_tools=False
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
async def _execute_impl(
|
|
75
|
+
self, context: RuntimeExecutionContext
|
|
76
|
+
) -> RuntimeExecutionResult:
|
|
77
|
+
"""
|
|
78
|
+
Execute agent using Agno framework without streaming.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
context: Execution context with prompt, history, config
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
RuntimeExecutionResult with response and metadata
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
# Create Agno agent
|
|
88
|
+
agent = self._create_agno_agent(context)
|
|
89
|
+
|
|
90
|
+
# Register for cancellation
|
|
91
|
+
self.cancellation_manager.register(
|
|
92
|
+
execution_id=context.execution_id,
|
|
93
|
+
instance=agent,
|
|
94
|
+
instance_type="agent",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Build conversation context
|
|
98
|
+
messages = self._build_conversation_messages(context)
|
|
99
|
+
|
|
100
|
+
# Execute without streaming
|
|
101
|
+
def run_agent():
|
|
102
|
+
if messages:
|
|
103
|
+
return agent.run(context.prompt, stream=False, messages=messages)
|
|
104
|
+
else:
|
|
105
|
+
return agent.run(context.prompt, stream=False)
|
|
106
|
+
|
|
107
|
+
# Run in thread pool to avoid blocking
|
|
108
|
+
result = await asyncio.to_thread(run_agent)
|
|
109
|
+
|
|
110
|
+
# Cleanup
|
|
111
|
+
self.cancellation_manager.unregister(context.execution_id)
|
|
112
|
+
|
|
113
|
+
# Extract response and metadata
|
|
114
|
+
response_content = (
|
|
115
|
+
result.content if hasattr(result, "content") else str(result)
|
|
116
|
+
)
|
|
117
|
+
usage = self._extract_usage(result)
|
|
118
|
+
tool_messages = self._extract_tool_messages(result)
|
|
119
|
+
|
|
120
|
+
return RuntimeExecutionResult(
|
|
121
|
+
response=response_content,
|
|
122
|
+
usage=usage,
|
|
123
|
+
success=True,
|
|
124
|
+
finish_reason="stop",
|
|
125
|
+
run_id=getattr(result, "run_id", None),
|
|
126
|
+
model=context.model_id,
|
|
127
|
+
tool_messages=tool_messages,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
except asyncio.CancelledError:
|
|
131
|
+
# Handle cancellation
|
|
132
|
+
self.cancellation_manager.cancel(context.execution_id)
|
|
133
|
+
self.cancellation_manager.unregister(context.execution_id)
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
self.logger.error(
|
|
138
|
+
"Agno execution failed",
|
|
139
|
+
execution_id=context.execution_id,
|
|
140
|
+
error=str(e),
|
|
141
|
+
)
|
|
142
|
+
self.cancellation_manager.unregister(context.execution_id)
|
|
143
|
+
|
|
144
|
+
return RuntimeExecutionResult(
|
|
145
|
+
response="",
|
|
146
|
+
usage={},
|
|
147
|
+
success=False,
|
|
148
|
+
error=str(e),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
async def _stream_execute_impl(
|
|
152
|
+
self,
|
|
153
|
+
context: RuntimeExecutionContext,
|
|
154
|
+
event_callback: Optional[Callable[[Dict], None]] = None,
|
|
155
|
+
) -> AsyncIterator[RuntimeExecutionResult]:
|
|
156
|
+
"""
|
|
157
|
+
Execute agent with streaming using Agno framework.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
context: Execution context
|
|
161
|
+
event_callback: Optional callback for real-time events
|
|
162
|
+
|
|
163
|
+
Yields:
|
|
164
|
+
RuntimeExecutionResult chunks as they arrive in real-time
|
|
165
|
+
"""
|
|
166
|
+
try:
|
|
167
|
+
# Build conversation context
|
|
168
|
+
messages = self._build_conversation_messages(context)
|
|
169
|
+
|
|
170
|
+
# Stream execution - publish events INSIDE the thread (like old code)
|
|
171
|
+
accumulated_response = ""
|
|
172
|
+
run_result = None
|
|
173
|
+
import time
|
|
174
|
+
import queue
|
|
175
|
+
|
|
176
|
+
# Create queue for streaming chunks from thread to async
|
|
177
|
+
chunk_queue = queue.Queue()
|
|
178
|
+
|
|
179
|
+
# Generate unique message ID
|
|
180
|
+
message_id = f"{context.execution_id}_msg_{int(time.time() * 1000000)}"
|
|
181
|
+
|
|
182
|
+
# Create tool hook that publishes directly to Control Plane
|
|
183
|
+
def tool_hook(name: str = None, function_name: str = None, function=None, arguments: dict = None, **kwargs):
|
|
184
|
+
"""Hook to capture tool execution for real-time streaming"""
|
|
185
|
+
tool_name = name or function_name or "unknown"
|
|
186
|
+
tool_args = arguments or {}
|
|
187
|
+
tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
|
|
188
|
+
|
|
189
|
+
# Publish tool start event (blocking call - OK in thread)
|
|
190
|
+
self.control_plane.publish_event(
|
|
191
|
+
execution_id=context.execution_id,
|
|
192
|
+
event_type="tool_started",
|
|
193
|
+
data={
|
|
194
|
+
"tool_name": tool_name,
|
|
195
|
+
"tool_execution_id": tool_execution_id,
|
|
196
|
+
"tool_arguments": tool_args,
|
|
197
|
+
"message": f"🔧 Executing tool: {tool_name}",
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Execute tool
|
|
202
|
+
result = None
|
|
203
|
+
error = None
|
|
204
|
+
try:
|
|
205
|
+
if function and callable(function):
|
|
206
|
+
result = function(**tool_args) if tool_args else function()
|
|
207
|
+
else:
|
|
208
|
+
raise ValueError(f"Function not callable: {function}")
|
|
209
|
+
status = "success"
|
|
210
|
+
except Exception as e:
|
|
211
|
+
error = e
|
|
212
|
+
status = "failed"
|
|
213
|
+
|
|
214
|
+
# Publish tool completion event
|
|
215
|
+
self.control_plane.publish_event(
|
|
216
|
+
execution_id=context.execution_id,
|
|
217
|
+
event_type="tool_completed",
|
|
218
|
+
data={
|
|
219
|
+
"tool_name": tool_name,
|
|
220
|
+
"tool_execution_id": tool_execution_id,
|
|
221
|
+
"status": status,
|
|
222
|
+
"tool_output": str(result)[:1000] if result else None,
|
|
223
|
+
"tool_error": str(error) if error else None,
|
|
224
|
+
"message": f"{'✅' if status == 'success' else '❌'} Tool {status}: {tool_name}",
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
if error:
|
|
229
|
+
raise error
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
# Create Agno agent with tool hooks
|
|
233
|
+
agent = self._create_agno_agent_with_hooks(context, tool_hook)
|
|
234
|
+
|
|
235
|
+
# Register for cancellation
|
|
236
|
+
self.cancellation_manager.register(
|
|
237
|
+
execution_id=context.execution_id,
|
|
238
|
+
instance=agent,
|
|
239
|
+
instance_type="agent",
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
# Cache execution metadata
|
|
243
|
+
self.control_plane.cache_metadata(context.execution_id, "AGENT")
|
|
244
|
+
|
|
245
|
+
def stream_agent_run():
|
|
246
|
+
"""
|
|
247
|
+
Run agent with streaming and publish events directly to Control Plane.
|
|
248
|
+
This runs in a thread pool, so blocking HTTP calls are OK here.
|
|
249
|
+
Put chunks in queue for async iterator to yield in real-time.
|
|
250
|
+
"""
|
|
251
|
+
nonlocal accumulated_response, run_result
|
|
252
|
+
run_id_published = False
|
|
253
|
+
|
|
254
|
+
try:
|
|
255
|
+
if messages:
|
|
256
|
+
stream_response = agent.run(
|
|
257
|
+
context.prompt,
|
|
258
|
+
stream=True,
|
|
259
|
+
messages=messages,
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
stream_response = agent.run(context.prompt, stream=True)
|
|
263
|
+
|
|
264
|
+
# Iterate over streaming chunks and publish IMMEDIATELY
|
|
265
|
+
for chunk in stream_response:
|
|
266
|
+
# Capture run_id for cancellation (first chunk)
|
|
267
|
+
if not run_id_published and hasattr(chunk, "run_id") and chunk.run_id:
|
|
268
|
+
self.cancellation_manager.set_run_id(
|
|
269
|
+
context.execution_id, chunk.run_id
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Publish run_id event
|
|
273
|
+
self.control_plane.publish_event(
|
|
274
|
+
execution_id=context.execution_id,
|
|
275
|
+
event_type="run_started",
|
|
276
|
+
data={
|
|
277
|
+
"run_id": chunk.run_id,
|
|
278
|
+
"execution_id": context.execution_id,
|
|
279
|
+
"cancellable": True,
|
|
280
|
+
}
|
|
281
|
+
)
|
|
282
|
+
run_id_published = True
|
|
283
|
+
|
|
284
|
+
# Extract content
|
|
285
|
+
chunk_content = ""
|
|
286
|
+
if hasattr(chunk, "content") and chunk.content:
|
|
287
|
+
if isinstance(chunk.content, str):
|
|
288
|
+
chunk_content = chunk.content
|
|
289
|
+
elif hasattr(chunk.content, "text"):
|
|
290
|
+
chunk_content = chunk.content.text
|
|
291
|
+
|
|
292
|
+
if chunk_content:
|
|
293
|
+
accumulated_response += chunk_content
|
|
294
|
+
|
|
295
|
+
# IMMEDIATELY publish to Control Plane (blocking call in thread - this is OK!)
|
|
296
|
+
self.control_plane.publish_event(
|
|
297
|
+
execution_id=context.execution_id,
|
|
298
|
+
event_type="message_chunk",
|
|
299
|
+
data={
|
|
300
|
+
"role": "assistant",
|
|
301
|
+
"content": chunk_content,
|
|
302
|
+
"is_chunk": True,
|
|
303
|
+
"message_id": message_id,
|
|
304
|
+
}
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Put chunk in queue for async iterator to yield
|
|
308
|
+
chunk_queue.put(("chunk", chunk_content))
|
|
309
|
+
|
|
310
|
+
# Store final result
|
|
311
|
+
run_result = stream_response
|
|
312
|
+
|
|
313
|
+
# Signal completion
|
|
314
|
+
chunk_queue.put(("done", run_result))
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
self.logger.error("Streaming error", error=str(e))
|
|
318
|
+
chunk_queue.put(("error", e))
|
|
319
|
+
raise
|
|
320
|
+
|
|
321
|
+
# Start streaming in background thread
|
|
322
|
+
import threading
|
|
323
|
+
stream_thread = threading.Thread(target=stream_agent_run, daemon=True)
|
|
324
|
+
stream_thread.start()
|
|
325
|
+
|
|
326
|
+
# Yield chunks as they arrive in the queue
|
|
327
|
+
while True:
|
|
328
|
+
try:
|
|
329
|
+
# Non-blocking get with short timeout for responsiveness
|
|
330
|
+
item_type, item_data = await asyncio.to_thread(chunk_queue.get, timeout=0.1)
|
|
331
|
+
|
|
332
|
+
if item_type == "chunk":
|
|
333
|
+
# Yield chunk immediately
|
|
334
|
+
yield RuntimeExecutionResult(
|
|
335
|
+
response=item_data,
|
|
336
|
+
usage={},
|
|
337
|
+
success=True,
|
|
338
|
+
)
|
|
339
|
+
elif item_type == "done":
|
|
340
|
+
# Final result - extract metadata and break
|
|
341
|
+
run_result = item_data
|
|
342
|
+
break
|
|
343
|
+
elif item_type == "error":
|
|
344
|
+
# Error occurred in thread
|
|
345
|
+
raise item_data
|
|
346
|
+
|
|
347
|
+
except queue.Empty:
|
|
348
|
+
# Queue empty, check if thread is still alive
|
|
349
|
+
if not stream_thread.is_alive():
|
|
350
|
+
# Thread died without putting "done" - something went wrong
|
|
351
|
+
break
|
|
352
|
+
# Thread still running, continue waiting
|
|
353
|
+
continue
|
|
354
|
+
|
|
355
|
+
# Wait for thread to complete
|
|
356
|
+
await asyncio.to_thread(stream_thread.join, timeout=5.0)
|
|
357
|
+
|
|
358
|
+
# Yield final result with complete metadata
|
|
359
|
+
usage = self._extract_usage(run_result) if run_result else {}
|
|
360
|
+
tool_messages = (
|
|
361
|
+
self._extract_tool_messages(run_result) if run_result else []
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
yield RuntimeExecutionResult(
|
|
365
|
+
response=accumulated_response, # Full accumulated response
|
|
366
|
+
usage=usage,
|
|
367
|
+
success=True,
|
|
368
|
+
finish_reason="stop",
|
|
369
|
+
run_id=getattr(run_result, "run_id", None) if run_result else None,
|
|
370
|
+
model=context.model_id,
|
|
371
|
+
tool_messages=tool_messages,
|
|
372
|
+
metadata={"accumulated_response": accumulated_response},
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
finally:
|
|
376
|
+
# Cleanup
|
|
377
|
+
self.cancellation_manager.unregister(context.execution_id)
|
|
378
|
+
|
|
379
|
+
# Private helper methods
|
|
380
|
+
|
|
381
|
+
def _create_agno_agent_with_hooks(
|
|
382
|
+
self,
|
|
383
|
+
context: RuntimeExecutionContext,
|
|
384
|
+
tool_hook: Callable,
|
|
385
|
+
) -> Agent:
|
|
386
|
+
"""
|
|
387
|
+
Create Agno Agent instance with tool hooks.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
context: Execution context
|
|
391
|
+
tool_hook: Tool hook function for real-time events
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
Configured Agno Agent instance
|
|
395
|
+
"""
|
|
396
|
+
# Get LiteLLM configuration
|
|
397
|
+
litellm_api_base = os.getenv(
|
|
398
|
+
"LITELLM_API_BASE", "https://llm-proxy.kubiya.ai"
|
|
399
|
+
)
|
|
400
|
+
litellm_api_key = os.getenv("LITELLM_API_KEY")
|
|
401
|
+
|
|
402
|
+
if not litellm_api_key:
|
|
403
|
+
raise ValueError("LITELLM_API_KEY environment variable not set")
|
|
404
|
+
|
|
405
|
+
model = context.model_id or os.environ.get(
|
|
406
|
+
"LITELLM_DEFAULT_MODEL", "kubiya/claude-sonnet-4"
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
# Create agent with tool hooks
|
|
410
|
+
agent = Agent(
|
|
411
|
+
name=f"Agent {context.agent_id}",
|
|
412
|
+
role=context.system_prompt or "You are a helpful AI assistant",
|
|
413
|
+
model=LiteLLM(
|
|
414
|
+
id=f"openai/{model}",
|
|
415
|
+
api_base=litellm_api_base,
|
|
416
|
+
api_key=litellm_api_key,
|
|
417
|
+
),
|
|
418
|
+
tools=context.skills if context.skills else None,
|
|
419
|
+
tool_hooks=[tool_hook], # Add hook for real-time tool updates
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
return agent
|
|
423
|
+
|
|
424
|
+
def _create_agno_agent(
|
|
425
|
+
self,
|
|
426
|
+
context: RuntimeExecutionContext,
|
|
427
|
+
event_callback: Optional[Callable] = None,
|
|
428
|
+
) -> Agent:
|
|
429
|
+
"""
|
|
430
|
+
Create Agno Agent instance.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
context: Execution context
|
|
434
|
+
event_callback: Optional callback for tool execution events
|
|
435
|
+
|
|
436
|
+
Returns:
|
|
437
|
+
Configured Agno Agent instance
|
|
438
|
+
"""
|
|
439
|
+
# Get LiteLLM configuration
|
|
440
|
+
litellm_api_base = os.getenv(
|
|
441
|
+
"LITELLM_API_BASE", "https://llm-proxy.kubiya.ai"
|
|
442
|
+
)
|
|
443
|
+
litellm_api_key = os.getenv("LITELLM_API_KEY")
|
|
444
|
+
|
|
445
|
+
if not litellm_api_key:
|
|
446
|
+
raise ValueError("LITELLM_API_KEY environment variable not set")
|
|
447
|
+
|
|
448
|
+
model = context.model_id or os.environ.get(
|
|
449
|
+
"LITELLM_DEFAULT_MODEL", "kubiya/claude-sonnet-4"
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
# Create tool hooks if event_callback provided
|
|
453
|
+
tool_hooks = []
|
|
454
|
+
if event_callback:
|
|
455
|
+
tool_hooks.append(self._create_tool_hook(context.execution_id, event_callback))
|
|
456
|
+
|
|
457
|
+
# Create agent
|
|
458
|
+
agent = Agent(
|
|
459
|
+
name=f"Agent {context.agent_id}",
|
|
460
|
+
role=context.system_prompt or "You are a helpful AI assistant",
|
|
461
|
+
model=LiteLLM(
|
|
462
|
+
id=f"openai/{model}",
|
|
463
|
+
api_base=litellm_api_base,
|
|
464
|
+
api_key=litellm_api_key,
|
|
465
|
+
),
|
|
466
|
+
tools=context.skills if context.skills else None,
|
|
467
|
+
tool_hooks=tool_hooks if tool_hooks else None,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return agent
|
|
471
|
+
|
|
472
|
+
def _create_tool_hook(
|
|
473
|
+
self, execution_id: str, event_callback: Callable
|
|
474
|
+
) -> Callable:
|
|
475
|
+
"""
|
|
476
|
+
Create a tool hook for capturing tool execution events.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
execution_id: Execution ID
|
|
480
|
+
event_callback: Callback to publish events
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Tool hook function
|
|
484
|
+
"""
|
|
485
|
+
|
|
486
|
+
def tool_hook(
|
|
487
|
+
name: str = None,
|
|
488
|
+
function_name: str = None,
|
|
489
|
+
function=None,
|
|
490
|
+
arguments: dict = None,
|
|
491
|
+
**kwargs,
|
|
492
|
+
):
|
|
493
|
+
"""Hook to capture tool execution for real-time streaming"""
|
|
494
|
+
import time
|
|
495
|
+
|
|
496
|
+
tool_name = name or function_name or "unknown"
|
|
497
|
+
tool_args = arguments or {}
|
|
498
|
+
tool_execution_id = f"{tool_name}_{int(time.time() * 1000000)}"
|
|
499
|
+
|
|
500
|
+
# Publish tool start event
|
|
501
|
+
event_callback(
|
|
502
|
+
{
|
|
503
|
+
"type": "tool_start",
|
|
504
|
+
"tool_name": tool_name,
|
|
505
|
+
"tool_execution_id": tool_execution_id,
|
|
506
|
+
"tool_args": tool_args,
|
|
507
|
+
"execution_id": execution_id,
|
|
508
|
+
}
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Execute tool
|
|
512
|
+
result = None
|
|
513
|
+
error = None
|
|
514
|
+
try:
|
|
515
|
+
if function and callable(function):
|
|
516
|
+
result = function(**tool_args) if tool_args else function()
|
|
517
|
+
else:
|
|
518
|
+
raise ValueError(f"Function not callable: {function}")
|
|
519
|
+
|
|
520
|
+
status = "success"
|
|
521
|
+
|
|
522
|
+
except Exception as e:
|
|
523
|
+
error = e
|
|
524
|
+
status = "failed"
|
|
525
|
+
|
|
526
|
+
# Publish tool completion event
|
|
527
|
+
event_callback(
|
|
528
|
+
{
|
|
529
|
+
"type": "tool_complete",
|
|
530
|
+
"tool_name": tool_name,
|
|
531
|
+
"tool_execution_id": tool_execution_id,
|
|
532
|
+
"status": status,
|
|
533
|
+
"output": str(result)[:1000] if result else None,
|
|
534
|
+
"error": str(error) if error else None,
|
|
535
|
+
"execution_id": execution_id,
|
|
536
|
+
}
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
if error:
|
|
540
|
+
raise error
|
|
541
|
+
|
|
542
|
+
return result
|
|
543
|
+
|
|
544
|
+
return tool_hook
|
|
545
|
+
|
|
546
|
+
def _build_conversation_messages(
|
|
547
|
+
self, context: RuntimeExecutionContext
|
|
548
|
+
) -> list:
|
|
549
|
+
"""
|
|
550
|
+
Build conversation messages for Agno from context history.
|
|
551
|
+
|
|
552
|
+
Args:
|
|
553
|
+
context: Execution context with history
|
|
554
|
+
|
|
555
|
+
Returns:
|
|
556
|
+
List of message dicts for Agno
|
|
557
|
+
"""
|
|
558
|
+
if not context.conversation_history:
|
|
559
|
+
return []
|
|
560
|
+
|
|
561
|
+
# Convert to Agno message format
|
|
562
|
+
messages = []
|
|
563
|
+
for msg in context.conversation_history:
|
|
564
|
+
role = msg.get("role", "user")
|
|
565
|
+
content = msg.get("content", "")
|
|
566
|
+
|
|
567
|
+
# Agno uses 'user' and 'assistant' roles
|
|
568
|
+
if role in ["user", "assistant"]:
|
|
569
|
+
messages.append({"role": role, "content": content})
|
|
570
|
+
|
|
571
|
+
return messages
|
|
572
|
+
|
|
573
|
+
def _extract_usage(self, result: Any) -> Dict[str, Any]:
|
|
574
|
+
"""
|
|
575
|
+
Extract usage metrics from Agno result.
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
result: Agno run result
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
Usage metrics dict
|
|
582
|
+
"""
|
|
583
|
+
usage = {}
|
|
584
|
+
if hasattr(result, "metrics") and result.metrics:
|
|
585
|
+
metrics = result.metrics
|
|
586
|
+
usage = {
|
|
587
|
+
"prompt_tokens": getattr(metrics, "input_tokens", 0),
|
|
588
|
+
"completion_tokens": getattr(metrics, "output_tokens", 0),
|
|
589
|
+
"total_tokens": getattr(metrics, "total_tokens", 0),
|
|
590
|
+
}
|
|
591
|
+
return usage
|
|
592
|
+
|
|
593
|
+
def _extract_tool_messages(self, result: Any) -> list:
|
|
594
|
+
"""
|
|
595
|
+
Extract tool messages from Agno result.
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
result: Agno run result
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
List of tool message dicts
|
|
602
|
+
"""
|
|
603
|
+
tool_messages = []
|
|
604
|
+
|
|
605
|
+
# Check if result has messages attribute
|
|
606
|
+
if hasattr(result, "messages") and result.messages:
|
|
607
|
+
for msg in result.messages:
|
|
608
|
+
if hasattr(msg, "role") and msg.role == "tool":
|
|
609
|
+
tool_messages.append(
|
|
610
|
+
{
|
|
611
|
+
"role": "tool",
|
|
612
|
+
"content": getattr(msg, "content", ""),
|
|
613
|
+
"tool_use_id": getattr(msg, "tool_use_id", None),
|
|
614
|
+
}
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
return tool_messages
|