indent 0.1.17__tar.gz → 0.1.19__tar.gz
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.
- {indent-0.1.17 → indent-0.1.19}/PKG-INFO +1 -1
- {indent-0.1.17 → indent-0.1.19}/exponent/__init__.py +2 -2
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/cli_rpc_types.py +23 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/client.py +62 -3
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/tool_execution.py +61 -4
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/truncation.py +10 -0
- {indent-0.1.17 → indent-0.1.19}/.gitignore +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/cli.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/commands/cloud_commands.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/commands/common.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/commands/config_commands.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/commands/run_commands.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/commands/settings.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/commands/types.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/commands/upgrade.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/commands/utils.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/config.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/graphql/__init__.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/graphql/client.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/graphql/get_chats_query.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/graphql/github_config_queries.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/graphql/mutations.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/graphql/queries.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/graphql/subscriptions.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/checkpoints.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/code_execution.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/error_info.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/exceptions.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/file_write.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/files.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/git.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/http_fetch.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/languages/python_execution.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/languages/shell_streaming.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/languages/types.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/session.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/system_context.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/tool_type_utils.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/types.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/utils.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/types/__init__.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/types/command_data.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/types/event_types.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/types/generated/__init__.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/core/types/generated/strategy_info.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/migration-docs/login.md +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/py.typed +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/utils/__init__.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/utils/colors.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/exponent/utils/version.py +0 -0
- {indent-0.1.17 → indent-0.1.19}/pyproject.toml +0 -0
|
@@ -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.19'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 19)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -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,
|
|
@@ -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"])
|
|
@@ -445,6 +468,8 @@ class RemoteExecutionClient:
|
|
|
445
468
|
if connection_tracker is not None:
|
|
446
469
|
await connection_tracker.set_connected(True)
|
|
447
470
|
|
|
471
|
+
self._websocket = websocket
|
|
472
|
+
|
|
448
473
|
beats: asyncio.Queue[HeartbeatInfo] = asyncio.Queue()
|
|
449
474
|
requests: asyncio.Queue[CliRpcRequest] = asyncio.Queue()
|
|
450
475
|
results: asyncio.Queue[CliRpcResponse] = asyncio.Queue()
|
|
@@ -579,6 +604,38 @@ class RemoteExecutionClient:
|
|
|
579
604
|
logger.info(f"Heartbeat response: {connected_state}")
|
|
580
605
|
return connected_state
|
|
581
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
|
+
|
|
582
639
|
async def handle_request(self, request: CliRpcRequest) -> CliRpcResponse:
|
|
583
640
|
# Update last request time for timeout functionality
|
|
584
641
|
self._last_request_time = time.time()
|
|
@@ -593,7 +650,7 @@ class RemoteExecutionClient:
|
|
|
593
650
|
)
|
|
594
651
|
else:
|
|
595
652
|
raw_result = await execute_tool( # type: ignore[assignment]
|
|
596
|
-
request.request.tool_input, self.working_directory
|
|
653
|
+
request.request.tool_input, self.working_directory, self
|
|
597
654
|
)
|
|
598
655
|
tool_result = truncate_result(raw_result)
|
|
599
656
|
return CliRpcResponse(
|
|
@@ -620,7 +677,9 @@ class RemoteExecutionClient:
|
|
|
620
677
|
)
|
|
621
678
|
)
|
|
622
679
|
else:
|
|
623
|
-
coros.append(
|
|
680
|
+
coros.append(
|
|
681
|
+
execute_tool(tool_input, self.working_directory, self)
|
|
682
|
+
)
|
|
624
683
|
|
|
625
684
|
results: list[ToolResultType | BaseException] = await asyncio.gather(
|
|
626
685
|
*coros, return_exceptions=True
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import logging
|
|
2
3
|
import uuid
|
|
3
4
|
from collections.abc import Callable
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from time import time
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
6
8
|
|
|
7
9
|
from anyio import Path as AsyncPath
|
|
8
10
|
|
|
@@ -19,6 +21,7 @@ from exponent.core.remote_execution.cli_rpc_types import (
|
|
|
19
21
|
GrepToolResult,
|
|
20
22
|
ListToolInput,
|
|
21
23
|
ListToolResult,
|
|
24
|
+
ReadToolArtifactResult,
|
|
22
25
|
ReadToolInput,
|
|
23
26
|
ReadToolResult,
|
|
24
27
|
ToolInputType,
|
|
@@ -26,6 +29,9 @@ from exponent.core.remote_execution.cli_rpc_types import (
|
|
|
26
29
|
WriteToolInput,
|
|
27
30
|
WriteToolResult,
|
|
28
31
|
)
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from exponent.core.remote_execution.client import RemoteExecutionClient
|
|
29
35
|
from exponent.core.remote_execution.code_execution import (
|
|
30
36
|
execute_code_streaming,
|
|
31
37
|
)
|
|
@@ -45,10 +51,12 @@ logger = logging.getLogger(__name__)
|
|
|
45
51
|
|
|
46
52
|
|
|
47
53
|
async def execute_tool(
|
|
48
|
-
tool_input: ToolInputType,
|
|
54
|
+
tool_input: ToolInputType,
|
|
55
|
+
working_directory: str,
|
|
56
|
+
upload_client: "RemoteExecutionClient | None" = None,
|
|
49
57
|
) -> ToolResultType:
|
|
50
58
|
if isinstance(tool_input, ReadToolInput):
|
|
51
|
-
return await execute_read_file(tool_input, working_directory)
|
|
59
|
+
return await execute_read_file(tool_input, working_directory, upload_client)
|
|
52
60
|
elif isinstance(tool_input, WriteToolInput):
|
|
53
61
|
return await execute_write_file(tool_input, working_directory)
|
|
54
62
|
elif isinstance(tool_input, ListToolInput):
|
|
@@ -69,9 +77,20 @@ def truncate_result[T: ToolResultType](tool_result: T) -> T:
|
|
|
69
77
|
return truncate_tool_result(tool_result)
|
|
70
78
|
|
|
71
79
|
|
|
80
|
+
def is_image_file(file_path: str) -> tuple[bool, str | None]:
|
|
81
|
+
ext = Path(file_path).suffix.lower()
|
|
82
|
+
if ext == ".png":
|
|
83
|
+
return (True, "image/png")
|
|
84
|
+
elif ext in [".jpg", ".jpeg"]:
|
|
85
|
+
return (True, "image/jpeg")
|
|
86
|
+
return (False, None)
|
|
87
|
+
|
|
88
|
+
|
|
72
89
|
async def execute_read_file( # noqa: PLR0911, PLR0915
|
|
73
|
-
tool_input: ReadToolInput,
|
|
74
|
-
|
|
90
|
+
tool_input: ReadToolInput,
|
|
91
|
+
working_directory: str,
|
|
92
|
+
upload_client: "RemoteExecutionClient | None" = None,
|
|
93
|
+
) -> ReadToolResult | ReadToolArtifactResult | ErrorToolResult:
|
|
75
94
|
# Validate absolute path requirement
|
|
76
95
|
if not tool_input.file_path.startswith("/"):
|
|
77
96
|
return ErrorToolResult(
|
|
@@ -87,6 +106,44 @@ async def execute_read_file( # noqa: PLR0911, PLR0915
|
|
|
87
106
|
|
|
88
107
|
file = AsyncPath(working_directory, tool_input.file_path)
|
|
89
108
|
|
|
109
|
+
# Check if this is an image file and we have an upload client
|
|
110
|
+
is_image, media_type = is_image_file(tool_input.file_path)
|
|
111
|
+
if is_image and media_type and upload_client is not None:
|
|
112
|
+
try:
|
|
113
|
+
import urllib.request
|
|
114
|
+
|
|
115
|
+
file_name = Path(tool_input.file_path).name
|
|
116
|
+
s3_key = f"images/{uuid.uuid4()}/{file_name}"
|
|
117
|
+
|
|
118
|
+
upload_response = await upload_client.request_upload_url(s3_key, media_type)
|
|
119
|
+
|
|
120
|
+
f = await file.open("rb")
|
|
121
|
+
async with f:
|
|
122
|
+
file_data = await f.read()
|
|
123
|
+
|
|
124
|
+
def _upload() -> int:
|
|
125
|
+
req = urllib.request.Request(
|
|
126
|
+
upload_response.upload_url,
|
|
127
|
+
data=file_data,
|
|
128
|
+
headers={"Content-Type": media_type},
|
|
129
|
+
method="PUT",
|
|
130
|
+
)
|
|
131
|
+
with urllib.request.urlopen(req) as resp:
|
|
132
|
+
status: int = resp.status
|
|
133
|
+
return status
|
|
134
|
+
|
|
135
|
+
status = await asyncio.to_thread(_upload)
|
|
136
|
+
if status != 200:
|
|
137
|
+
raise RuntimeError(f"Upload failed with status {status}")
|
|
138
|
+
|
|
139
|
+
return ReadToolArtifactResult(
|
|
140
|
+
s3_uri=upload_response.s3_uri,
|
|
141
|
+
file_path=tool_input.file_path,
|
|
142
|
+
media_type=media_type,
|
|
143
|
+
)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
return ErrorToolResult(error_message=f"Failed to upload image to S3: {e!s}")
|
|
146
|
+
|
|
90
147
|
try:
|
|
91
148
|
exists = await file.exists()
|
|
92
149
|
except (OSError, PermissionError) as e:
|
|
@@ -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"),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{indent-0.1.17 → indent-0.1.19}/exponent/core/remote_execution/languages/python_execution.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|