indent 0.1.16__py3-none-any.whl → 0.1.18__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 indent might be problematic. Click here for more details.
- exponent/__init__.py +2 -2
- exponent/cli.py +0 -2
- exponent/commands/common.py +12 -30
- exponent/commands/run_commands.py +1 -2
- exponent/core/remote_execution/cli_rpc_types.py +23 -0
- exponent/core/remote_execution/client.py +67 -11
- exponent/core/remote_execution/languages/shell_streaming.py +4 -1
- exponent/core/remote_execution/tool_execution.py +79 -15
- exponent/core/remote_execution/truncation.py +10 -0
- exponent/core/remote_execution/types.py +19 -18
- exponent/utils/version.py +1 -1
- {indent-0.1.16.dist-info → indent-0.1.18.dist-info}/METADATA +1 -1
- {indent-0.1.16.dist-info → indent-0.1.18.dist-info}/RECORD +15 -16
- exponent/commands/workflow_commands.py +0 -111
- {indent-0.1.16.dist-info → indent-0.1.18.dist-info}/WHEEL +0 -0
- {indent-0.1.16.dist-info → indent-0.1.18.dist-info}/entry_points.txt +0 -0
exponent/__init__.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.18'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 18)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
exponent/cli.py
CHANGED
|
@@ -7,7 +7,6 @@ from exponent.commands.config_commands import config_cli
|
|
|
7
7
|
from exponent.commands.run_commands import run, run_cli
|
|
8
8
|
from exponent.commands.types import ExponentGroup, exponent_cli_group
|
|
9
9
|
from exponent.commands.upgrade import upgrade_cli
|
|
10
|
-
from exponent.commands.workflow_commands import workflow_cli
|
|
11
10
|
from exponent.utils.version import (
|
|
12
11
|
get_installed_version,
|
|
13
12
|
)
|
|
@@ -34,7 +33,6 @@ sources: list[ExponentGroup] = [
|
|
|
34
33
|
run_cli, # Run AI chat commands
|
|
35
34
|
upgrade_cli, # Upgrade-related commands
|
|
36
35
|
cloud_cli, # Cloud commands
|
|
37
|
-
workflow_cli, # Workflow commands
|
|
38
36
|
]
|
|
39
37
|
|
|
40
38
|
for source in sources:
|
exponent/commands/common.py
CHANGED
|
@@ -37,7 +37,8 @@ from exponent.core.remote_execution.exceptions import (
|
|
|
37
37
|
)
|
|
38
38
|
from exponent.core.remote_execution.files import FileCache
|
|
39
39
|
from exponent.core.remote_execution.git import get_git_info
|
|
40
|
-
from exponent.core.remote_execution.
|
|
40
|
+
from exponent.core.remote_execution.session import send_exception_log
|
|
41
|
+
from exponent.core.remote_execution.types import ChatSource
|
|
41
42
|
|
|
42
43
|
load_dotenv()
|
|
43
44
|
|
|
@@ -212,6 +213,13 @@ def run_until_complete(coro: Coroutine[Any, Any, Any]) -> Any:
|
|
|
212
213
|
except asyncio.CancelledError:
|
|
213
214
|
pass
|
|
214
215
|
except ExponentError as e:
|
|
216
|
+
try:
|
|
217
|
+
settings = get_settings()
|
|
218
|
+
loop.run_until_complete(
|
|
219
|
+
send_exception_log(e, session=None, settings=settings)
|
|
220
|
+
)
|
|
221
|
+
except Exception:
|
|
222
|
+
pass
|
|
215
223
|
click.secho(f"Encountered error: {e}", fg="red")
|
|
216
224
|
click.secho(
|
|
217
225
|
"The Indent team has been notified, "
|
|
@@ -321,7 +329,9 @@ async def start_client(
|
|
|
321
329
|
if prompt:
|
|
322
330
|
# If given a prompt, we also need to send a request
|
|
323
331
|
# to kick off the initial turn loop for the chat
|
|
324
|
-
|
|
332
|
+
aux_coros.append(
|
|
333
|
+
start_chat_turn(api_key, base_api_url, base_ws_url, chat_uuid, prompt)
|
|
334
|
+
)
|
|
325
335
|
elif workflow_id:
|
|
326
336
|
# Similarly, if given a workflow ID, we need to send
|
|
327
337
|
# a request to kick off the workflow
|
|
@@ -346,34 +356,6 @@ async def create_chat(
|
|
|
346
356
|
return None
|
|
347
357
|
|
|
348
358
|
|
|
349
|
-
async def get_gh_app_installation_token(
|
|
350
|
-
api_key: str, base_api_url: str, base_ws_url: str, git_info: GitInfo
|
|
351
|
-
) -> dict[str, Any] | None:
|
|
352
|
-
try:
|
|
353
|
-
async with RemoteExecutionClient.session(
|
|
354
|
-
api_key, base_api_url, base_ws_url, os.getcwd()
|
|
355
|
-
) as client:
|
|
356
|
-
return await client.get_gh_installation_token(git_info)
|
|
357
|
-
except (httpx.ConnectError, ExponentError) as e:
|
|
358
|
-
click.secho(f"Error: {e}", fg="red")
|
|
359
|
-
return None
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
async def verify_gh_app_installation(
|
|
363
|
-
api_key: str, base_api_url: str, base_ws_url: str, git_info: GitInfo
|
|
364
|
-
) -> bool:
|
|
365
|
-
try:
|
|
366
|
-
async with RemoteExecutionClient.session(
|
|
367
|
-
api_key, base_api_url, base_ws_url, os.getcwd()
|
|
368
|
-
) as client:
|
|
369
|
-
res = await client.get_gh_installation_token(git_info)
|
|
370
|
-
if "token" in res:
|
|
371
|
-
return True
|
|
372
|
-
except (httpx.ConnectError, ExponentError) as e:
|
|
373
|
-
click.secho(f"Error: {e}", fg="red")
|
|
374
|
-
return False
|
|
375
|
-
|
|
376
|
-
|
|
377
359
|
async def set_login_complete(api_key: str, base_api_url: str, base_ws_url: str) -> None:
|
|
378
360
|
graphql_client = GraphQLClient(
|
|
379
361
|
api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
|
|
@@ -105,8 +105,7 @@ def run(
|
|
|
105
105
|
sys.exit(1)
|
|
106
106
|
|
|
107
107
|
if (
|
|
108
|
-
not
|
|
109
|
-
and (not inside_ssh_session())
|
|
108
|
+
(not inside_ssh_session())
|
|
110
109
|
and (not workflow_id)
|
|
111
110
|
# If the user specified a chat ID, they probably don't want to re-launch the chat
|
|
112
111
|
and (not chat_id)
|
|
@@ -51,6 +51,7 @@ class ErrorToolResult(ToolResult, tag="error"):
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
READ_TOOL_NAME = "read"
|
|
54
|
+
READ_TOOL_ARTIFACT_NAME = "read_tool_artifact"
|
|
54
55
|
|
|
55
56
|
|
|
56
57
|
class ReadToolInput(ToolInput, tag=READ_TOOL_NAME):
|
|
@@ -80,6 +81,15 @@ class ReadToolResult(ToolResult, tag=READ_TOOL_NAME):
|
|
|
80
81
|
return "\n".join(lines)
|
|
81
82
|
|
|
82
83
|
|
|
84
|
+
class ReadToolArtifactResult(ToolResult, tag=READ_TOOL_ARTIFACT_NAME):
|
|
85
|
+
s3_uri: str
|
|
86
|
+
file_path: str
|
|
87
|
+
media_type: str
|
|
88
|
+
|
|
89
|
+
def to_text(self) -> str:
|
|
90
|
+
return f"[Image artifact uploaded to {self.s3_uri}]"
|
|
91
|
+
|
|
92
|
+
|
|
83
93
|
LIST_TOOL_NAME = "ls"
|
|
84
94
|
|
|
85
95
|
|
|
@@ -217,6 +227,7 @@ PartialToolResultType = PartialBashToolResult
|
|
|
217
227
|
|
|
218
228
|
ToolResultType = (
|
|
219
229
|
ReadToolResult
|
|
230
|
+
| ReadToolArtifactResult
|
|
220
231
|
| WriteToolResult
|
|
221
232
|
| ListToolResult
|
|
222
233
|
| GlobToolResult
|
|
@@ -273,6 +284,16 @@ class KeepAliveCliChatResponse(msgspec.Struct, tag="keep_alive_cli_chat"):
|
|
|
273
284
|
pass
|
|
274
285
|
|
|
275
286
|
|
|
287
|
+
class GenerateUploadUrlRequest(msgspec.Struct, tag="generate_upload_url"):
|
|
288
|
+
s3_key: str
|
|
289
|
+
content_type: str
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class GenerateUploadUrlResponse(msgspec.Struct, tag="generate_upload_url"):
|
|
293
|
+
upload_url: str
|
|
294
|
+
s3_uri: str
|
|
295
|
+
|
|
296
|
+
|
|
276
297
|
class CliRpcRequest(msgspec.Struct):
|
|
277
298
|
request_id: str
|
|
278
299
|
request: (
|
|
@@ -283,6 +304,7 @@ class CliRpcRequest(msgspec.Struct):
|
|
|
283
304
|
| BatchToolExecutionRequest
|
|
284
305
|
| SwitchCLIChatRequest
|
|
285
306
|
| KeepAliveCliChatRequest
|
|
307
|
+
| GenerateUploadUrlRequest
|
|
286
308
|
)
|
|
287
309
|
|
|
288
310
|
|
|
@@ -305,4 +327,5 @@ class CliRpcResponse(msgspec.Struct):
|
|
|
305
327
|
| HttpResponse
|
|
306
328
|
| SwitchCLIChatResponse
|
|
307
329
|
| KeepAliveCliChatResponse
|
|
330
|
+
| GenerateUploadUrlResponse
|
|
308
331
|
)
|
|
@@ -31,6 +31,8 @@ from exponent.core.remote_execution.cli_rpc_types import (
|
|
|
31
31
|
CliRpcResponse,
|
|
32
32
|
ErrorResponse,
|
|
33
33
|
ErrorToolResult,
|
|
34
|
+
GenerateUploadUrlRequest,
|
|
35
|
+
GenerateUploadUrlResponse,
|
|
34
36
|
GetAllFilesRequest,
|
|
35
37
|
GetAllFilesResponse,
|
|
36
38
|
HttpRequest,
|
|
@@ -52,6 +54,7 @@ from exponent.core.remote_execution.http_fetch import fetch_http_content
|
|
|
52
54
|
from exponent.core.remote_execution.session import (
|
|
53
55
|
RemoteExecutionClientSession,
|
|
54
56
|
get_session,
|
|
57
|
+
send_exception_log,
|
|
55
58
|
)
|
|
56
59
|
from exponent.core.remote_execution.tool_execution import (
|
|
57
60
|
execute_bash_tool,
|
|
@@ -62,7 +65,6 @@ from exponent.core.remote_execution.types import (
|
|
|
62
65
|
ChatSource,
|
|
63
66
|
CLIConnectedState,
|
|
64
67
|
CreateChatResponse,
|
|
65
|
-
GitInfo,
|
|
66
68
|
HeartbeatInfo,
|
|
67
69
|
RemoteExecutionResponseType,
|
|
68
70
|
RunWorkflowRequest,
|
|
@@ -115,6 +117,13 @@ class RemoteExecutionClient:
|
|
|
115
117
|
# Track last request time for timeout functionality
|
|
116
118
|
self._last_request_time: float | None = None
|
|
117
119
|
|
|
120
|
+
# Track pending upload URL requests
|
|
121
|
+
self._pending_upload_requests: dict[
|
|
122
|
+
str, asyncio.Future[GenerateUploadUrlResponse]
|
|
123
|
+
] = {}
|
|
124
|
+
self._upload_request_lock = asyncio.Lock()
|
|
125
|
+
self._websocket: ClientConnection | None = None
|
|
126
|
+
|
|
118
127
|
@property
|
|
119
128
|
def working_directory(self) -> str:
|
|
120
129
|
return self.current_session.working_directory
|
|
@@ -186,7 +195,21 @@ class RemoteExecutionClient:
|
|
|
186
195
|
self._last_request_time = time.time()
|
|
187
196
|
|
|
188
197
|
msg_data = json.loads(msg)
|
|
189
|
-
if msg_data["type"]
|
|
198
|
+
if msg_data["type"] == "result":
|
|
199
|
+
data = json.dumps(msg_data["data"])
|
|
200
|
+
try:
|
|
201
|
+
response = msgspec.json.decode(data, type=CliRpcResponse)
|
|
202
|
+
if isinstance(response.response, GenerateUploadUrlResponse):
|
|
203
|
+
async with self._upload_request_lock:
|
|
204
|
+
if response.request_id in self._pending_upload_requests:
|
|
205
|
+
future = self._pending_upload_requests.pop(
|
|
206
|
+
response.request_id
|
|
207
|
+
)
|
|
208
|
+
future.set_result(response.response)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
logger.error(f"Error handling upload URL response: {e}")
|
|
211
|
+
return None
|
|
212
|
+
elif msg_data["type"] != "request":
|
|
190
213
|
return None
|
|
191
214
|
|
|
192
215
|
data = json.dumps(msg_data["data"])
|
|
@@ -357,6 +380,10 @@ class RemoteExecutionClient:
|
|
|
357
380
|
await results.put(response)
|
|
358
381
|
except Exception as e:
|
|
359
382
|
logger.info(f"Error handling request {request}:\n\n{e}")
|
|
383
|
+
try:
|
|
384
|
+
await send_exception_log(e, session=self.current_session)
|
|
385
|
+
except Exception:
|
|
386
|
+
pass
|
|
360
387
|
async with results_lock:
|
|
361
388
|
await results.put(
|
|
362
389
|
CliRpcResponse(
|
|
@@ -441,6 +468,8 @@ class RemoteExecutionClient:
|
|
|
441
468
|
if connection_tracker is not None:
|
|
442
469
|
await connection_tracker.set_connected(True)
|
|
443
470
|
|
|
471
|
+
self._websocket = websocket
|
|
472
|
+
|
|
444
473
|
beats: asyncio.Queue[HeartbeatInfo] = asyncio.Queue()
|
|
445
474
|
requests: asyncio.Queue[CliRpcRequest] = asyncio.Queue()
|
|
446
475
|
results: asyncio.Queue[CliRpcResponse] = asyncio.Queue()
|
|
@@ -523,13 +552,6 @@ class RemoteExecutionClient:
|
|
|
523
552
|
)
|
|
524
553
|
return await deserialize_api_response(response, CreateChatResponse)
|
|
525
554
|
|
|
526
|
-
async def get_gh_installation_token(self, git_info: GitInfo) -> dict[str, Any]:
|
|
527
|
-
response = await self.api_client.post(
|
|
528
|
-
"/github_app/exchange_token",
|
|
529
|
-
json=git_info.model_dump(),
|
|
530
|
-
)
|
|
531
|
-
return cast(dict[str, Any], response.json())
|
|
532
|
-
|
|
533
555
|
# deprecated
|
|
534
556
|
async def run_workflow(self, chat_uuid: str, workflow_id: str) -> dict[str, Any]:
|
|
535
557
|
response = await self.api_client.post(
|
|
@@ -582,6 +604,38 @@ class RemoteExecutionClient:
|
|
|
582
604
|
logger.info(f"Heartbeat response: {connected_state}")
|
|
583
605
|
return connected_state
|
|
584
606
|
|
|
607
|
+
async def request_upload_url(
|
|
608
|
+
self, s3_key: str, content_type: str
|
|
609
|
+
) -> GenerateUploadUrlResponse:
|
|
610
|
+
if self._websocket is None:
|
|
611
|
+
raise RuntimeError("No active websocket connection")
|
|
612
|
+
|
|
613
|
+
request_id = str(uuid.uuid4())
|
|
614
|
+
request = CliRpcRequest(
|
|
615
|
+
request_id=request_id,
|
|
616
|
+
request=GenerateUploadUrlRequest(s3_key=s3_key, content_type=content_type),
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
future: asyncio.Future[GenerateUploadUrlResponse] = asyncio.Future()
|
|
620
|
+
async with self._upload_request_lock:
|
|
621
|
+
self._pending_upload_requests[request_id] = future
|
|
622
|
+
|
|
623
|
+
try:
|
|
624
|
+
await self._websocket.send(
|
|
625
|
+
json.dumps({"type": "request", "data": msgspec.to_builtins(request)})
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
response = await asyncio.wait_for(future, timeout=30)
|
|
629
|
+
return response
|
|
630
|
+
except TimeoutError:
|
|
631
|
+
async with self._upload_request_lock:
|
|
632
|
+
self._pending_upload_requests.pop(request_id, None)
|
|
633
|
+
raise RuntimeError("Timeout waiting for upload URL response")
|
|
634
|
+
except Exception as e:
|
|
635
|
+
async with self._upload_request_lock:
|
|
636
|
+
self._pending_upload_requests.pop(request_id, None)
|
|
637
|
+
raise e
|
|
638
|
+
|
|
585
639
|
async def handle_request(self, request: CliRpcRequest) -> CliRpcResponse:
|
|
586
640
|
# Update last request time for timeout functionality
|
|
587
641
|
self._last_request_time = time.time()
|
|
@@ -596,7 +650,7 @@ class RemoteExecutionClient:
|
|
|
596
650
|
)
|
|
597
651
|
else:
|
|
598
652
|
raw_result = await execute_tool( # type: ignore[assignment]
|
|
599
|
-
request.request.tool_input, self.working_directory
|
|
653
|
+
request.request.tool_input, self.working_directory, self
|
|
600
654
|
)
|
|
601
655
|
tool_result = truncate_result(raw_result)
|
|
602
656
|
return CliRpcResponse(
|
|
@@ -623,7 +677,9 @@ class RemoteExecutionClient:
|
|
|
623
677
|
)
|
|
624
678
|
)
|
|
625
679
|
else:
|
|
626
|
-
coros.append(
|
|
680
|
+
coros.append(
|
|
681
|
+
execute_tool(tool_input, self.working_directory, self)
|
|
682
|
+
)
|
|
627
683
|
|
|
628
684
|
results: list[ToolResultType | BaseException] = await asyncio.gather(
|
|
629
685
|
*coros, return_exceptions=True
|
|
@@ -149,7 +149,10 @@ async def execute_shell_streaming( # noqa: PLR0915
|
|
|
149
149
|
def on_timeout() -> None:
|
|
150
150
|
nonlocal timed_out
|
|
151
151
|
timed_out = True
|
|
152
|
-
|
|
152
|
+
try:
|
|
153
|
+
process.kill()
|
|
154
|
+
except ProcessLookupError:
|
|
155
|
+
pass
|
|
153
156
|
|
|
154
157
|
try:
|
|
155
158
|
halt_task = asyncio.create_task(monitor_halt()) if should_halt else None
|
|
@@ -3,6 +3,7 @@ import uuid
|
|
|
3
3
|
from collections.abc import Callable
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from time import time
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
6
7
|
|
|
7
8
|
from anyio import Path as AsyncPath
|
|
8
9
|
|
|
@@ -19,6 +20,7 @@ from exponent.core.remote_execution.cli_rpc_types import (
|
|
|
19
20
|
GrepToolResult,
|
|
20
21
|
ListToolInput,
|
|
21
22
|
ListToolResult,
|
|
23
|
+
ReadToolArtifactResult,
|
|
22
24
|
ReadToolInput,
|
|
23
25
|
ReadToolResult,
|
|
24
26
|
ToolInputType,
|
|
@@ -26,6 +28,9 @@ from exponent.core.remote_execution.cli_rpc_types import (
|
|
|
26
28
|
WriteToolInput,
|
|
27
29
|
WriteToolResult,
|
|
28
30
|
)
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from exponent.core.remote_execution.client import RemoteExecutionClient
|
|
29
34
|
from exponent.core.remote_execution.code_execution import (
|
|
30
35
|
execute_code_streaming,
|
|
31
36
|
)
|
|
@@ -45,10 +50,12 @@ logger = logging.getLogger(__name__)
|
|
|
45
50
|
|
|
46
51
|
|
|
47
52
|
async def execute_tool(
|
|
48
|
-
tool_input: ToolInputType,
|
|
53
|
+
tool_input: ToolInputType,
|
|
54
|
+
working_directory: str,
|
|
55
|
+
upload_client: "RemoteExecutionClient | None" = None,
|
|
49
56
|
) -> ToolResultType:
|
|
50
57
|
if isinstance(tool_input, ReadToolInput):
|
|
51
|
-
return await execute_read_file(tool_input, working_directory)
|
|
58
|
+
return await execute_read_file(tool_input, working_directory, upload_client)
|
|
52
59
|
elif isinstance(tool_input, WriteToolInput):
|
|
53
60
|
return await execute_write_file(tool_input, working_directory)
|
|
54
61
|
elif isinstance(tool_input, ListToolInput):
|
|
@@ -69,9 +76,20 @@ def truncate_result[T: ToolResultType](tool_result: T) -> T:
|
|
|
69
76
|
return truncate_tool_result(tool_result)
|
|
70
77
|
|
|
71
78
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
def is_image_file(file_path: str) -> tuple[bool, str | None]:
|
|
80
|
+
ext = Path(file_path).suffix.lower()
|
|
81
|
+
if ext == ".png":
|
|
82
|
+
return (True, "image/png")
|
|
83
|
+
elif ext in [".jpg", ".jpeg"]:
|
|
84
|
+
return (True, "image/jpeg")
|
|
85
|
+
return (False, None)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def execute_read_file( # noqa: PLR0911, PLR0915
|
|
89
|
+
tool_input: ReadToolInput,
|
|
90
|
+
working_directory: str,
|
|
91
|
+
upload_client: "RemoteExecutionClient | None" = None,
|
|
92
|
+
) -> ReadToolResult | ReadToolArtifactResult | ErrorToolResult:
|
|
75
93
|
# Validate absolute path requirement
|
|
76
94
|
if not tool_input.file_path.startswith("/"):
|
|
77
95
|
return ErrorToolResult(
|
|
@@ -82,16 +100,44 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
82
100
|
offset = tool_input.offset if tool_input.offset is not None else 0
|
|
83
101
|
limit = tool_input.limit if tool_input.limit is not None else 2000
|
|
84
102
|
|
|
85
|
-
if offset < 0:
|
|
86
|
-
return ErrorToolResult(
|
|
87
|
-
error_message=f"Offset must be non-negative, got: {offset}"
|
|
88
|
-
)
|
|
89
|
-
|
|
90
103
|
if limit <= 0:
|
|
91
104
|
return ErrorToolResult(error_message=f"Limit must be positive, got: {limit}")
|
|
92
105
|
|
|
93
106
|
file = AsyncPath(working_directory, tool_input.file_path)
|
|
94
107
|
|
|
108
|
+
# Check if this is an image file and we have an upload client
|
|
109
|
+
is_image, media_type = is_image_file(tool_input.file_path)
|
|
110
|
+
if is_image and media_type and upload_client is not None:
|
|
111
|
+
try:
|
|
112
|
+
import aiohttp
|
|
113
|
+
|
|
114
|
+
file_name = Path(tool_input.file_path).name
|
|
115
|
+
s3_key = f"images/{uuid.uuid4()}/{file_name}"
|
|
116
|
+
|
|
117
|
+
upload_response = await upload_client.request_upload_url(s3_key, media_type)
|
|
118
|
+
|
|
119
|
+
async with aiohttp.ClientSession() as session:
|
|
120
|
+
f = await file.open("rb")
|
|
121
|
+
async with f:
|
|
122
|
+
file_data = await f.read()
|
|
123
|
+
async with session.put(
|
|
124
|
+
upload_response.upload_url,
|
|
125
|
+
data=file_data,
|
|
126
|
+
headers={"Content-Type": media_type},
|
|
127
|
+
) as resp:
|
|
128
|
+
if resp.status != 200:
|
|
129
|
+
raise RuntimeError(
|
|
130
|
+
f"Upload failed with status {resp.status}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return ReadToolArtifactResult(
|
|
134
|
+
s3_uri=upload_response.s3_uri,
|
|
135
|
+
file_path=tool_input.file_path,
|
|
136
|
+
media_type=media_type,
|
|
137
|
+
)
|
|
138
|
+
except Exception as e:
|
|
139
|
+
return ErrorToolResult(error_message=f"Failed to upload image to S3: {e!s}")
|
|
140
|
+
|
|
95
141
|
try:
|
|
96
142
|
exists = await file.exists()
|
|
97
143
|
except (OSError, PermissionError) as e:
|
|
@@ -138,8 +184,8 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
138
184
|
content_lines = content.splitlines(keepends=True)
|
|
139
185
|
total_lines = len(content_lines)
|
|
140
186
|
|
|
141
|
-
# Handle offset beyond file length
|
|
142
|
-
if offset >= total_lines:
|
|
187
|
+
# Handle offset beyond file length for positive offsets
|
|
188
|
+
if offset >= 0 and offset >= total_lines:
|
|
143
189
|
return ReadToolResult(
|
|
144
190
|
content="",
|
|
145
191
|
num_lines=0,
|
|
@@ -148,8 +194,26 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
148
194
|
metadata=metadata,
|
|
149
195
|
)
|
|
150
196
|
|
|
151
|
-
#
|
|
152
|
-
|
|
197
|
+
# Use Python's native slicing - it handles negative offsets naturally
|
|
198
|
+
# Handle the case where offset + limit < 0 (can't mix negative and non-negative indices)
|
|
199
|
+
if offset < 0 and offset + limit < 0:
|
|
200
|
+
# Both start and end are negative, use negative end index
|
|
201
|
+
end_index = offset + limit
|
|
202
|
+
elif offset < 0 and offset + limit >= 0:
|
|
203
|
+
# Start is negative but end would be positive/zero, slice to end
|
|
204
|
+
end_index = None
|
|
205
|
+
else:
|
|
206
|
+
# Normal case: both indices are non-negative
|
|
207
|
+
end_index = offset + limit
|
|
208
|
+
|
|
209
|
+
content_lines = content_lines[offset:end_index]
|
|
210
|
+
|
|
211
|
+
# Calculate the actual start line for the result
|
|
212
|
+
if offset < 0:
|
|
213
|
+
# For negative offsets, calculate where we actually started
|
|
214
|
+
actual_start_line = max(0, total_lines + offset)
|
|
215
|
+
else:
|
|
216
|
+
actual_start_line = offset
|
|
153
217
|
|
|
154
218
|
# Apply character-level truncation at line boundaries to ensure consistency
|
|
155
219
|
# This ensures the content field and num_lines field remain in sync
|
|
@@ -186,7 +250,7 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
186
250
|
return ReadToolResult(
|
|
187
251
|
content=final_content,
|
|
188
252
|
num_lines=num_lines,
|
|
189
|
-
start_line=
|
|
253
|
+
start_line=actual_start_line,
|
|
190
254
|
total_lines=total_lines,
|
|
191
255
|
metadata=metadata,
|
|
192
256
|
)
|
|
@@ -11,6 +11,7 @@ from exponent.core.remote_execution.cli_rpc_types import (
|
|
|
11
11
|
GlobToolResult,
|
|
12
12
|
GrepToolResult,
|
|
13
13
|
ListToolResult,
|
|
14
|
+
ReadToolArtifactResult,
|
|
14
15
|
ReadToolResult,
|
|
15
16
|
ToolResult,
|
|
16
17
|
WriteToolResult,
|
|
@@ -173,6 +174,14 @@ class TailTruncation(TruncationStrategy):
|
|
|
173
174
|
return replace(result, **updates)
|
|
174
175
|
|
|
175
176
|
|
|
177
|
+
class NoOpTruncation(TruncationStrategy):
|
|
178
|
+
def should_truncate(self, result: ToolResult) -> bool:
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
def truncate(self, result: ToolResult) -> ToolResult:
|
|
182
|
+
return result
|
|
183
|
+
|
|
184
|
+
|
|
176
185
|
class StringListTruncation(TruncationStrategy):
|
|
177
186
|
"""Truncation for lists of strings that limits both number of items and individual string length."""
|
|
178
187
|
|
|
@@ -264,6 +273,7 @@ class StringListTruncation(TruncationStrategy):
|
|
|
264
273
|
|
|
265
274
|
TRUNCATION_REGISTRY: dict[type[ToolResult], TruncationStrategy] = {
|
|
266
275
|
ReadToolResult: StringFieldTruncation("content"),
|
|
276
|
+
ReadToolArtifactResult: NoOpTruncation(),
|
|
267
277
|
WriteToolResult: StringFieldTruncation("message"),
|
|
268
278
|
BashToolResult: TailTruncation("shell_output"),
|
|
269
279
|
GrepToolResult: StringListTruncation("matches"),
|
|
@@ -52,6 +52,9 @@ class PrReviewWorkflowInput(BaseModel):
|
|
|
52
52
|
class SlackWorkflowInput(BaseModel):
|
|
53
53
|
channel_id: str
|
|
54
54
|
thread_ts: str
|
|
55
|
+
slack_url: str | None = None
|
|
56
|
+
channel_name: str | None = None
|
|
57
|
+
message_ts: str | None = None
|
|
55
58
|
|
|
56
59
|
|
|
57
60
|
WorkflowInput = PrReviewWorkflowInput | SlackWorkflowInput
|
|
@@ -216,8 +219,18 @@ class PromptAttachment(BaseModel):
|
|
|
216
219
|
prompt_content: str
|
|
217
220
|
|
|
218
221
|
|
|
222
|
+
class SQLAttachment(BaseModel):
|
|
223
|
+
attachment_type: Literal["sql"] = "sql"
|
|
224
|
+
query_content: str
|
|
225
|
+
query_id: str
|
|
226
|
+
|
|
227
|
+
|
|
219
228
|
MessageAttachment = Annotated[
|
|
220
|
-
FileAttachment
|
|
229
|
+
FileAttachment
|
|
230
|
+
| URLAttachment
|
|
231
|
+
| TableSchemaAttachment
|
|
232
|
+
| PromptAttachment
|
|
233
|
+
| SQLAttachment,
|
|
221
234
|
Field(discriminator="attachment_type"),
|
|
222
235
|
]
|
|
223
236
|
|
|
@@ -542,30 +555,18 @@ class CLIConnectedState(BaseModel):
|
|
|
542
555
|
|
|
543
556
|
|
|
544
557
|
class DevboxConnectedState(str, Enum):
|
|
545
|
-
# TODO: Only needed if we create devbox async
|
|
546
|
-
INITIALIZED = "INITIALIZED"
|
|
547
558
|
# The chat has been initialized, but the devbox is still loading
|
|
548
559
|
DEVBOX_LOADING = "DEVBOX_LOADING"
|
|
549
|
-
# Devbox is ready to use but not tied to a chat
|
|
550
|
-
DEVBOX_READY = "DEVBOX_READY"
|
|
551
560
|
# CLI is connected and running on devbox
|
|
552
561
|
CONNECTED = "CONNECTED"
|
|
553
|
-
# CLI has disconnected
|
|
554
|
-
# TODO: what condition?
|
|
555
|
-
CLI_DISCONNECTED = "CLI_DISCONNECTED"
|
|
556
|
-
# CLI has an error, devbox is running
|
|
557
|
-
CLI_ERROR = "CLI_ERROR"
|
|
558
562
|
# Devbox has an error
|
|
559
563
|
DEVBOX_ERROR = "DEVBOX_ERROR"
|
|
560
564
|
# Devbox is going to idle
|
|
561
|
-
|
|
562
|
-
# Devbox is
|
|
563
|
-
|
|
564
|
-
#
|
|
565
|
-
|
|
566
|
-
# Devbox_shutdown
|
|
567
|
-
# TODO: In theory our terminal state, do we want to name something different?
|
|
568
|
-
DEVBOX_SHUTDOWN = "DEVBOX_SHUTDOWN"
|
|
565
|
+
PAUSING = "PAUSING"
|
|
566
|
+
# Devbox has been paused and is not running
|
|
567
|
+
PAUSED = "PAUSED"
|
|
568
|
+
# Dev box is starting up. Sandbox exists but devbox is not running
|
|
569
|
+
RESUMING = "RESUMING"
|
|
569
570
|
|
|
570
571
|
|
|
571
572
|
class CloudConnectedState(BaseModel):
|
exponent/utils/version.py
CHANGED
|
@@ -260,7 +260,7 @@ def upgrade_exponent_in_background(
|
|
|
260
260
|
return
|
|
261
261
|
|
|
262
262
|
click.secho(
|
|
263
|
-
f"\nUpgrading
|
|
263
|
+
f"\nUpgrading Indent from {current_version} to {new_version} (this will take effect next time)\n",
|
|
264
264
|
fg="cyan",
|
|
265
265
|
bold=True,
|
|
266
266
|
)
|
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
exponent/__init__.py,sha256=
|
|
2
|
-
exponent/cli.py,sha256=
|
|
1
|
+
exponent/__init__.py,sha256=Nx3lULyklTDQB2p2ofjQ59zAxYunJHGjMIsvHePGZsI,706
|
|
2
|
+
exponent/cli.py,sha256=QnIeDTgWaQJrRs5WESCkQpVEQiJiAO4qWgB0rYlkd78,3344
|
|
3
3
|
exponent/py.typed,sha256=9XZl5avs8yHp89XP_1Fjtbeg_2rjYorCC9I0k_j-h2c,334
|
|
4
4
|
exponent/commands/cloud_commands.py,sha256=TNSbKnc7VBo7VALj44CqV5tdCACJejEGmtYvc5wjza4,19080
|
|
5
|
-
exponent/commands/common.py,sha256=
|
|
5
|
+
exponent/commands/common.py,sha256=M2KI9yKjB8fecPoDBphMa123c35-iNeaE9q4DxhkaFU,12817
|
|
6
6
|
exponent/commands/config_commands.py,sha256=iVIX7LuoO5QshzZNSrfvw5yPIiLlce8GQSMBEp7-nzw,11415
|
|
7
|
-
exponent/commands/run_commands.py,sha256=
|
|
7
|
+
exponent/commands/run_commands.py,sha256=sQlrp2HjRqFc9HjEzPu2MWoLEEptLv7b5iBpFsPzjKc,6384
|
|
8
8
|
exponent/commands/settings.py,sha256=UwwwoCgCY5hzAFD9slOBbA9Gr1hNfoyJ2blsFDC6V8w,1559
|
|
9
9
|
exponent/commands/types.py,sha256=iDJL3hdwhO1PrhsJTJBioNYSKo0CWV8Nv-ONcDaWIRs,3670
|
|
10
10
|
exponent/commands/upgrade.py,sha256=JZr0sNazziuLByQHdT8GZb-lDbRG1YpHW8VB94q-r8w,803
|
|
11
11
|
exponent/commands/utils.py,sha256=Z3eu3mvYwBh7J_hq17lyt7_MwMG8KcsP7AnsCgOnTNc,4638
|
|
12
|
-
exponent/commands/workflow_commands.py,sha256=eQufFbKrKCZtewyDpuVe_0QcndbxuaIKHfHpHF8-XzI,3602
|
|
13
12
|
exponent/core/config.py,sha256=TNFLUgLnfSocRMVSav_7E4VcaNHXZ_3Mg5Lp1smP46U,5731
|
|
14
13
|
exponent/core/graphql/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
14
|
exponent/core/graphql/client.py,sha256=SRagD3YPyoYZSO1RtfO-OXD7b5dm1NvgoL6CTbN380o,2009
|
|
@@ -19,8 +18,8 @@ exponent/core/graphql/mutations.py,sha256=cAAiSefyamBgy1zJEZ7LNk0rbjwawTPxMyj8eU
|
|
|
19
18
|
exponent/core/graphql/queries.py,sha256=RYsk8bub0esspqgakilhzX07yJf2652Ey9tBZK1l_lY,3297
|
|
20
19
|
exponent/core/graphql/subscriptions.py,sha256=SQngv_nYVNJjiZ_P2k0UcLIu1pzc4vi7q7lhH89NCZM,393
|
|
21
20
|
exponent/core/remote_execution/checkpoints.py,sha256=3QGYMLa8vT7XmxMYTRcGrW8kNGHwRC0AkUfULribJWg,6354
|
|
22
|
-
exponent/core/remote_execution/cli_rpc_types.py,sha256=
|
|
23
|
-
exponent/core/remote_execution/client.py,sha256=
|
|
21
|
+
exponent/core/remote_execution/cli_rpc_types.py,sha256=39MQRwi2WRBBOfIwUpbBDs_ffLgURTaT6kBby3TVTZQ,8255
|
|
22
|
+
exponent/core/remote_execution/client.py,sha256=9TlYzwh1lBiOeBlhGUhSo73KQGMh6nvbm1xdfxM48OY,31349
|
|
24
23
|
exponent/core/remote_execution/code_execution.py,sha256=jYPB_7dJzS9BTPLX9fKQpsFPatwjbXuaFFSxT9tDTfI,2388
|
|
25
24
|
exponent/core/remote_execution/error_info.py,sha256=Rd7OA3ps06qYejPVcOaMBB9AtftP3wqQoOfiILFASnc,1378
|
|
26
25
|
exponent/core/remote_execution/exceptions.py,sha256=eT57lBnBhvh-KJ5lsKWcfgGA5-WisAxhjZx-Z6OupZY,135
|
|
@@ -30,13 +29,13 @@ exponent/core/remote_execution/git.py,sha256=dGjBpeoKJZsYgRwctSq29GmbsNIN9tbSA3V
|
|
|
30
29
|
exponent/core/remote_execution/http_fetch.py,sha256=aFEyXd0S-MRfisSMuIFiEyc1AEAj9nUZ9Rj_P_YRows,2827
|
|
31
30
|
exponent/core/remote_execution/session.py,sha256=jlQIdeUj0f7uOk3BgzlJtBJ_GyTIjCchBp5ApQuF2-I,3847
|
|
32
31
|
exponent/core/remote_execution/system_context.py,sha256=QY1zY8_fWj3sh-fmLYtewvgxh7uTX4ITIJqlUTDkj6c,648
|
|
33
|
-
exponent/core/remote_execution/tool_execution.py,sha256=
|
|
32
|
+
exponent/core/remote_execution/tool_execution.py,sha256=jhsZCV_XDyXC9moN01xUTLvl-q84jMFtpauPqPGz2cQ,15591
|
|
34
33
|
exponent/core/remote_execution/tool_type_utils.py,sha256=7qi6Qd8fvHts019ZSLPbtiy17BUqgqBg3P_gdfvFf7w,1301
|
|
35
|
-
exponent/core/remote_execution/truncation.py,sha256=
|
|
36
|
-
exponent/core/remote_execution/types.py,sha256=
|
|
34
|
+
exponent/core/remote_execution/truncation.py,sha256=1L0qwbWgQFRwJZeCXRVKkEhKmzoS1x2usDLPUIp-jlU,9923
|
|
35
|
+
exponent/core/remote_execution/types.py,sha256=uo8h_ftcafmDp8YTxAI-H9qSaDygHYRFnnrmrBxukm4,14934
|
|
37
36
|
exponent/core/remote_execution/utils.py,sha256=6PlBqYJ3OQwZ0dgXiIu3br04a-d-glDeDZpD0XGGPAE,14793
|
|
38
37
|
exponent/core/remote_execution/languages/python_execution.py,sha256=nsX_LsXcUcHhiEHpSTjOTVNd7CxM146al0kw_iQX5OU,7724
|
|
39
|
-
exponent/core/remote_execution/languages/shell_streaming.py,sha256=
|
|
38
|
+
exponent/core/remote_execution/languages/shell_streaming.py,sha256=rqnCipe4bu4N5k0FNZjwL_5xc0rCUihZtKc-8v5Iuhg,7458
|
|
40
39
|
exponent/core/remote_execution/languages/types.py,sha256=f7FjSRNRSga-ZaE3LddDhxCirUVjlSYMEdoskG6Pta4,314
|
|
41
40
|
exponent/core/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
42
41
|
exponent/core/types/command_data.py,sha256=_HqQsnamRZeVoVaTpeO3ecVUzNBdG62WXlFy6Q7rtUM,5294
|
|
@@ -46,8 +45,8 @@ exponent/core/types/generated/strategy_info.py,sha256=LN6_ykFMszb21Qc3yw77xEKUtd
|
|
|
46
45
|
exponent/migration-docs/login.md,sha256=KIeXy3m2nzSUgw-4PW1XzXfHael1D4Zu93CplLMb3hI,4252
|
|
47
46
|
exponent/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
47
|
exponent/utils/colors.py,sha256=HBkqe_ZmhJ9YiL2Fpulqek4KvLS5mwBTY4LQSM5N8SM,2762
|
|
49
|
-
exponent/utils/version.py,sha256=
|
|
50
|
-
indent-0.1.
|
|
51
|
-
indent-0.1.
|
|
52
|
-
indent-0.1.
|
|
53
|
-
indent-0.1.
|
|
48
|
+
exponent/utils/version.py,sha256=GHZ9ET1kMyDubJZU3w2sah5Pw8XpiEakS5IOlt3wUnQ,8888
|
|
49
|
+
indent-0.1.18.dist-info/METADATA,sha256=fhSeEdMOl6WPMNlWcAWROiZT13E1-b2DooMkOwemXm4,1308
|
|
50
|
+
indent-0.1.18.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
51
|
+
indent-0.1.18.dist-info/entry_points.txt,sha256=q8q1t1sbl4NULGOR0OV5RmSG4KEjkpEQRU_RUXEGzcs,44
|
|
52
|
+
indent-0.1.18.dist-info/RECORD,,
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
import sys
|
|
4
|
-
from typing import cast
|
|
5
|
-
|
|
6
|
-
import click
|
|
7
|
-
|
|
8
|
-
from exponent.commands.run_commands import run_chat
|
|
9
|
-
from exponent.commands.settings import use_settings
|
|
10
|
-
from exponent.commands.types import exponent_cli_group
|
|
11
|
-
from exponent.core.config import Settings
|
|
12
|
-
from exponent.core.remote_execution.client import RemoteExecutionClient, WSDisconnected
|
|
13
|
-
from exponent.core.remote_execution.types import (
|
|
14
|
-
PrReviewWorkflowInput,
|
|
15
|
-
WorkflowTriggerResponse,
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@exponent_cli_group(name="workflow")
|
|
20
|
-
def workflow_cli() -> None:
|
|
21
|
-
"""Workflow commands."""
|
|
22
|
-
pass
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
@workflow_cli.group(hidden=True)
|
|
26
|
-
def workflow() -> None:
|
|
27
|
-
"""Workflow management commands."""
|
|
28
|
-
pass
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@workflow.command()
|
|
32
|
-
@use_settings
|
|
33
|
-
@click.argument("workflow_type", type=click.STRING)
|
|
34
|
-
def trigger(settings: Settings, workflow_type: str) -> None:
|
|
35
|
-
"""Trigger a workflow."""
|
|
36
|
-
|
|
37
|
-
if not settings.api_key:
|
|
38
|
-
raise click.ClickException(
|
|
39
|
-
"No API key found. Use `indent login` to set your API key."
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
if workflow_type != "pr_review":
|
|
43
|
-
raise click.UsageError("Invalid workflow name. Only 'pr_review' is supported.")
|
|
44
|
-
|
|
45
|
-
loop = asyncio.get_event_loop()
|
|
46
|
-
response = loop.run_until_complete(trigger_pr_review_workflow(settings))
|
|
47
|
-
|
|
48
|
-
while True:
|
|
49
|
-
result = run_chat(
|
|
50
|
-
loop, settings.api_key, response.chat_uuid, settings, None, None, None
|
|
51
|
-
)
|
|
52
|
-
if result is None or isinstance(result, WSDisconnected):
|
|
53
|
-
# NOTE: None here means that handle_connection_changes exited
|
|
54
|
-
# first. We should likely have a different message for this.
|
|
55
|
-
if result and result.error_message:
|
|
56
|
-
click.secho(f"Error: {result.error_message}", fg="red")
|
|
57
|
-
sys.exit(10)
|
|
58
|
-
else:
|
|
59
|
-
click.echo("Disconnected upon user request, shutting down...")
|
|
60
|
-
break
|
|
61
|
-
else:
|
|
62
|
-
raise click.ClickException("Workflow run exited unexpectedly")
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
async def _subprocess_check_output(command: str) -> str:
|
|
66
|
-
process = await asyncio.create_subprocess_shell(
|
|
67
|
-
command,
|
|
68
|
-
stdout=asyncio.subprocess.PIPE,
|
|
69
|
-
stderr=asyncio.subprocess.STDOUT,
|
|
70
|
-
)
|
|
71
|
-
stdout, _ = await process.communicate()
|
|
72
|
-
|
|
73
|
-
if process.returncode != 0:
|
|
74
|
-
output = stdout.decode().strip()
|
|
75
|
-
raise click.ClickException(
|
|
76
|
-
f"Command '{command}' failed with exit code {process.returncode}:\n{output}"
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
return stdout.decode().strip()
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
async def trigger_pr_review_workflow(settings: Settings) -> WorkflowTriggerResponse:
|
|
83
|
-
origin_url = await _subprocess_check_output("git ls-remote --get-url origin")
|
|
84
|
-
url = origin_url.strip().removesuffix(".git")
|
|
85
|
-
remote = url.split(":")[-1]
|
|
86
|
-
owner, repo = remote.split("/")[-2:]
|
|
87
|
-
|
|
88
|
-
pr_number_str = os.environ.get("PR_NUMBER")
|
|
89
|
-
if not pr_number_str:
|
|
90
|
-
raise click.ClickException("PR_NUMBER environment variable is not set")
|
|
91
|
-
try:
|
|
92
|
-
pr_number = int(pr_number_str)
|
|
93
|
-
except ValueError:
|
|
94
|
-
raise click.ClickException(
|
|
95
|
-
"PR_NUMBER environment variable is not a valid integer"
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
async with RemoteExecutionClient.session(
|
|
99
|
-
api_key=cast(str, settings.api_key),
|
|
100
|
-
base_url=settings.get_base_api_url(),
|
|
101
|
-
base_ws_url=settings.get_base_ws_url(),
|
|
102
|
-
working_directory=os.getcwd(),
|
|
103
|
-
) as client:
|
|
104
|
-
return await client.trigger_workflow(
|
|
105
|
-
workflow_name="pr_review",
|
|
106
|
-
workflow_input=PrReviewWorkflowInput(
|
|
107
|
-
repo_owner=owner,
|
|
108
|
-
repo_name=repo,
|
|
109
|
-
pr_number=pr_number,
|
|
110
|
-
),
|
|
111
|
-
)
|
|
File without changes
|
|
File without changes
|