indent 0.1.13__py3-none-any.whl → 0.1.28__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.
- exponent/__init__.py +2 -2
- exponent/cli.py +0 -2
- exponent/commands/cloud_commands.py +2 -87
- exponent/commands/common.py +25 -40
- exponent/commands/config_commands.py +0 -87
- exponent/commands/run_commands.py +5 -2
- exponent/core/config.py +1 -1
- exponent/core/container_build/__init__.py +0 -0
- exponent/core/container_build/types.py +25 -0
- exponent/core/graphql/mutations.py +2 -31
- exponent/core/graphql/queries.py +0 -3
- exponent/core/remote_execution/cli_rpc_types.py +201 -5
- exponent/core/remote_execution/client.py +355 -92
- exponent/core/remote_execution/code_execution.py +26 -7
- exponent/core/remote_execution/default_env.py +31 -0
- exponent/core/remote_execution/languages/shell_streaming.py +11 -6
- exponent/core/remote_execution/port_utils.py +73 -0
- exponent/core/remote_execution/system_context.py +2 -0
- exponent/core/remote_execution/terminal_session.py +517 -0
- exponent/core/remote_execution/terminal_types.py +29 -0
- exponent/core/remote_execution/tool_execution.py +228 -18
- exponent/core/remote_execution/tool_type_utils.py +39 -0
- exponent/core/remote_execution/truncation.py +9 -1
- exponent/core/remote_execution/types.py +71 -19
- exponent/utils/version.py +8 -7
- {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/METADATA +5 -2
- {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/RECORD +29 -24
- exponent/commands/workflow_commands.py +0 -111
- exponent/core/graphql/github_config_queries.py +0 -56
- {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/WHEEL +0 -0
- {indent-0.1.13.dist-info → indent-0.1.28.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
|
@@ -10,6 +12,8 @@ from exponent.core.remote_execution import files
|
|
|
10
12
|
from exponent.core.remote_execution.cli_rpc_types import (
|
|
11
13
|
BashToolInput,
|
|
12
14
|
BashToolResult,
|
|
15
|
+
DownloadArtifactToolInput,
|
|
16
|
+
DownloadArtifactToolResult,
|
|
13
17
|
EditToolInput,
|
|
14
18
|
EditToolResult,
|
|
15
19
|
ErrorToolResult,
|
|
@@ -19,22 +23,32 @@ from exponent.core.remote_execution.cli_rpc_types import (
|
|
|
19
23
|
GrepToolResult,
|
|
20
24
|
ListToolInput,
|
|
21
25
|
ListToolResult,
|
|
26
|
+
ReadToolArtifactResult,
|
|
22
27
|
ReadToolInput,
|
|
23
28
|
ReadToolResult,
|
|
24
29
|
ToolInputType,
|
|
25
30
|
ToolResultType,
|
|
31
|
+
UploadArtifactToolInput,
|
|
32
|
+
UploadArtifactToolResult,
|
|
26
33
|
WriteToolInput,
|
|
27
34
|
WriteToolResult,
|
|
28
35
|
)
|
|
36
|
+
|
|
37
|
+
if TYPE_CHECKING:
|
|
38
|
+
from exponent.core.remote_execution.client import RemoteExecutionClient
|
|
39
|
+
import urllib.request
|
|
40
|
+
|
|
41
|
+
import aiohttp
|
|
42
|
+
|
|
43
|
+
from exponent.core.remote_execution.cli_rpc_types import (
|
|
44
|
+
StreamingCodeExecutionRequest,
|
|
45
|
+
StreamingCodeExecutionResponse,
|
|
46
|
+
)
|
|
29
47
|
from exponent.core.remote_execution.code_execution import (
|
|
30
48
|
execute_code_streaming,
|
|
31
49
|
)
|
|
32
50
|
from exponent.core.remote_execution.file_write import execute_full_file_rewrite
|
|
33
51
|
from exponent.core.remote_execution.truncation import truncate_tool_result
|
|
34
|
-
from exponent.core.remote_execution.types import (
|
|
35
|
-
StreamingCodeExecutionRequest,
|
|
36
|
-
StreamingCodeExecutionResponse,
|
|
37
|
-
)
|
|
38
52
|
from exponent.core.remote_execution.utils import (
|
|
39
53
|
assert_unreachable,
|
|
40
54
|
safe_get_file_metadata,
|
|
@@ -45,10 +59,12 @@ logger = logging.getLogger(__name__)
|
|
|
45
59
|
|
|
46
60
|
|
|
47
61
|
async def execute_tool(
|
|
48
|
-
tool_input: ToolInputType,
|
|
62
|
+
tool_input: ToolInputType,
|
|
63
|
+
working_directory: str,
|
|
64
|
+
upload_client: "RemoteExecutionClient | None" = None,
|
|
49
65
|
) -> ToolResultType:
|
|
50
66
|
if isinstance(tool_input, ReadToolInput):
|
|
51
|
-
return await execute_read_file(tool_input, working_directory)
|
|
67
|
+
return await execute_read_file(tool_input, working_directory, upload_client)
|
|
52
68
|
elif isinstance(tool_input, WriteToolInput):
|
|
53
69
|
return await execute_write_file(tool_input, working_directory)
|
|
54
70
|
elif isinstance(tool_input, ListToolInput):
|
|
@@ -59,6 +75,10 @@ async def execute_tool(
|
|
|
59
75
|
return await execute_grep_files(tool_input, working_directory)
|
|
60
76
|
elif isinstance(tool_input, EditToolInput):
|
|
61
77
|
return await execute_edit_file(tool_input, working_directory)
|
|
78
|
+
elif isinstance(tool_input, DownloadArtifactToolInput):
|
|
79
|
+
return await execute_download_artifact(tool_input, working_directory)
|
|
80
|
+
elif isinstance(tool_input, UploadArtifactToolInput):
|
|
81
|
+
return await execute_upload_artifact(tool_input, working_directory)
|
|
62
82
|
elif isinstance(tool_input, BashToolInput):
|
|
63
83
|
raise ValueError("Bash tool input should be handled by execute_bash_tool")
|
|
64
84
|
else:
|
|
@@ -69,8 +89,19 @@ def truncate_result[T: ToolResultType](tool_result: T) -> T:
|
|
|
69
89
|
return truncate_tool_result(tool_result)
|
|
70
90
|
|
|
71
91
|
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
def is_image_file(file_path: str) -> tuple[bool, str | None]:
|
|
93
|
+
ext = Path(file_path).suffix.lower()
|
|
94
|
+
if ext == ".png":
|
|
95
|
+
return (True, "image/png")
|
|
96
|
+
elif ext in [".jpg", ".jpeg"]:
|
|
97
|
+
return (True, "image/jpeg")
|
|
98
|
+
return (False, None)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
async def execute_read_file( # noqa: PLR0911, PLR0915
|
|
102
|
+
tool_input: ReadToolInput,
|
|
103
|
+
working_directory: str,
|
|
104
|
+
upload_client: "RemoteExecutionClient | None" = None,
|
|
74
105
|
) -> ReadToolResult | ErrorToolResult:
|
|
75
106
|
# Validate absolute path requirement
|
|
76
107
|
if not tool_input.file_path.startswith("/"):
|
|
@@ -82,16 +113,49 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
82
113
|
offset = tool_input.offset if tool_input.offset is not None else 0
|
|
83
114
|
limit = tool_input.limit if tool_input.limit is not None else 2000
|
|
84
115
|
|
|
85
|
-
if offset < 0:
|
|
86
|
-
return ErrorToolResult(
|
|
87
|
-
error_message=f"Offset must be non-negative, got: {offset}"
|
|
88
|
-
)
|
|
89
|
-
|
|
90
116
|
if limit <= 0:
|
|
91
117
|
return ErrorToolResult(error_message=f"Limit must be positive, got: {limit}")
|
|
92
118
|
|
|
93
119
|
file = AsyncPath(working_directory, tool_input.file_path)
|
|
94
120
|
|
|
121
|
+
# Check if this is an image file and we have an upload client
|
|
122
|
+
is_image, media_type = is_image_file(tool_input.file_path)
|
|
123
|
+
if is_image and media_type and upload_client is not None:
|
|
124
|
+
try:
|
|
125
|
+
file_name = Path(tool_input.file_path).name
|
|
126
|
+
s3_key = f"images/{uuid.uuid4()}/{file_name}"
|
|
127
|
+
|
|
128
|
+
upload_response = await upload_client.request_upload_url(s3_key, media_type)
|
|
129
|
+
|
|
130
|
+
f = await file.open("rb")
|
|
131
|
+
async with f:
|
|
132
|
+
file_data = await f.read()
|
|
133
|
+
|
|
134
|
+
def _upload() -> int:
|
|
135
|
+
req = urllib.request.Request(
|
|
136
|
+
upload_response.upload_url,
|
|
137
|
+
data=file_data,
|
|
138
|
+
headers={"Content-Type": media_type},
|
|
139
|
+
method="PUT",
|
|
140
|
+
)
|
|
141
|
+
with urllib.request.urlopen(req) as resp:
|
|
142
|
+
status: int = resp.status
|
|
143
|
+
return status
|
|
144
|
+
|
|
145
|
+
status = await asyncio.to_thread(_upload)
|
|
146
|
+
if status != 200:
|
|
147
|
+
raise RuntimeError(f"Upload failed with status {status}")
|
|
148
|
+
|
|
149
|
+
return ReadToolResult(
|
|
150
|
+
artifact=ReadToolArtifactResult(
|
|
151
|
+
s3_uri=upload_response.s3_uri,
|
|
152
|
+
file_path=tool_input.file_path,
|
|
153
|
+
media_type=media_type,
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
return ErrorToolResult(error_message=f"Failed to upload image to S3: {e!s}")
|
|
158
|
+
|
|
95
159
|
try:
|
|
96
160
|
exists = await file.exists()
|
|
97
161
|
except (OSError, PermissionError) as e:
|
|
@@ -138,8 +202,8 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
138
202
|
content_lines = content.splitlines(keepends=True)
|
|
139
203
|
total_lines = len(content_lines)
|
|
140
204
|
|
|
141
|
-
# Handle offset beyond file length
|
|
142
|
-
if offset >= total_lines:
|
|
205
|
+
# Handle offset beyond file length for positive offsets
|
|
206
|
+
if offset >= 0 and offset >= total_lines:
|
|
143
207
|
return ReadToolResult(
|
|
144
208
|
content="",
|
|
145
209
|
num_lines=0,
|
|
@@ -148,8 +212,26 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
148
212
|
metadata=metadata,
|
|
149
213
|
)
|
|
150
214
|
|
|
151
|
-
#
|
|
152
|
-
|
|
215
|
+
# Use Python's native slicing - it handles negative offsets naturally
|
|
216
|
+
# Handle the case where offset + limit < 0 (can't mix negative and non-negative indices)
|
|
217
|
+
if offset < 0 and offset + limit < 0:
|
|
218
|
+
# Both start and end are negative, use negative end index
|
|
219
|
+
end_index = offset + limit
|
|
220
|
+
elif offset < 0 and offset + limit >= 0:
|
|
221
|
+
# Start is negative but end would be positive/zero, slice to end
|
|
222
|
+
end_index = None
|
|
223
|
+
else:
|
|
224
|
+
# Normal case: both indices are non-negative
|
|
225
|
+
end_index = offset + limit
|
|
226
|
+
|
|
227
|
+
content_lines = content_lines[offset:end_index]
|
|
228
|
+
|
|
229
|
+
# Calculate the actual start line for the result
|
|
230
|
+
if offset < 0:
|
|
231
|
+
# For negative offsets, calculate where we actually started
|
|
232
|
+
actual_start_line = max(0, total_lines + offset)
|
|
233
|
+
else:
|
|
234
|
+
actual_start_line = offset
|
|
153
235
|
|
|
154
236
|
# Apply character-level truncation at line boundaries to ensure consistency
|
|
155
237
|
# This ensures the content field and num_lines field remain in sync
|
|
@@ -186,7 +268,7 @@ async def execute_read_file( # noqa: PLR0911
|
|
|
186
268
|
return ReadToolResult(
|
|
187
269
|
content=final_content,
|
|
188
270
|
num_lines=num_lines,
|
|
189
|
-
start_line=
|
|
271
|
+
start_line=actual_start_line,
|
|
190
272
|
total_lines=total_lines,
|
|
191
273
|
metadata=metadata,
|
|
192
274
|
)
|
|
@@ -383,3 +465,131 @@ async def execute_bash_tool(
|
|
|
383
465
|
timed_out=result.cancelled_for_timeout,
|
|
384
466
|
stopped_by_user=result.halted,
|
|
385
467
|
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
async def execute_download_artifact(
|
|
471
|
+
tool_input: DownloadArtifactToolInput, working_directory: str
|
|
472
|
+
) -> DownloadArtifactToolResult | ErrorToolResult:
|
|
473
|
+
"""Download an artifact from S3 using a pre-signed URL."""
|
|
474
|
+
|
|
475
|
+
# Validate absolute path
|
|
476
|
+
if not tool_input.file_path.startswith("/"):
|
|
477
|
+
return ErrorToolResult(
|
|
478
|
+
error_message=f"File path must be absolute, got relative path: {tool_input.file_path}"
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
# Check if file exists and overwrite is False
|
|
482
|
+
file_path = Path(tool_input.file_path)
|
|
483
|
+
if file_path.exists() and not tool_input.overwrite:
|
|
484
|
+
return ErrorToolResult(
|
|
485
|
+
error_message=f"File already exists: {tool_input.file_path}. Set overwrite=True to replace it."
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
# Download from pre-signed URL
|
|
490
|
+
async with aiohttp.ClientSession() as session:
|
|
491
|
+
async with session.get(tool_input.presigned_url) as response:
|
|
492
|
+
if response.status != 200:
|
|
493
|
+
error_text = await response.text()
|
|
494
|
+
return ErrorToolResult(
|
|
495
|
+
error_message=f"Failed to download artifact: HTTP {response.status} - {error_text}"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Create parent directory if needed
|
|
499
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
500
|
+
|
|
501
|
+
# Write file
|
|
502
|
+
content = await response.read()
|
|
503
|
+
file_path.write_bytes(content)
|
|
504
|
+
|
|
505
|
+
file_size = len(content)
|
|
506
|
+
|
|
507
|
+
# Try to generate content preview for text files
|
|
508
|
+
# Attempt to decode as UTF-8 to determine if it's a text file
|
|
509
|
+
content_preview = None
|
|
510
|
+
num_lines = None
|
|
511
|
+
total_lines = None
|
|
512
|
+
truncated = False
|
|
513
|
+
|
|
514
|
+
try:
|
|
515
|
+
text_content = content.decode("utf-8")
|
|
516
|
+
lines = text_content.splitlines()
|
|
517
|
+
total_lines = len(lines)
|
|
518
|
+
|
|
519
|
+
# Show first 50 lines
|
|
520
|
+
preview_limit = 50
|
|
521
|
+
if len(lines) > preview_limit:
|
|
522
|
+
preview_lines = lines[:preview_limit]
|
|
523
|
+
truncated = True
|
|
524
|
+
num_lines = preview_limit
|
|
525
|
+
else:
|
|
526
|
+
preview_lines = lines
|
|
527
|
+
num_lines = len(lines)
|
|
528
|
+
|
|
529
|
+
content_preview = "\n".join(preview_lines)
|
|
530
|
+
except UnicodeDecodeError:
|
|
531
|
+
# Binary file, skip preview
|
|
532
|
+
pass
|
|
533
|
+
|
|
534
|
+
return DownloadArtifactToolResult(
|
|
535
|
+
file_path=tool_input.file_path,
|
|
536
|
+
artifact_id=tool_input.artifact_id,
|
|
537
|
+
file_size_bytes=file_size,
|
|
538
|
+
content_preview=content_preview,
|
|
539
|
+
num_lines=num_lines,
|
|
540
|
+
total_lines=total_lines,
|
|
541
|
+
truncated=truncated,
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
except Exception as e:
|
|
545
|
+
logger.exception("Failed to download artifact")
|
|
546
|
+
return ErrorToolResult(error_message=f"Failed to download artifact: {e!s}")
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
async def execute_upload_artifact(
|
|
550
|
+
tool_input: UploadArtifactToolInput, working_directory: str
|
|
551
|
+
) -> UploadArtifactToolResult | ErrorToolResult:
|
|
552
|
+
"""Upload an artifact to S3 using a pre-signed URL."""
|
|
553
|
+
|
|
554
|
+
# Validate absolute path
|
|
555
|
+
if not tool_input.file_path.startswith("/"):
|
|
556
|
+
return ErrorToolResult(
|
|
557
|
+
error_message=f"File path must be absolute, got relative path: {tool_input.file_path}"
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
# Check if file exists
|
|
561
|
+
file_path = Path(tool_input.file_path)
|
|
562
|
+
if not file_path.exists():
|
|
563
|
+
return ErrorToolResult(error_message=f"File not found: {tool_input.file_path}")
|
|
564
|
+
|
|
565
|
+
if not file_path.is_file():
|
|
566
|
+
return ErrorToolResult(
|
|
567
|
+
error_message=f"Path is not a file: {tool_input.file_path}"
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
try:
|
|
571
|
+
# Read file
|
|
572
|
+
content = file_path.read_bytes()
|
|
573
|
+
file_size = len(content)
|
|
574
|
+
|
|
575
|
+
# Upload to pre-signed URL
|
|
576
|
+
async with aiohttp.ClientSession() as session:
|
|
577
|
+
headers = {"Content-Type": tool_input.content_type}
|
|
578
|
+
async with session.put(
|
|
579
|
+
tool_input.presigned_url, data=content, headers=headers
|
|
580
|
+
) as response:
|
|
581
|
+
if response.status not in (200, 204):
|
|
582
|
+
error_text = await response.text()
|
|
583
|
+
return ErrorToolResult(
|
|
584
|
+
error_message=f"Failed to upload artifact: HTTP {response.status} - {error_text}"
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
return UploadArtifactToolResult(
|
|
588
|
+
artifact_id=tool_input.artifact_id,
|
|
589
|
+
file_size_bytes=file_size,
|
|
590
|
+
content_type=tool_input.content_type,
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
except Exception as e:
|
|
594
|
+
logger.exception("Failed to upload artifact")
|
|
595
|
+
return ErrorToolResult(error_message=f"Failed to upload artifact: {e!s}")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import msgspec
|
|
4
|
+
|
|
5
|
+
from exponent.core.remote_execution.cli_rpc_types import ToolResult
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def to_mostly_xml(tool_result: ToolResult) -> str:
|
|
9
|
+
"""
|
|
10
|
+
This provides a default textual representation of the tool result. Override it as needed for your tool."""
|
|
11
|
+
d = msgspec.to_builtins(tool_result)
|
|
12
|
+
del d["tool_name"]
|
|
13
|
+
return to_mostly_xml_helper(d)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def to_mostly_xml_helper(
|
|
17
|
+
d: Any,
|
|
18
|
+
) -> str:
|
|
19
|
+
if isinstance(d, dict):
|
|
20
|
+
# No outer wrapper at top level, each field gets XML tags
|
|
21
|
+
parts = []
|
|
22
|
+
for key, value in d.items():
|
|
23
|
+
if isinstance(value, list):
|
|
24
|
+
# Handle lists with item tags
|
|
25
|
+
list_items = "\n".join(
|
|
26
|
+
f"<item>\n{to_mostly_xml_helper(item)}\n</item>" for item in value
|
|
27
|
+
)
|
|
28
|
+
parts.append(f"<{key}>\n{list_items}\n</{key}>")
|
|
29
|
+
elif isinstance(value, dict):
|
|
30
|
+
# Nested dict
|
|
31
|
+
parts.append(f"<{key}>\n{to_mostly_xml_helper(value)}\n</{key}>")
|
|
32
|
+
else:
|
|
33
|
+
# Scalar value
|
|
34
|
+
parts.append(f"<{key}>\n{value!s}\n</{key}>")
|
|
35
|
+
return "\n".join(parts)
|
|
36
|
+
elif isinstance(d, list):
|
|
37
|
+
raise ValueError("Lists are not allowed at the top level")
|
|
38
|
+
else:
|
|
39
|
+
return str(d)
|
|
@@ -17,7 +17,7 @@ from exponent.core.remote_execution.cli_rpc_types import (
|
|
|
17
17
|
)
|
|
18
18
|
from exponent.core.remote_execution.utils import truncate_output
|
|
19
19
|
|
|
20
|
-
DEFAULT_CHARACTER_LIMIT =
|
|
20
|
+
DEFAULT_CHARACTER_LIMIT = 50_000
|
|
21
21
|
DEFAULT_LIST_ITEM_LIMIT = 1000
|
|
22
22
|
DEFAULT_LIST_PREVIEW_ITEMS = 10
|
|
23
23
|
|
|
@@ -173,6 +173,14 @@ class TailTruncation(TruncationStrategy):
|
|
|
173
173
|
return replace(result, **updates)
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
class NoOpTruncation(TruncationStrategy):
|
|
177
|
+
def should_truncate(self, result: ToolResult) -> bool:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def truncate(self, result: ToolResult) -> ToolResult:
|
|
181
|
+
return result
|
|
182
|
+
|
|
183
|
+
|
|
176
184
|
class StringListTruncation(TruncationStrategy):
|
|
177
185
|
"""Truncation for lists of strings that limits both number of items and individual string length."""
|
|
178
186
|
|
|
@@ -49,9 +49,48 @@ class PrReviewWorkflowInput(BaseModel):
|
|
|
49
49
|
pr_number: int
|
|
50
50
|
|
|
51
51
|
|
|
52
|
+
class SlackWorkflowInput(BaseModel):
|
|
53
|
+
discriminator: Literal["slack_workflow"] = "slack_workflow"
|
|
54
|
+
channel_id: str
|
|
55
|
+
thread_ts: str
|
|
56
|
+
slack_url: str | None = None
|
|
57
|
+
channel_name: str | None = None
|
|
58
|
+
message_ts: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SlackPlanApprovalWorkflowInput(BaseModel):
|
|
62
|
+
discriminator: Literal["slack_plan_approval"] = "slack_plan_approval"
|
|
63
|
+
channel_id: str
|
|
64
|
+
thread_ts: str
|
|
65
|
+
slack_url: str
|
|
66
|
+
channel_name: str
|
|
67
|
+
message_ts: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class SentryWorkflowInput(BaseModel):
|
|
71
|
+
title: str
|
|
72
|
+
issue_id: str
|
|
73
|
+
permalink: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class GenericCloudWorkflowInput(BaseModel):
|
|
77
|
+
initial_prompt: str
|
|
78
|
+
system_prompt_override: str | None = None
|
|
79
|
+
reasoning_level: str = "LOW"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
WorkflowInput = (
|
|
83
|
+
PrReviewWorkflowInput
|
|
84
|
+
| SlackWorkflowInput
|
|
85
|
+
| SentryWorkflowInput
|
|
86
|
+
| GenericCloudWorkflowInput
|
|
87
|
+
| SlackPlanApprovalWorkflowInput
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
52
91
|
class WorkflowTriggerRequest(BaseModel):
|
|
53
92
|
workflow_name: str
|
|
54
|
-
workflow_input:
|
|
93
|
+
workflow_input: WorkflowInput
|
|
55
94
|
|
|
56
95
|
|
|
57
96
|
class WorkflowTriggerResponse(BaseModel):
|
|
@@ -81,6 +120,14 @@ class PythonEnvInfo(BaseModel):
|
|
|
81
120
|
provider: Literal["venv", "pyenv", "pipenv", "conda"] | None = "pyenv"
|
|
82
121
|
|
|
83
122
|
|
|
123
|
+
class PortInfo(BaseModel):
|
|
124
|
+
process_name: str
|
|
125
|
+
port: int
|
|
126
|
+
protocol: str
|
|
127
|
+
pid: int | None
|
|
128
|
+
uptime_seconds: float | None
|
|
129
|
+
|
|
130
|
+
|
|
84
131
|
class SystemInfo(BaseModel):
|
|
85
132
|
name: str
|
|
86
133
|
cwd: str
|
|
@@ -88,6 +135,7 @@ class SystemInfo(BaseModel):
|
|
|
88
135
|
shell: str
|
|
89
136
|
git: GitInfo | None
|
|
90
137
|
python_env: PythonEnvInfo | None
|
|
138
|
+
port_usage: list[PortInfo] | None = None
|
|
91
139
|
|
|
92
140
|
|
|
93
141
|
class HeartbeatInfo(BaseModel):
|
|
@@ -208,8 +256,18 @@ class PromptAttachment(BaseModel):
|
|
|
208
256
|
prompt_content: str
|
|
209
257
|
|
|
210
258
|
|
|
259
|
+
class SQLAttachment(BaseModel):
|
|
260
|
+
attachment_type: Literal["sql"] = "sql"
|
|
261
|
+
query_content: str
|
|
262
|
+
query_id: str
|
|
263
|
+
|
|
264
|
+
|
|
211
265
|
MessageAttachment = Annotated[
|
|
212
|
-
FileAttachment
|
|
266
|
+
FileAttachment
|
|
267
|
+
| URLAttachment
|
|
268
|
+
| TableSchemaAttachment
|
|
269
|
+
| PromptAttachment
|
|
270
|
+
| SQLAttachment,
|
|
213
271
|
Field(discriminator="attachment_type"),
|
|
214
272
|
]
|
|
215
273
|
|
|
@@ -509,6 +567,10 @@ class ChatMode(str, Enum):
|
|
|
509
567
|
DATABASE = "DATABASE" # chat with database connection
|
|
510
568
|
WORKFLOW = "WORKFLOW"
|
|
511
569
|
|
|
570
|
+
@classmethod
|
|
571
|
+
def requires_cli(cls, mode: "ChatMode") -> bool:
|
|
572
|
+
return mode not in [cls.DATABASE]
|
|
573
|
+
|
|
512
574
|
|
|
513
575
|
class ChatSource(str, Enum):
|
|
514
576
|
CLI_SHELL = "CLI_SHELL"
|
|
@@ -516,6 +578,8 @@ class ChatSource(str, Enum):
|
|
|
516
578
|
WEB = "WEB"
|
|
517
579
|
DESKTOP_APP = "DESKTOP_APP"
|
|
518
580
|
VSCODE_EXTENSION = "VSCODE_EXTENSION"
|
|
581
|
+
SLACK_APP = "SLACK_APP"
|
|
582
|
+
SENTRY_APP = "SENTRY_APP"
|
|
519
583
|
|
|
520
584
|
|
|
521
585
|
class CLIConnectedState(BaseModel):
|
|
@@ -529,30 +593,18 @@ class CLIConnectedState(BaseModel):
|
|
|
529
593
|
|
|
530
594
|
|
|
531
595
|
class DevboxConnectedState(str, Enum):
|
|
532
|
-
# TODO: Only needed if we create devbox async
|
|
533
|
-
INITIALIZED = "INITIALIZED"
|
|
534
596
|
# The chat has been initialized, but the devbox is still loading
|
|
535
597
|
DEVBOX_LOADING = "DEVBOX_LOADING"
|
|
536
|
-
# Devbox is ready to use but not tied to a chat
|
|
537
|
-
DEVBOX_READY = "DEVBOX_READY"
|
|
538
598
|
# CLI is connected and running on devbox
|
|
539
599
|
CONNECTED = "CONNECTED"
|
|
540
|
-
# CLI has disconnected
|
|
541
|
-
# TODO: what condition?
|
|
542
|
-
CLI_DISCONNECTED = "CLI_DISCONNECTED"
|
|
543
|
-
# CLI has an error, devbox is running
|
|
544
|
-
CLI_ERROR = "CLI_ERROR"
|
|
545
600
|
# Devbox has an error
|
|
546
601
|
DEVBOX_ERROR = "DEVBOX_ERROR"
|
|
547
602
|
# Devbox is going to idle
|
|
548
|
-
|
|
549
|
-
# Devbox is
|
|
550
|
-
|
|
551
|
-
#
|
|
552
|
-
|
|
553
|
-
# Devbox_shutdown
|
|
554
|
-
# TODO: In theory our terminal state, do we want to name something different?
|
|
555
|
-
DEVBOX_SHUTDOWN = "DEVBOX_SHUTDOWN"
|
|
603
|
+
PAUSING = "PAUSING"
|
|
604
|
+
# Devbox has been paused and is not running
|
|
605
|
+
PAUSED = "PAUSED"
|
|
606
|
+
# Dev box is starting up. Sandbox exists but devbox is not running
|
|
607
|
+
RESUMING = "RESUMING"
|
|
556
608
|
|
|
557
609
|
|
|
558
610
|
class CloudConnectedState(BaseModel):
|
exponent/utils/version.py
CHANGED
|
@@ -135,8 +135,8 @@ def _get_upgrade_command_str(version: str) -> str:
|
|
|
135
135
|
|
|
136
136
|
def _new_version_str(current_version: str, new_version: str) -> str:
|
|
137
137
|
return (
|
|
138
|
-
f"\n{click.style('A new
|
|
139
|
-
f"See {click.style('https://docs.
|
|
138
|
+
f"\n{click.style('A new Indent version is available:', fg='cyan')} {new_version} (current: {current_version})\n"
|
|
139
|
+
f"See {click.style('https://docs.indent.com/help_and_resources/troubleshooting#installation-methods', underline=True)} for details.\n"
|
|
140
140
|
)
|
|
141
141
|
|
|
142
142
|
|
|
@@ -148,6 +148,7 @@ def _ask_continue_without_upgrading() -> None:
|
|
|
148
148
|
if click.confirm("Continue without upgrading?", default=False):
|
|
149
149
|
click.secho("Using outdated version.", fg="red")
|
|
150
150
|
else:
|
|
151
|
+
click.secho("Exiting due to outdated version", fg="red")
|
|
151
152
|
sys.exit(1)
|
|
152
153
|
|
|
153
154
|
|
|
@@ -196,15 +197,15 @@ def upgrade_exponent(
|
|
|
196
197
|
|
|
197
198
|
if result.returncode != 0:
|
|
198
199
|
click.secho(
|
|
199
|
-
"\nFailed to upgrade
|
|
200
|
+
"\nFailed to upgrade Indent. See https://docs.indent.com/help_and_resources/troubleshooting#installation-methods for help, or reach out to team@indent.com",
|
|
200
201
|
fg="red",
|
|
201
202
|
)
|
|
202
203
|
sys.exit(2)
|
|
203
204
|
|
|
204
|
-
click.secho(f"Successfully upgraded
|
|
205
|
+
click.secho(f"Successfully upgraded Indent to version {new_version}!", fg="green")
|
|
205
206
|
|
|
206
|
-
click.echo("Re-run
|
|
207
|
-
sys.exit(
|
|
207
|
+
click.echo("Re-run indent to use the latest version.")
|
|
208
|
+
sys.exit(0)
|
|
208
209
|
|
|
209
210
|
|
|
210
211
|
def _upgrade_thread_worker(
|
|
@@ -260,7 +261,7 @@ def upgrade_exponent_in_background(
|
|
|
260
261
|
return
|
|
261
262
|
|
|
262
263
|
click.secho(
|
|
263
|
-
f"\nUpgrading
|
|
264
|
+
f"\nUpgrading Indent from {current_version} to {new_version} (this will take effect next time)\n",
|
|
264
265
|
fg="cyan",
|
|
265
266
|
bold=True,
|
|
266
267
|
)
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: indent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.28
|
|
4
4
|
Summary: Indent is an AI Pair Programmer
|
|
5
5
|
Author-email: Sashank Thupukari <sashank@exponent.run>
|
|
6
6
|
Requires-Python: <3.13,>=3.10
|
|
7
|
+
Requires-Dist: aiohttp>=3.13.2
|
|
7
8
|
Requires-Dist: anyio<5,>=4.6.0
|
|
8
9
|
Requires-Dist: async-timeout<5,>=4.0.3; python_version < '3.11'
|
|
9
10
|
Requires-Dist: beautifulsoup4[chardet]<5,>=4.13.4
|
|
@@ -22,6 +23,8 @@ Requires-Dist: msgspec>=0.19.0
|
|
|
22
23
|
Requires-Dist: packaging~=24.1
|
|
23
24
|
Requires-Dist: pip<26,>=25.0.1
|
|
24
25
|
Requires-Dist: prompt-toolkit<4,>=3.0.36
|
|
26
|
+
Requires-Dist: psutil<7,>=5.9.0
|
|
27
|
+
Requires-Dist: pydantic-ai==0.0.30
|
|
25
28
|
Requires-Dist: pydantic-settings<3,>=2.2.1
|
|
26
29
|
Requires-Dist: pydantic[email]<3,>=2.6.4
|
|
27
30
|
Requires-Dist: pygit2<2,>=1.15.0
|
|
@@ -29,7 +32,7 @@ Requires-Dist: python-ripgrep==0.0.9
|
|
|
29
32
|
Requires-Dist: pyyaml>=6.0.2
|
|
30
33
|
Requires-Dist: questionary<3,>=2.0.1
|
|
31
34
|
Requires-Dist: rapidfuzz<4,>=3.9.0
|
|
32
|
-
Requires-Dist: rich
|
|
35
|
+
Requires-Dist: rich>=13.7.1
|
|
33
36
|
Requires-Dist: sentry-sdk<3,>=2.1.1
|
|
34
37
|
Requires-Dist: toml<0.11,>=0.10.2
|
|
35
38
|
Requires-Dist: websockets~=15.0
|