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
|
File without changes
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"""Agent execution workflow for Temporal"""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from typing import Optional, List, Dict, Any
|
|
6
|
+
from temporalio import workflow
|
|
7
|
+
import asyncio
|
|
8
|
+
|
|
9
|
+
with workflow.unsafe.imports_passed_through():
|
|
10
|
+
from control_plane_api.worker.activities.agent_activities import (
|
|
11
|
+
execute_agent_llm,
|
|
12
|
+
update_execution_status,
|
|
13
|
+
update_agent_status,
|
|
14
|
+
persist_conversation_history,
|
|
15
|
+
ActivityExecuteAgentInput,
|
|
16
|
+
ActivityUpdateExecutionInput,
|
|
17
|
+
ActivityUpdateAgentInput,
|
|
18
|
+
ActivityPersistConversationInput,
|
|
19
|
+
)
|
|
20
|
+
from control_plane_api.worker.activities.runtime_activities import (
|
|
21
|
+
execute_with_runtime,
|
|
22
|
+
ActivityRuntimeExecuteInput,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class AgentExecutionInput:
|
|
28
|
+
"""Input for agent execution workflow"""
|
|
29
|
+
execution_id: str
|
|
30
|
+
agent_id: str
|
|
31
|
+
organization_id: str
|
|
32
|
+
prompt: str
|
|
33
|
+
system_prompt: Optional[str] = None
|
|
34
|
+
model_id: Optional[str] = None
|
|
35
|
+
model_config: dict = None
|
|
36
|
+
agent_config: dict = None
|
|
37
|
+
mcp_servers: dict = None # MCP servers configuration
|
|
38
|
+
user_metadata: dict = None
|
|
39
|
+
runtime_type: str = "default" # "default" (Agno) or "claude_code"
|
|
40
|
+
|
|
41
|
+
def __post_init__(self):
|
|
42
|
+
if self.model_config is None:
|
|
43
|
+
self.model_config = {}
|
|
44
|
+
if self.agent_config is None:
|
|
45
|
+
self.agent_config = {}
|
|
46
|
+
if self.mcp_servers is None:
|
|
47
|
+
self.mcp_servers = {}
|
|
48
|
+
if self.user_metadata is None:
|
|
49
|
+
self.user_metadata = {}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class TeamExecutionInput:
|
|
54
|
+
"""Input for team execution workflow (uses same workflow as agent)"""
|
|
55
|
+
execution_id: str
|
|
56
|
+
team_id: str
|
|
57
|
+
organization_id: str
|
|
58
|
+
prompt: str
|
|
59
|
+
system_prompt: Optional[str] = None
|
|
60
|
+
model_id: Optional[str] = None
|
|
61
|
+
model_config: dict = None
|
|
62
|
+
team_config: dict = None
|
|
63
|
+
mcp_servers: dict = None # MCP servers configuration
|
|
64
|
+
user_metadata: dict = None
|
|
65
|
+
runtime_type: str = "default" # "default" (Agno) or "claude_code"
|
|
66
|
+
|
|
67
|
+
def __post_init__(self):
|
|
68
|
+
if self.model_config is None:
|
|
69
|
+
self.model_config = {}
|
|
70
|
+
if self.team_config is None:
|
|
71
|
+
self.team_config = {}
|
|
72
|
+
if self.mcp_servers is None:
|
|
73
|
+
self.mcp_servers = {}
|
|
74
|
+
if self.user_metadata is None:
|
|
75
|
+
self.user_metadata = {}
|
|
76
|
+
|
|
77
|
+
def to_agent_input(self) -> AgentExecutionInput:
|
|
78
|
+
"""Convert TeamExecutionInput to AgentExecutionInput for workflow reuse"""
|
|
79
|
+
return AgentExecutionInput(
|
|
80
|
+
execution_id=self.execution_id,
|
|
81
|
+
agent_id=self.team_id, # Use team_id as agent_id
|
|
82
|
+
organization_id=self.organization_id,
|
|
83
|
+
prompt=self.prompt,
|
|
84
|
+
system_prompt=self.system_prompt,
|
|
85
|
+
model_id=self.model_id,
|
|
86
|
+
model_config=self.model_config,
|
|
87
|
+
agent_config=self.team_config,
|
|
88
|
+
mcp_servers=self.mcp_servers,
|
|
89
|
+
user_metadata=self.user_metadata,
|
|
90
|
+
runtime_type=self.runtime_type,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ChatMessage:
|
|
96
|
+
"""Represents a message in the conversation"""
|
|
97
|
+
role: str # "user", "assistant", "system", "tool"
|
|
98
|
+
content: str
|
|
99
|
+
timestamp: str
|
|
100
|
+
tool_name: Optional[str] = None
|
|
101
|
+
tool_input: Optional[Dict[str, Any]] = None
|
|
102
|
+
tool_output: Optional[Dict[str, Any]] = None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class ExecutionState:
|
|
107
|
+
"""Current state of the execution for queries"""
|
|
108
|
+
status: str # "pending", "running", "waiting_for_input", "completed", "failed"
|
|
109
|
+
messages: List[ChatMessage] = field(default_factory=list)
|
|
110
|
+
current_response: str = ""
|
|
111
|
+
error_message: Optional[str] = None
|
|
112
|
+
usage: Dict[str, Any] = field(default_factory=dict)
|
|
113
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
114
|
+
is_waiting_for_input: bool = False
|
|
115
|
+
should_complete: bool = False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@workflow.defn
|
|
119
|
+
class AgentExecutionWorkflow:
|
|
120
|
+
"""
|
|
121
|
+
Workflow for executing an agent with LLM with Temporal message passing support.
|
|
122
|
+
|
|
123
|
+
This workflow:
|
|
124
|
+
1. Updates execution status to running
|
|
125
|
+
2. Executes the agent's LLM call
|
|
126
|
+
3. Updates execution with results
|
|
127
|
+
4. Updates agent status
|
|
128
|
+
5. Supports queries for real-time state access
|
|
129
|
+
6. Supports signals for adding followup messages
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(self) -> None:
|
|
133
|
+
"""Initialize workflow state"""
|
|
134
|
+
self._state = ExecutionState(status="pending")
|
|
135
|
+
self._lock = asyncio.Lock()
|
|
136
|
+
self._new_message_count = 0
|
|
137
|
+
self._processed_message_count = 0
|
|
138
|
+
|
|
139
|
+
def _messages_to_dict(self, messages: List[ChatMessage]) -> List[Dict[str, Any]]:
|
|
140
|
+
"""
|
|
141
|
+
Convert ChatMessage objects to dict format for persistence.
|
|
142
|
+
|
|
143
|
+
This ensures the conversation history is in a clean, serializable format
|
|
144
|
+
that can be stored in the database and retrieved later.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
messages: List of ChatMessage objects from workflow state
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
List of message dicts ready for persistence
|
|
151
|
+
"""
|
|
152
|
+
return [
|
|
153
|
+
{
|
|
154
|
+
"role": msg.role,
|
|
155
|
+
"content": msg.content,
|
|
156
|
+
"timestamp": msg.timestamp,
|
|
157
|
+
"tool_name": msg.tool_name,
|
|
158
|
+
"tool_input": msg.tool_input,
|
|
159
|
+
"tool_output": msg.tool_output,
|
|
160
|
+
}
|
|
161
|
+
for msg in messages
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
@workflow.query
|
|
165
|
+
def get_state(self) -> ExecutionState:
|
|
166
|
+
"""Query handler: Get current execution state including messages and status"""
|
|
167
|
+
return self._state
|
|
168
|
+
|
|
169
|
+
@workflow.signal
|
|
170
|
+
async def add_message(self, message: ChatMessage) -> None:
|
|
171
|
+
"""
|
|
172
|
+
Signal handler: Add a message to the conversation.
|
|
173
|
+
This allows clients to send followup messages while the workflow is running.
|
|
174
|
+
The workflow will wake up and process this message.
|
|
175
|
+
"""
|
|
176
|
+
async with self._lock:
|
|
177
|
+
self._state.messages.append(message)
|
|
178
|
+
self._new_message_count += 1
|
|
179
|
+
self._state.is_waiting_for_input = False
|
|
180
|
+
workflow.logger.info(
|
|
181
|
+
f"Message added to conversation",
|
|
182
|
+
extra={
|
|
183
|
+
"role": message.role,
|
|
184
|
+
"content_preview": message.content[:100] if message.content else "",
|
|
185
|
+
"total_messages": len(self._state.messages)
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
@workflow.signal
|
|
190
|
+
async def mark_as_done(self) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Signal handler: Mark the workflow as complete.
|
|
193
|
+
This signals that the user is done with the conversation and the workflow should complete.
|
|
194
|
+
"""
|
|
195
|
+
async with self._lock:
|
|
196
|
+
self._state.should_complete = True
|
|
197
|
+
self._state.is_waiting_for_input = False
|
|
198
|
+
workflow.logger.info("Workflow marked as done by user")
|
|
199
|
+
|
|
200
|
+
@workflow.run
|
|
201
|
+
async def run(self, input: AgentExecutionInput) -> dict:
|
|
202
|
+
"""
|
|
203
|
+
Run the agent execution workflow with Human-in-the-Loop (HITL) pattern.
|
|
204
|
+
|
|
205
|
+
This workflow implements a continuous conversation loop:
|
|
206
|
+
1. Process the initial user message
|
|
207
|
+
2. Execute LLM and return response
|
|
208
|
+
3. Wait for user input (signals)
|
|
209
|
+
4. Process followup messages in a loop
|
|
210
|
+
5. Only complete when user explicitly marks as done
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
input: Workflow input with execution details
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Execution result dict with response, usage, etc.
|
|
217
|
+
"""
|
|
218
|
+
workflow.logger.info(
|
|
219
|
+
f"Starting agent execution workflow with HITL pattern",
|
|
220
|
+
extra={
|
|
221
|
+
"execution_id": input.execution_id,
|
|
222
|
+
"agent_id": input.agent_id,
|
|
223
|
+
"organization_id": input.organization_id,
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Initialize state with user's initial message
|
|
228
|
+
self._state.messages.append(ChatMessage(
|
|
229
|
+
role="user",
|
|
230
|
+
content=input.prompt,
|
|
231
|
+
timestamp=workflow.now().isoformat(),
|
|
232
|
+
))
|
|
233
|
+
self._state.status = "running"
|
|
234
|
+
self._new_message_count = 1 # Initial message counts as a new message
|
|
235
|
+
self._processed_message_count = 0 # No messages processed yet (no response)
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
# Step 1: Update execution status to running
|
|
239
|
+
await workflow.execute_activity(
|
|
240
|
+
update_execution_status,
|
|
241
|
+
ActivityUpdateExecutionInput(
|
|
242
|
+
execution_id=input.execution_id,
|
|
243
|
+
status="running",
|
|
244
|
+
started_at=workflow.now().isoformat(),
|
|
245
|
+
execution_metadata={
|
|
246
|
+
"workflow_started": True,
|
|
247
|
+
"has_mcp_servers": bool(input.mcp_servers),
|
|
248
|
+
"mcp_server_count": len(input.mcp_servers) if input.mcp_servers else 0,
|
|
249
|
+
"hitl_enabled": True,
|
|
250
|
+
},
|
|
251
|
+
),
|
|
252
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Step 2: Update agent status to running
|
|
256
|
+
await workflow.execute_activity(
|
|
257
|
+
update_agent_status,
|
|
258
|
+
ActivityUpdateAgentInput(
|
|
259
|
+
agent_id=input.agent_id,
|
|
260
|
+
organization_id=input.organization_id,
|
|
261
|
+
status="running",
|
|
262
|
+
last_active_at=workflow.now().isoformat(),
|
|
263
|
+
),
|
|
264
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
# HITL Conversation Loop - Continue until user marks as done
|
|
268
|
+
conversation_turn = 0
|
|
269
|
+
while not self._state.should_complete:
|
|
270
|
+
conversation_turn += 1
|
|
271
|
+
workflow.logger.info(
|
|
272
|
+
f"Starting conversation turn {conversation_turn}",
|
|
273
|
+
extra={"turn": conversation_turn, "message_count": len(self._state.messages)}
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Get the latest user message (last message added)
|
|
277
|
+
latest_message = self._state.messages[-1] if self._state.messages else None
|
|
278
|
+
latest_prompt = latest_message.content if latest_message and latest_message.role == "user" else input.prompt
|
|
279
|
+
|
|
280
|
+
# Execute using RuntimeFactory (supports both "default" Agno and "claude_code")
|
|
281
|
+
workflow.logger.info(
|
|
282
|
+
f"Executing with runtime: {input.runtime_type}",
|
|
283
|
+
extra={"runtime_type": input.runtime_type, "turn": conversation_turn}
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
llm_result = await workflow.execute_activity(
|
|
287
|
+
execute_with_runtime,
|
|
288
|
+
ActivityRuntimeExecuteInput(
|
|
289
|
+
execution_id=input.execution_id,
|
|
290
|
+
agent_id=input.agent_id,
|
|
291
|
+
organization_id=input.organization_id,
|
|
292
|
+
prompt=latest_prompt, # Current turn's prompt
|
|
293
|
+
runtime_type=input.runtime_type,
|
|
294
|
+
system_prompt=input.system_prompt,
|
|
295
|
+
model_id=input.model_id,
|
|
296
|
+
model_config=input.model_config,
|
|
297
|
+
agent_config=input.agent_config,
|
|
298
|
+
mcp_servers=input.mcp_servers,
|
|
299
|
+
conversation_history=[], # Agno manages history via session_id
|
|
300
|
+
user_metadata=input.user_metadata,
|
|
301
|
+
runtime_config={
|
|
302
|
+
"session_id": input.execution_id, # For Agno runtime
|
|
303
|
+
},
|
|
304
|
+
stream=True, # Enable streaming for real-time updates
|
|
305
|
+
),
|
|
306
|
+
start_to_close_timeout=timedelta(minutes=10),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# Add tool execution status messages (real-time updates)
|
|
310
|
+
if llm_result.get("tool_execution_messages"):
|
|
311
|
+
async with self._lock:
|
|
312
|
+
for tool_msg in llm_result["tool_execution_messages"]:
|
|
313
|
+
self._state.messages.append(ChatMessage(
|
|
314
|
+
role="system",
|
|
315
|
+
content=tool_msg.get("content", ""),
|
|
316
|
+
timestamp=tool_msg.get("timestamp", workflow.now().isoformat()),
|
|
317
|
+
tool_name=tool_msg.get("tool_name"),
|
|
318
|
+
))
|
|
319
|
+
|
|
320
|
+
# Add tool messages to state (detailed tool info)
|
|
321
|
+
if llm_result.get("tool_messages"):
|
|
322
|
+
async with self._lock:
|
|
323
|
+
for tool_msg in llm_result["tool_messages"]:
|
|
324
|
+
self._state.messages.append(ChatMessage(
|
|
325
|
+
role="tool",
|
|
326
|
+
content=tool_msg.get("content", ""),
|
|
327
|
+
timestamp=tool_msg.get("timestamp", workflow.now().isoformat()),
|
|
328
|
+
tool_name=tool_msg.get("tool_name"),
|
|
329
|
+
tool_input=tool_msg.get("tool_input"),
|
|
330
|
+
))
|
|
331
|
+
|
|
332
|
+
# Update state with assistant response
|
|
333
|
+
if llm_result.get("response"):
|
|
334
|
+
async with self._lock:
|
|
335
|
+
self._state.messages.append(ChatMessage(
|
|
336
|
+
role="assistant",
|
|
337
|
+
content=llm_result["response"],
|
|
338
|
+
timestamp=workflow.now().isoformat(),
|
|
339
|
+
))
|
|
340
|
+
self._state.current_response = llm_result["response"]
|
|
341
|
+
self._processed_message_count += 1
|
|
342
|
+
|
|
343
|
+
# Update usage and metadata (accumulate across turns)
|
|
344
|
+
if llm_result.get("usage"):
|
|
345
|
+
# Accumulate token usage across conversation turns
|
|
346
|
+
current_usage = self._state.usage
|
|
347
|
+
new_usage = llm_result.get("usage", {})
|
|
348
|
+
self._state.usage = {
|
|
349
|
+
"prompt_tokens": current_usage.get("prompt_tokens", 0) + new_usage.get("prompt_tokens", 0),
|
|
350
|
+
"completion_tokens": current_usage.get("completion_tokens", 0) + new_usage.get("completion_tokens", 0),
|
|
351
|
+
"total_tokens": current_usage.get("total_tokens", 0) + new_usage.get("total_tokens", 0),
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
# Update metadata with latest turn info
|
|
355
|
+
self._state.metadata.update({
|
|
356
|
+
"model": llm_result.get("model"),
|
|
357
|
+
"latest_finish_reason": llm_result.get("finish_reason"),
|
|
358
|
+
"mcp_tools_used": self._state.metadata.get("mcp_tools_used", 0) + llm_result.get("mcp_tools_used", 0),
|
|
359
|
+
"latest_run_id": llm_result.get("run_id"),
|
|
360
|
+
"conversation_turns": conversation_turn,
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
# Extract session_id from runtime result for conversation continuity
|
|
364
|
+
# This enables multi-turn conversations in Claude Code runtime
|
|
365
|
+
llm_metadata = llm_result.get("metadata", {})
|
|
366
|
+
if "claude_code_session_id" in llm_metadata:
|
|
367
|
+
# Update input.user_metadata so next turn can resume the session
|
|
368
|
+
if not input.user_metadata:
|
|
369
|
+
input.user_metadata = {}
|
|
370
|
+
input.user_metadata["claude_code_session_id"] = llm_metadata["claude_code_session_id"]
|
|
371
|
+
workflow.logger.info(
|
|
372
|
+
f"Updated user_metadata with session_id for turn continuity",
|
|
373
|
+
extra={
|
|
374
|
+
"turn": conversation_turn,
|
|
375
|
+
"session_id": llm_metadata["claude_code_session_id"][:16]
|
|
376
|
+
}
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# Persist conversation history after each turn
|
|
380
|
+
# This ensures conversation state is saved end-to-end, runtime-agnostic
|
|
381
|
+
workflow.logger.info(
|
|
382
|
+
f"Persisting conversation after turn {conversation_turn}",
|
|
383
|
+
extra={"turn": conversation_turn, "message_count": len(self._state.messages)}
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
try:
|
|
387
|
+
persist_result = await workflow.execute_activity(
|
|
388
|
+
persist_conversation_history,
|
|
389
|
+
ActivityPersistConversationInput(
|
|
390
|
+
execution_id=input.execution_id,
|
|
391
|
+
session_id=input.execution_id, # Use execution_id as session_id
|
|
392
|
+
messages=self._messages_to_dict(self._state.messages),
|
|
393
|
+
user_id=input.user_metadata.get("user_id") if input.user_metadata else None,
|
|
394
|
+
metadata={
|
|
395
|
+
"agent_id": input.agent_id,
|
|
396
|
+
"organization_id": input.organization_id,
|
|
397
|
+
"conversation_turn": conversation_turn,
|
|
398
|
+
"total_messages": len(self._state.messages),
|
|
399
|
+
},
|
|
400
|
+
),
|
|
401
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
if persist_result.get("success"):
|
|
405
|
+
workflow.logger.info(
|
|
406
|
+
f"✅ Conversation persisted for turn {conversation_turn}",
|
|
407
|
+
extra={
|
|
408
|
+
"turn": conversation_turn,
|
|
409
|
+
"message_count": persist_result.get("message_count", len(self._state.messages))
|
|
410
|
+
}
|
|
411
|
+
)
|
|
412
|
+
else:
|
|
413
|
+
workflow.logger.warning(
|
|
414
|
+
f"⚠️ Persistence returned failure for turn {conversation_turn}",
|
|
415
|
+
extra={
|
|
416
|
+
"turn": conversation_turn,
|
|
417
|
+
"error": persist_result.get("error", "Unknown error")
|
|
418
|
+
}
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
except Exception as persist_error:
|
|
422
|
+
# Log but don't fail the workflow if persistence fails
|
|
423
|
+
# Extract useful error information
|
|
424
|
+
error_type = type(persist_error).__name__
|
|
425
|
+
error_msg = str(persist_error) if str(persist_error) else "No error message"
|
|
426
|
+
|
|
427
|
+
workflow.logger.error(
|
|
428
|
+
f"❌ Failed to persist conversation for turn {conversation_turn}",
|
|
429
|
+
extra={
|
|
430
|
+
"turn": conversation_turn,
|
|
431
|
+
"error_type": error_type,
|
|
432
|
+
"error": error_msg[:200], # Truncate long errors
|
|
433
|
+
"message_count": len(self._state.messages),
|
|
434
|
+
}
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Check if LLM call failed
|
|
438
|
+
if not llm_result.get("success"):
|
|
439
|
+
self._state.status = "failed"
|
|
440
|
+
self._state.error_message = llm_result.get("error")
|
|
441
|
+
break
|
|
442
|
+
|
|
443
|
+
# Update execution status to waiting_for_input
|
|
444
|
+
self._state.status = "waiting_for_input"
|
|
445
|
+
self._state.is_waiting_for_input = True
|
|
446
|
+
|
|
447
|
+
# Update database to reflect waiting state
|
|
448
|
+
await workflow.execute_activity(
|
|
449
|
+
update_execution_status,
|
|
450
|
+
ActivityUpdateExecutionInput(
|
|
451
|
+
execution_id=input.execution_id,
|
|
452
|
+
status="waiting_for_input",
|
|
453
|
+
response=self._state.current_response,
|
|
454
|
+
usage=self._state.usage,
|
|
455
|
+
execution_metadata={
|
|
456
|
+
**self._state.metadata,
|
|
457
|
+
"conversation_turns": conversation_turn,
|
|
458
|
+
"waiting_for_user": True,
|
|
459
|
+
},
|
|
460
|
+
),
|
|
461
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
workflow.logger.info(
|
|
465
|
+
f"Waiting for user input after turn {conversation_turn}",
|
|
466
|
+
extra={"turn": conversation_turn}
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Wait for either:
|
|
470
|
+
# 1. New message from user (add_message signal)
|
|
471
|
+
# 2. User marks as done (mark_as_done signal)
|
|
472
|
+
# 3. Timeout (24 hours for long-running conversations)
|
|
473
|
+
await workflow.wait_condition(
|
|
474
|
+
lambda: self._new_message_count > self._processed_message_count or self._state.should_complete,
|
|
475
|
+
timeout=timedelta(hours=24)
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
# Don't update processed count here - it will be updated after we add the assistant's response
|
|
479
|
+
|
|
480
|
+
if self._state.should_complete:
|
|
481
|
+
workflow.logger.info("User marked workflow as done")
|
|
482
|
+
break
|
|
483
|
+
|
|
484
|
+
# Continue loop to process new message
|
|
485
|
+
self._state.status = "running"
|
|
486
|
+
|
|
487
|
+
# Conversation complete - finalize workflow
|
|
488
|
+
final_status = "failed" if self._state.status == "failed" else "completed"
|
|
489
|
+
self._state.status = final_status
|
|
490
|
+
|
|
491
|
+
await workflow.execute_activity(
|
|
492
|
+
update_execution_status,
|
|
493
|
+
ActivityUpdateExecutionInput(
|
|
494
|
+
execution_id=input.execution_id,
|
|
495
|
+
status=final_status,
|
|
496
|
+
completed_at=workflow.now().isoformat(),
|
|
497
|
+
response=self._state.current_response,
|
|
498
|
+
error_message=self._state.error_message,
|
|
499
|
+
usage=self._state.usage,
|
|
500
|
+
execution_metadata={
|
|
501
|
+
**self._state.metadata,
|
|
502
|
+
"workflow_completed": True,
|
|
503
|
+
"total_conversation_turns": conversation_turn,
|
|
504
|
+
},
|
|
505
|
+
),
|
|
506
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Update agent final status
|
|
510
|
+
agent_final_status = "completed" if final_status == "completed" else "failed"
|
|
511
|
+
await workflow.execute_activity(
|
|
512
|
+
update_agent_status,
|
|
513
|
+
ActivityUpdateAgentInput(
|
|
514
|
+
agent_id=input.agent_id,
|
|
515
|
+
organization_id=input.organization_id,
|
|
516
|
+
status=agent_final_status,
|
|
517
|
+
last_active_at=workflow.now().isoformat(),
|
|
518
|
+
error_message=self._state.error_message if final_status == "failed" else None,
|
|
519
|
+
),
|
|
520
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
workflow.logger.info(
|
|
524
|
+
f"Agent execution workflow completed with HITL",
|
|
525
|
+
extra={
|
|
526
|
+
"execution_id": input.execution_id,
|
|
527
|
+
"status": final_status,
|
|
528
|
+
"conversation_turns": conversation_turn,
|
|
529
|
+
}
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
"success": final_status == "completed",
|
|
534
|
+
"execution_id": input.execution_id,
|
|
535
|
+
"status": final_status,
|
|
536
|
+
"response": self._state.current_response,
|
|
537
|
+
"usage": self._state.usage,
|
|
538
|
+
"conversation_turns": conversation_turn,
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
except Exception as e:
|
|
542
|
+
# Update state with error
|
|
543
|
+
self._state.status = "failed"
|
|
544
|
+
self._state.error_message = str(e)
|
|
545
|
+
self._state.metadata["error_type"] = type(e).__name__
|
|
546
|
+
|
|
547
|
+
workflow.logger.error(
|
|
548
|
+
f"Agent execution workflow failed",
|
|
549
|
+
extra={
|
|
550
|
+
"execution_id": input.execution_id,
|
|
551
|
+
"error": str(e),
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
# Update execution as failed
|
|
556
|
+
try:
|
|
557
|
+
await workflow.execute_activity(
|
|
558
|
+
update_execution_status,
|
|
559
|
+
ActivityUpdateExecutionInput(
|
|
560
|
+
execution_id=input.execution_id,
|
|
561
|
+
status="failed",
|
|
562
|
+
completed_at=workflow.now().isoformat(),
|
|
563
|
+
error_message=f"Workflow error: {str(e)}",
|
|
564
|
+
execution_metadata={
|
|
565
|
+
"workflow_error": True,
|
|
566
|
+
"error_type": type(e).__name__,
|
|
567
|
+
},
|
|
568
|
+
),
|
|
569
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
await workflow.execute_activity(
|
|
573
|
+
update_agent_status,
|
|
574
|
+
ActivityUpdateAgentInput(
|
|
575
|
+
agent_id=input.agent_id,
|
|
576
|
+
organization_id=input.organization_id,
|
|
577
|
+
status="failed",
|
|
578
|
+
last_active_at=workflow.now().isoformat(),
|
|
579
|
+
error_message=str(e),
|
|
580
|
+
),
|
|
581
|
+
start_to_close_timeout=timedelta(seconds=30),
|
|
582
|
+
)
|
|
583
|
+
except Exception as update_error:
|
|
584
|
+
workflow.logger.error(
|
|
585
|
+
f"Failed to update status after error",
|
|
586
|
+
extra={"error": str(update_error)}
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
raise
|