run-dbt 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pdt_cli/__init__.py +1 -0
- pdt_cli/adapter.py +206 -0
- pdt_cli/engine.py +312 -0
- pdt_cli/main.py +298 -0
- pdt_cli/parser.py +106 -0
- pdt_cli/resolver.py +55 -0
- pdt_cli/server.py +110 -0
- pdt_cli/state.py +114 -0
- pdt_cli/workspace.py +107 -0
- run_dbt-0.1.0.dist-info/METADATA +157 -0
- run_dbt-0.1.0.dist-info/RECORD +14 -0
- run_dbt-0.1.0.dist-info/WHEEL +4 -0
- run_dbt-0.1.0.dist-info/entry_points.txt +2 -0
- run_dbt-0.1.0.dist-info/licenses/LICENSE +21 -0
pdt_cli/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""PDT CLI package."""
|
pdt_cli/adapter.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
import tempfile
|
|
5
|
+
import subprocess
|
|
6
|
+
import uuid
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import List, Dict, Any, Optional
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
class ToolCall(BaseModel):
|
|
12
|
+
call_id: str
|
|
13
|
+
name: str
|
|
14
|
+
arguments: Dict[str, Any]
|
|
15
|
+
|
|
16
|
+
class CompletionResponse(BaseModel):
|
|
17
|
+
text: Optional[str] = None
|
|
18
|
+
tool_calls: List[ToolCall] = Field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
class LLMAdapter(ABC):
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def complete(self, prompt: str, system_instruction: str = None, tools: Optional[List[Dict[str, Any]]] = None) -> CompletionResponse:
|
|
23
|
+
"""Sends a prompt to the model and returns a structured response."""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
class CLIDelegatorAdapter(LLMAdapter):
|
|
27
|
+
def __init__(self, command: str, args: List[str]):
|
|
28
|
+
self.command = command
|
|
29
|
+
self.args = args
|
|
30
|
+
|
|
31
|
+
def complete(self, prompt: str, system_instruction: str = None, tools: Optional[List[Dict[str, Any]]] = None) -> CompletionResponse:
|
|
32
|
+
full_system = system_instruction or ""
|
|
33
|
+
if tools:
|
|
34
|
+
tools_instruction = (
|
|
35
|
+
"\n\nYou have access to the following tools:\n"
|
|
36
|
+
+ json.dumps(tools, indent=2)
|
|
37
|
+
+ "\nTo call a tool, you MUST output a JSON object with a single top-level key 'tool_call', like this:\n"
|
|
38
|
+
+ '{"tool_call": {"name": "tool_name", "arguments": {"arg1": "val1"}}}'
|
|
39
|
+
+ "\nDo not output any other text when calling a tool."
|
|
40
|
+
)
|
|
41
|
+
full_system += tools_instruction
|
|
42
|
+
|
|
43
|
+
full_prompt = f"{full_system}\n\n{prompt}" if full_system else prompt
|
|
44
|
+
|
|
45
|
+
with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".txt") as temp:
|
|
46
|
+
temp.write(full_prompt)
|
|
47
|
+
temp_path = temp.name
|
|
48
|
+
|
|
49
|
+
resolved_args = [arg.replace("{prompt_file}", temp_path) for arg in self.args]
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
result = subprocess.run(
|
|
53
|
+
[self.command] + resolved_args,
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
check=True
|
|
57
|
+
)
|
|
58
|
+
output = result.stdout.strip()
|
|
59
|
+
|
|
60
|
+
tool_calls = []
|
|
61
|
+
# Try to parse entire output as JSON
|
|
62
|
+
try:
|
|
63
|
+
data = json.loads(output)
|
|
64
|
+
if "tool_call" in data:
|
|
65
|
+
tc = data["tool_call"]
|
|
66
|
+
tool_calls.append(ToolCall(
|
|
67
|
+
call_id=uuid.uuid4().hex[:8],
|
|
68
|
+
name=tc["name"],
|
|
69
|
+
arguments=tc.get("arguments") or {}
|
|
70
|
+
))
|
|
71
|
+
output = None
|
|
72
|
+
except Exception:
|
|
73
|
+
# Try finding JSON block in the output
|
|
74
|
+
json_match = re.search(r'\{\s*"tool_call"\s*:\s*\{.*?\}\s*\}', output, re.DOTALL)
|
|
75
|
+
if json_match:
|
|
76
|
+
try:
|
|
77
|
+
data = json.loads(json_match.group(0))
|
|
78
|
+
tc = data["tool_call"]
|
|
79
|
+
tool_calls.append(ToolCall(
|
|
80
|
+
call_id=uuid.uuid4().hex[:8],
|
|
81
|
+
name=tc["name"],
|
|
82
|
+
arguments=tc.get("arguments") or {}
|
|
83
|
+
))
|
|
84
|
+
output = output.replace(json_match.group(0), "").strip()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
return CompletionResponse(
|
|
89
|
+
text=output if (output and output.strip()) else None,
|
|
90
|
+
tool_calls=tool_calls
|
|
91
|
+
)
|
|
92
|
+
finally:
|
|
93
|
+
os.unlink(temp_path)
|
|
94
|
+
|
|
95
|
+
class GeminiAdapter(LLMAdapter):
|
|
96
|
+
def __init__(self, model: str = "gemini-2.5-flash"):
|
|
97
|
+
self.model = model
|
|
98
|
+
# Import dynamically to avoid strict dependency loading issues
|
|
99
|
+
from google import genai
|
|
100
|
+
self.client = genai.Client()
|
|
101
|
+
|
|
102
|
+
def complete(self, prompt: str, system_instruction: str = None, tools: Optional[List[Dict[str, Any]]] = None) -> CompletionResponse:
|
|
103
|
+
from google.genai import types
|
|
104
|
+
|
|
105
|
+
config_args = {}
|
|
106
|
+
if system_instruction:
|
|
107
|
+
config_args["system_instruction"] = system_instruction
|
|
108
|
+
|
|
109
|
+
if tools:
|
|
110
|
+
gemini_tools = []
|
|
111
|
+
for t in tools:
|
|
112
|
+
fd = types.FunctionDeclaration(
|
|
113
|
+
name=t["name"],
|
|
114
|
+
description=t["description"],
|
|
115
|
+
parameters=t.get("parameters")
|
|
116
|
+
)
|
|
117
|
+
gemini_tools.append(types.Tool(function_declarations=[fd]))
|
|
118
|
+
config_args["tools"] = gemini_tools
|
|
119
|
+
|
|
120
|
+
config = types.GenerateContentConfig(**config_args)
|
|
121
|
+
|
|
122
|
+
response = self.client.models.generate_content(
|
|
123
|
+
model=self.model,
|
|
124
|
+
contents=prompt,
|
|
125
|
+
config=config
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
tool_calls = []
|
|
129
|
+
if response.function_calls:
|
|
130
|
+
for fc in response.function_calls:
|
|
131
|
+
tool_calls.append(ToolCall(
|
|
132
|
+
call_id=uuid.uuid4().hex[:8],
|
|
133
|
+
name=fc.name,
|
|
134
|
+
arguments=fc.args
|
|
135
|
+
))
|
|
136
|
+
|
|
137
|
+
return CompletionResponse(
|
|
138
|
+
text=response.text,
|
|
139
|
+
tool_calls=tool_calls
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
class OpenAIAdapter(LLMAdapter):
|
|
143
|
+
def __init__(self, model: str = "gpt-4o"):
|
|
144
|
+
self.model = model
|
|
145
|
+
self.api_key = os.getenv("OPENAI_API_KEY")
|
|
146
|
+
|
|
147
|
+
def complete(self, prompt: str, system_instruction: str = None, tools: Optional[List[Dict[str, Any]]] = None) -> CompletionResponse:
|
|
148
|
+
import requests
|
|
149
|
+
|
|
150
|
+
if not self.api_key:
|
|
151
|
+
raise ValueError("OPENAI_API_KEY environment variable is not set")
|
|
152
|
+
|
|
153
|
+
headers = {
|
|
154
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
155
|
+
"Content-Type": "application/json"
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
messages = []
|
|
159
|
+
if system_instruction:
|
|
160
|
+
messages.append({"role": "system", "content": system_instruction})
|
|
161
|
+
messages.append({"role": "user", "content": prompt})
|
|
162
|
+
|
|
163
|
+
payload = {
|
|
164
|
+
"model": self.model,
|
|
165
|
+
"messages": messages
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if tools:
|
|
169
|
+
openai_tools = []
|
|
170
|
+
for t in tools:
|
|
171
|
+
openai_tools.append({
|
|
172
|
+
"type": "function",
|
|
173
|
+
"function": {
|
|
174
|
+
"name": t["name"],
|
|
175
|
+
"description": t["description"],
|
|
176
|
+
"parameters": t.get("parameters") or {"type": "object", "properties": {}}
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
payload["tools"] = openai_tools
|
|
180
|
+
|
|
181
|
+
response = requests.post("https://api.openai.com/v1/chat/completions", json=payload, headers=headers)
|
|
182
|
+
response.raise_for_status()
|
|
183
|
+
resp_json = response.json()
|
|
184
|
+
|
|
185
|
+
message = resp_json["choices"][0]["message"]
|
|
186
|
+
text = message.get("content")
|
|
187
|
+
|
|
188
|
+
tool_calls = []
|
|
189
|
+
if "tool_calls" in message:
|
|
190
|
+
for tc in message["tool_calls"]:
|
|
191
|
+
if tc["type"] == "function":
|
|
192
|
+
func = tc["function"]
|
|
193
|
+
try:
|
|
194
|
+
args = json.loads(func["arguments"])
|
|
195
|
+
except Exception:
|
|
196
|
+
args = {}
|
|
197
|
+
tool_calls.append(ToolCall(
|
|
198
|
+
call_id=tc["id"],
|
|
199
|
+
name=func["name"],
|
|
200
|
+
arguments=args
|
|
201
|
+
))
|
|
202
|
+
|
|
203
|
+
return CompletionResponse(
|
|
204
|
+
text=text,
|
|
205
|
+
tool_calls=tool_calls
|
|
206
|
+
)
|
pdt_cli/engine.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import uuid
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, List, Optional
|
|
8
|
+
from ruamel.yaml import YAML
|
|
9
|
+
|
|
10
|
+
from pdt_cli.workspace import WorkspaceConfig
|
|
11
|
+
from pdt_cli.parser import ProcessDocument, WorkflowStep
|
|
12
|
+
from pdt_cli.resolver import resolve_reference
|
|
13
|
+
from pdt_cli.state import StateManager, RunState, StepState, ToolCallRecord
|
|
14
|
+
from pdt_cli.adapter import LLMAdapter, CLIDelegatorAdapter, GeminiAdapter, OpenAIAdapter, CompletionResponse
|
|
15
|
+
|
|
16
|
+
def step_requires_approval(step: WorkflowStep) -> bool:
|
|
17
|
+
content = (step.title + " " + step.instructions).lower()
|
|
18
|
+
return "approval" in content or "approve" in content or "gate" in content or "confirm" in content or "review" in content
|
|
19
|
+
|
|
20
|
+
class ExecutionEngine:
|
|
21
|
+
def __init__(self, workspace_root: Path, config: WorkspaceConfig, process_doc: ProcessDocument, run_id: Optional[str] = None):
|
|
22
|
+
self.workspace_root = workspace_root
|
|
23
|
+
self.config = config
|
|
24
|
+
self.process_doc = process_doc
|
|
25
|
+
self.state_manager = StateManager(workspace_root, run_id=run_id)
|
|
26
|
+
|
|
27
|
+
def execute(self, inputs: Dict[str, str], target_step: Optional[int] = None, resume: bool = False, approval_input: Optional[str] = None) -> RunState:
|
|
28
|
+
# Load or initialize state
|
|
29
|
+
if resume:
|
|
30
|
+
state = self.state_manager.load_state()
|
|
31
|
+
state.status = "running"
|
|
32
|
+
self.state_manager.save_state(state)
|
|
33
|
+
|
|
34
|
+
# Register approval output if we were waiting for approval
|
|
35
|
+
if state.steps:
|
|
36
|
+
current_step_idx = state.current_step_index
|
|
37
|
+
for s in state.steps:
|
|
38
|
+
if s.index == current_step_idx and s.status == "waiting_for_approval":
|
|
39
|
+
s.status = "completed"
|
|
40
|
+
s.end_time = datetime.now(timezone.utc)
|
|
41
|
+
s.output = approval_input or "Approved by user."
|
|
42
|
+
state.current_step_index += 1
|
|
43
|
+
self.state_manager.save_state(state)
|
|
44
|
+
self.state_manager.log_system(f"Step {current_step_idx} approved and marked completed.")
|
|
45
|
+
break
|
|
46
|
+
else:
|
|
47
|
+
# Check if state already exists for this run_id (if explicit run_id was provided)
|
|
48
|
+
try:
|
|
49
|
+
state = self.state_manager.load_state()
|
|
50
|
+
except FileNotFoundError:
|
|
51
|
+
# Initialize new run
|
|
52
|
+
step_states = []
|
|
53
|
+
for step in self.process_doc.steps:
|
|
54
|
+
step_states.append(StepState(
|
|
55
|
+
index=step.index,
|
|
56
|
+
title=step.title,
|
|
57
|
+
status="pending"
|
|
58
|
+
))
|
|
59
|
+
state = self.state_manager.initialize_run(
|
|
60
|
+
process_id=self.process_doc.frontmatter.id,
|
|
61
|
+
version=self.process_doc.frontmatter.version,
|
|
62
|
+
inputs=inputs,
|
|
63
|
+
steps=step_states
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Main step execution loop
|
|
67
|
+
while state.current_step_index <= len(state.steps):
|
|
68
|
+
step_idx = state.current_step_index
|
|
69
|
+
|
|
70
|
+
# If target_step is specified and we are targeting a single step, check it
|
|
71
|
+
if target_step is not None and step_idx != target_step:
|
|
72
|
+
# If we've already done the target step or are not there yet, jump or break
|
|
73
|
+
# Let's say if we target a single step, we only run that step and then break.
|
|
74
|
+
if step_idx < target_step:
|
|
75
|
+
# Skip to the target step
|
|
76
|
+
state.current_step_index = target_step
|
|
77
|
+
self.state_manager.save_state(state)
|
|
78
|
+
continue
|
|
79
|
+
else:
|
|
80
|
+
# We are past the target step
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
step_state = state.steps[step_idx - 1]
|
|
84
|
+
step_doc = self.process_doc.steps[step_idx - 1]
|
|
85
|
+
|
|
86
|
+
self.state_manager.log_system(f"Starting execution of Step {step_idx}: {step_state.title}")
|
|
87
|
+
step_state.status = "running"
|
|
88
|
+
step_state.start_time = datetime.now(timezone.utc)
|
|
89
|
+
self.state_manager.save_state(state)
|
|
90
|
+
|
|
91
|
+
# Resolve references for the active step
|
|
92
|
+
resolved_skills = []
|
|
93
|
+
resolved_tools = []
|
|
94
|
+
resolved_schemas = []
|
|
95
|
+
|
|
96
|
+
for ref in step_doc.references:
|
|
97
|
+
try:
|
|
98
|
+
abs_path = resolve_reference(ref, self.workspace_root, self.config)
|
|
99
|
+
if ref.startswith("skill/"):
|
|
100
|
+
with open(abs_path, 'r') as f:
|
|
101
|
+
resolved_skills.append((ref, f.read()))
|
|
102
|
+
elif ref.startswith("tool/"):
|
|
103
|
+
yaml = YAML(typ='safe')
|
|
104
|
+
with open(abs_path, 'r') as f:
|
|
105
|
+
tool_data = yaml.load(f) or {}
|
|
106
|
+
resolved_tools.append((ref, tool_data, abs_path.parent))
|
|
107
|
+
elif ref.startswith("schema/"):
|
|
108
|
+
with open(abs_path, 'r') as f:
|
|
109
|
+
resolved_schemas.append((ref, f.read()))
|
|
110
|
+
except Exception as e:
|
|
111
|
+
self.state_manager.log_system(f"Error resolving reference {ref}: {e}")
|
|
112
|
+
step_state.status = "failed"
|
|
113
|
+
step_state.end_time = datetime.now(timezone.utc)
|
|
114
|
+
state.status = "failed"
|
|
115
|
+
self.state_manager.save_state(state)
|
|
116
|
+
raise
|
|
117
|
+
|
|
118
|
+
# Synthesize system prompt and bounded prompt
|
|
119
|
+
system_prompt = (
|
|
120
|
+
"You are an operational assistant executing a step-by-step Standard Operating Procedure (SOP).\n"
|
|
121
|
+
"Your actions must strictly adhere to the global boundaries and rules specified in the Process Description."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Completed steps info
|
|
125
|
+
completed_info = ""
|
|
126
|
+
for s in state.steps[:step_idx - 1]:
|
|
127
|
+
completed_info += f"Step {s.index}: {s.title} ({s.status})\nOutput: {s.output}\n\n"
|
|
128
|
+
if not completed_info:
|
|
129
|
+
completed_info = "None (this is the first step).\n"
|
|
130
|
+
|
|
131
|
+
# Referenced skills text
|
|
132
|
+
skills_text = ""
|
|
133
|
+
for ref, content in resolved_skills:
|
|
134
|
+
skills_text += f"Skill: {ref}\n---\n{content}\n\n"
|
|
135
|
+
if not skills_text:
|
|
136
|
+
skills_text = "None\n"
|
|
137
|
+
|
|
138
|
+
# Referenced tools text
|
|
139
|
+
tools_text = ""
|
|
140
|
+
tools_list_for_adapter = []
|
|
141
|
+
for ref, tool_data, tool_dir in resolved_tools:
|
|
142
|
+
tools_text += f"Tool: {ref}\nDescription: {tool_data.get('description', '')}\nParameters Schema: {json.dumps(tool_data.get('parameters', {}))}\n\n"
|
|
143
|
+
tools_list_for_adapter.append({
|
|
144
|
+
"name": tool_data.get("name") or ref.split('/')[-1],
|
|
145
|
+
"description": tool_data.get("description", ""),
|
|
146
|
+
"parameters": tool_data.get("parameters")
|
|
147
|
+
})
|
|
148
|
+
if not tools_text:
|
|
149
|
+
tools_text = "None\n"
|
|
150
|
+
|
|
151
|
+
# Referenced schemas text
|
|
152
|
+
schemas_text = ""
|
|
153
|
+
for ref, content in resolved_schemas:
|
|
154
|
+
schemas_text += f"Schema: {ref}\n---\n{content}\n\n"
|
|
155
|
+
if not schemas_text:
|
|
156
|
+
schemas_text = "None\n"
|
|
157
|
+
|
|
158
|
+
prompt = (
|
|
159
|
+
f"[PROCESS DESCRIPTION CONTEXT]\n"
|
|
160
|
+
f"Id: {self.process_doc.frontmatter.id}\n"
|
|
161
|
+
f"Owner: {self.process_doc.frontmatter.owner}\n"
|
|
162
|
+
f"Description:\n{self.process_doc.description}\n\n"
|
|
163
|
+
f"[EXECUTION STATE]\n"
|
|
164
|
+
f"The following steps have already completed:\n{completed_info}\n"
|
|
165
|
+
f"[ACTIVE STEP TO EXECUTE]\n"
|
|
166
|
+
f"Step Index: {step_doc.index}\n"
|
|
167
|
+
f"Step Title: {step_doc.title}\n"
|
|
168
|
+
f"Instructions:\n{step_doc.instructions}\n\n"
|
|
169
|
+
f"[AVAILABLE CAPABILITIES]\n"
|
|
170
|
+
f"Referenced Skills:\n{skills_text}\n"
|
|
171
|
+
f"Referenced Tools:\n{tools_text}\n"
|
|
172
|
+
f"Referenced JSON Schemas:\n{schemas_text}\n"
|
|
173
|
+
f"INSTRUCTIONS:\n"
|
|
174
|
+
f"Examine the active step. Invoke available tools to retrieve data or execute calculations.\n"
|
|
175
|
+
f"Produce an output detailing the result, and save relevant outputs.\n"
|
|
176
|
+
f"When finished, output your summary."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Get adapter
|
|
180
|
+
adapter = self.get_llm_adapter()
|
|
181
|
+
|
|
182
|
+
self.state_manager.log_system("Sending prompt to LLM adapter...")
|
|
183
|
+
response = adapter.complete(prompt, system_instruction=system_prompt, tools=tools_list_for_adapter)
|
|
184
|
+
self.state_manager.log_llm(prompt, response.text or str(response.tool_calls))
|
|
185
|
+
|
|
186
|
+
# Handle tool calls in a loop
|
|
187
|
+
while response.tool_calls:
|
|
188
|
+
for tc in response.tool_calls:
|
|
189
|
+
# Find tool definition
|
|
190
|
+
tool_ref = f"tool/{tc.name}"
|
|
191
|
+
tool_def = None
|
|
192
|
+
tool_dir = None
|
|
193
|
+
for ref, tool_data, t_dir in resolved_tools:
|
|
194
|
+
if ref == tool_ref or tool_data.get("name") == tc.name:
|
|
195
|
+
tool_def = tool_data
|
|
196
|
+
tool_dir = t_dir
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
if not tool_def:
|
|
200
|
+
result_str = f"Error: Tool {tc.name} is not available or referenced in this step."
|
|
201
|
+
self.state_manager.log_system(result_str)
|
|
202
|
+
else:
|
|
203
|
+
self.state_manager.log_system(f"Executing tool '{tc.name}' locally with args: {tc.arguments}")
|
|
204
|
+
|
|
205
|
+
record = ToolCallRecord(
|
|
206
|
+
tool_id=tc.name,
|
|
207
|
+
arguments=tc.arguments
|
|
208
|
+
)
|
|
209
|
+
step_state.tool_calls.append(record)
|
|
210
|
+
self.state_manager.save_state(state)
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
# Run tool
|
|
214
|
+
result_str = self.execute_local_tool(tool_def, tool_dir, tc.arguments)
|
|
215
|
+
|
|
216
|
+
# Save evidence
|
|
217
|
+
evidence_filename = f"step{step_idx}_tool_{tc.name}_{uuid.uuid4().hex[:4]}.json"
|
|
218
|
+
evidence_rel_path = self.state_manager.save_evidence(evidence_filename, result_str)
|
|
219
|
+
record.output_file = str(evidence_rel_path)
|
|
220
|
+
self.state_manager.save_state(state)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
result_str = f"Error executing tool {tc.name}: {e}"
|
|
223
|
+
self.state_manager.log_system(result_str)
|
|
224
|
+
|
|
225
|
+
# Append tool call result back to prompt history
|
|
226
|
+
prompt += f"\n\n[TOOL CALL RESULT]\nTool: {tc.name}\nResult:\n{result_str}"
|
|
227
|
+
|
|
228
|
+
# Query LLM again with results
|
|
229
|
+
self.state_manager.log_system("Sending tool results back to LLM adapter...")
|
|
230
|
+
response = adapter.complete(prompt, system_instruction=system_prompt, tools=tools_list_for_adapter)
|
|
231
|
+
self.state_manager.log_llm(prompt, response.text or str(response.tool_calls))
|
|
232
|
+
|
|
233
|
+
# Once LLM completes step execution
|
|
234
|
+
step_state.output = response.text
|
|
235
|
+
|
|
236
|
+
# Check if approval is required
|
|
237
|
+
if step_requires_approval(step_doc):
|
|
238
|
+
step_state.status = "waiting_for_approval"
|
|
239
|
+
state.status = "waiting_for_approval"
|
|
240
|
+
self.state_manager.log_system(f"Step {step_idx} completed but requires human approval/gate.")
|
|
241
|
+
self.state_manager.save_state(state)
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
step_state.status = "completed"
|
|
245
|
+
step_state.end_time = datetime.now(timezone.utc)
|
|
246
|
+
self.state_manager.log_system(f"Step {step_idx} completed successfully.")
|
|
247
|
+
|
|
248
|
+
state.current_step_index += 1
|
|
249
|
+
self.state_manager.save_state(state)
|
|
250
|
+
|
|
251
|
+
if target_step is not None and step_idx == target_step:
|
|
252
|
+
break
|
|
253
|
+
|
|
254
|
+
# Finalize run status
|
|
255
|
+
if state.current_step_index > len(state.steps):
|
|
256
|
+
state.status = "completed"
|
|
257
|
+
state.end_time = datetime.now(timezone.utc)
|
|
258
|
+
self.state_manager.save_state(state)
|
|
259
|
+
self.state_manager.log_system("Workflow execution completed successfully.")
|
|
260
|
+
|
|
261
|
+
return state
|
|
262
|
+
|
|
263
|
+
def execute_local_tool(self, tool_def: Dict[str, Any], tool_dir: Path, arguments: Dict[str, Any]) -> str:
|
|
264
|
+
entrypoint = tool_def.get("entrypoint", "main.py")
|
|
265
|
+
|
|
266
|
+
# Build cmd
|
|
267
|
+
if entrypoint.endswith(".py") and "python" not in entrypoint:
|
|
268
|
+
cmd = ["python", entrypoint]
|
|
269
|
+
elif entrypoint.endswith(".js") and "node" not in entrypoint:
|
|
270
|
+
cmd = ["node", entrypoint]
|
|
271
|
+
else:
|
|
272
|
+
cmd = entrypoint.split()
|
|
273
|
+
|
|
274
|
+
# Workspace environment
|
|
275
|
+
env = os.environ.copy()
|
|
276
|
+
if self.config.deploy and self.config.deploy.env:
|
|
277
|
+
for k, v in self.config.deploy.env.items():
|
|
278
|
+
env[k] = v
|
|
279
|
+
|
|
280
|
+
env["PDT_TOOL_ARGS"] = json.dumps(arguments)
|
|
281
|
+
|
|
282
|
+
res = subprocess.run(
|
|
283
|
+
cmd,
|
|
284
|
+
cwd=tool_dir,
|
|
285
|
+
input=json.dumps(arguments),
|
|
286
|
+
capture_output=True,
|
|
287
|
+
text=True,
|
|
288
|
+
env=env
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if res.returncode != 0:
|
|
292
|
+
raise RuntimeError(f"Tool process exited with non-zero code {res.returncode}. Stderr: {res.stderr}")
|
|
293
|
+
|
|
294
|
+
return res.stdout
|
|
295
|
+
|
|
296
|
+
def get_llm_adapter(self) -> LLMAdapter:
|
|
297
|
+
provider = self.config.llm.provider
|
|
298
|
+
model = self.config.llm.model
|
|
299
|
+
|
|
300
|
+
if provider == "cli":
|
|
301
|
+
if not self.config.llm.cli:
|
|
302
|
+
raise ValueError("LLM provider is set to 'cli', but 'llm.cli' is not configured in pdt.yaml")
|
|
303
|
+
return CLIDelegatorAdapter(
|
|
304
|
+
command=self.config.llm.cli.command,
|
|
305
|
+
args=self.config.llm.cli.args
|
|
306
|
+
)
|
|
307
|
+
elif provider == "gemini":
|
|
308
|
+
return GeminiAdapter(model=model)
|
|
309
|
+
elif provider == "openai":
|
|
310
|
+
return OpenAIAdapter(model=model)
|
|
311
|
+
else:
|
|
312
|
+
raise ValueError(f"Unsupported LLM provider: {provider}")
|