indent 0.1.26__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 +34 -0
- exponent/cli.py +110 -0
- exponent/commands/cloud_commands.py +585 -0
- exponent/commands/common.py +411 -0
- exponent/commands/config_commands.py +334 -0
- exponent/commands/run_commands.py +222 -0
- exponent/commands/settings.py +56 -0
- exponent/commands/types.py +111 -0
- exponent/commands/upgrade.py +29 -0
- exponent/commands/utils.py +146 -0
- exponent/core/config.py +180 -0
- exponent/core/graphql/__init__.py +0 -0
- exponent/core/graphql/client.py +61 -0
- exponent/core/graphql/get_chats_query.py +47 -0
- exponent/core/graphql/mutations.py +160 -0
- exponent/core/graphql/queries.py +146 -0
- exponent/core/graphql/subscriptions.py +16 -0
- exponent/core/remote_execution/checkpoints.py +212 -0
- exponent/core/remote_execution/cli_rpc_types.py +499 -0
- exponent/core/remote_execution/client.py +999 -0
- exponent/core/remote_execution/code_execution.py +77 -0
- exponent/core/remote_execution/default_env.py +31 -0
- exponent/core/remote_execution/error_info.py +45 -0
- exponent/core/remote_execution/exceptions.py +10 -0
- exponent/core/remote_execution/file_write.py +35 -0
- exponent/core/remote_execution/files.py +330 -0
- exponent/core/remote_execution/git.py +268 -0
- exponent/core/remote_execution/http_fetch.py +94 -0
- exponent/core/remote_execution/languages/python_execution.py +239 -0
- exponent/core/remote_execution/languages/shell_streaming.py +226 -0
- exponent/core/remote_execution/languages/types.py +20 -0
- exponent/core/remote_execution/port_utils.py +73 -0
- exponent/core/remote_execution/session.py +128 -0
- exponent/core/remote_execution/system_context.py +26 -0
- exponent/core/remote_execution/terminal_session.py +375 -0
- exponent/core/remote_execution/terminal_types.py +29 -0
- exponent/core/remote_execution/tool_execution.py +595 -0
- exponent/core/remote_execution/tool_type_utils.py +39 -0
- exponent/core/remote_execution/truncation.py +296 -0
- exponent/core/remote_execution/types.py +635 -0
- exponent/core/remote_execution/utils.py +477 -0
- exponent/core/types/__init__.py +0 -0
- exponent/core/types/command_data.py +206 -0
- exponent/core/types/event_types.py +89 -0
- exponent/core/types/generated/__init__.py +0 -0
- exponent/core/types/generated/strategy_info.py +213 -0
- exponent/migration-docs/login.md +112 -0
- exponent/py.typed +4 -0
- exponent/utils/__init__.py +0 -0
- exponent/utils/colors.py +92 -0
- exponent/utils/version.py +289 -0
- indent-0.1.26.dist-info/METADATA +38 -0
- indent-0.1.26.dist-info/RECORD +55 -0
- indent-0.1.26.dist-info/WHEEL +4 -0
- indent-0.1.26.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
import os.path
|
|
5
|
+
import platform
|
|
6
|
+
import ssl
|
|
7
|
+
import stat
|
|
8
|
+
import sys
|
|
9
|
+
import webbrowser
|
|
10
|
+
from collections.abc import Coroutine
|
|
11
|
+
from typing import Any, cast
|
|
12
|
+
|
|
13
|
+
import certifi
|
|
14
|
+
import click
|
|
15
|
+
import httpx
|
|
16
|
+
from dotenv import load_dotenv
|
|
17
|
+
|
|
18
|
+
from exponent.commands.utils import ConnectionTracker
|
|
19
|
+
from exponent.core.config import (
|
|
20
|
+
Settings,
|
|
21
|
+
get_settings,
|
|
22
|
+
)
|
|
23
|
+
from exponent.core.graphql.client import GraphQLClient
|
|
24
|
+
from exponent.core.graphql.mutations import (
|
|
25
|
+
CREATE_CLOUD_CHAT_MUTATION,
|
|
26
|
+
REFRESH_API_KEY_MUTATION,
|
|
27
|
+
SET_LOGIN_COMPLETE_MUTATION,
|
|
28
|
+
START_CHAT_TURN_MUTATION,
|
|
29
|
+
)
|
|
30
|
+
from exponent.core.remote_execution.client import (
|
|
31
|
+
REMOTE_EXECUTION_CLIENT_EXIT_INFO,
|
|
32
|
+
RemoteExecutionClient,
|
|
33
|
+
)
|
|
34
|
+
from exponent.core.remote_execution.exceptions import (
|
|
35
|
+
ExponentError,
|
|
36
|
+
HandledExponentError,
|
|
37
|
+
)
|
|
38
|
+
from exponent.core.remote_execution.files import FileCache
|
|
39
|
+
from exponent.core.remote_execution.git import get_git_info
|
|
40
|
+
from exponent.core.remote_execution.session import send_exception_log
|
|
41
|
+
from exponent.core.remote_execution.types import ChatSource
|
|
42
|
+
|
|
43
|
+
load_dotenv()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def set_log_level() -> None:
|
|
47
|
+
settings = get_settings()
|
|
48
|
+
logging.basicConfig(level=getattr(logging, settings.log_level), stream=sys.stdout)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def redirect_to_login(settings: Settings, cause: str = "detected") -> None:
|
|
52
|
+
if inside_ssh_session():
|
|
53
|
+
click.echo(f"No API Key {cause}, run 'indent login --key <API-KEY>'")
|
|
54
|
+
else:
|
|
55
|
+
click.echo("No API Key detected, redirecting to login...")
|
|
56
|
+
webbrowser.open(f"{settings.base_url}/settings")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def inside_ssh_session() -> bool:
|
|
60
|
+
return (os.environ.get("SSH_TTY") or os.environ.get("SSH_TTY")) is not None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def inside_git_repo() -> bool:
|
|
64
|
+
git_info = await get_git_info(os.getcwd())
|
|
65
|
+
|
|
66
|
+
return git_info is not None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def missing_ssl_certs() -> bool:
|
|
70
|
+
if platform.system().lower() != "darwin":
|
|
71
|
+
return False
|
|
72
|
+
|
|
73
|
+
openssl_dir, openssl_cafile = os.path.split(
|
|
74
|
+
ssl.get_default_verify_paths().openssl_cafile
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return not os.path.exists(os.path.join(openssl_dir, openssl_cafile))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def install_ssl_certs() -> None:
|
|
81
|
+
STAT_0o775 = (
|
|
82
|
+
stat.S_IRUSR
|
|
83
|
+
| stat.S_IWUSR
|
|
84
|
+
| stat.S_IXUSR
|
|
85
|
+
| stat.S_IRGRP
|
|
86
|
+
| stat.S_IWGRP
|
|
87
|
+
| stat.S_IXGRP
|
|
88
|
+
| stat.S_IROTH
|
|
89
|
+
| stat.S_IXOTH
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
openssl_dir, openssl_cafile = os.path.split(
|
|
93
|
+
ssl.get_default_verify_paths().openssl_cafile
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
cwd = os.getcwd()
|
|
97
|
+
# change working directory to the default SSL directory
|
|
98
|
+
os.chdir(openssl_dir)
|
|
99
|
+
relpath_to_certifi_cafile = os.path.relpath(certifi.where())
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
os.remove(openssl_cafile)
|
|
103
|
+
except FileNotFoundError:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
click.echo(" -- creating symlink to certifi certificate bundle")
|
|
107
|
+
os.symlink(relpath_to_certifi_cafile, openssl_cafile)
|
|
108
|
+
click.echo(" -- setting permissions")
|
|
109
|
+
os.chmod(openssl_cafile, STAT_0o775)
|
|
110
|
+
click.echo(" -- update complete")
|
|
111
|
+
os.chdir(cwd)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def check_ssl() -> None:
|
|
115
|
+
if missing_ssl_certs():
|
|
116
|
+
click.confirm(
|
|
117
|
+
"Missing root SSL certs required for python to make HTTP requests, "
|
|
118
|
+
"install certifi certificates now?",
|
|
119
|
+
abort=True,
|
|
120
|
+
default=True,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
install_ssl_certs()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def check_inside_git_repo(settings: Settings) -> None:
|
|
127
|
+
if not settings.options.git_warning_disabled and not (await inside_git_repo()):
|
|
128
|
+
click.echo(
|
|
129
|
+
click.style(
|
|
130
|
+
"\nWarning: Running from a folder that is not a git repository",
|
|
131
|
+
fg="yellow",
|
|
132
|
+
bold=True,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
click.echo(
|
|
136
|
+
"This is a check to make sure you are running Indent from the root of your project."
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
click.echo(f"\nCurrent directory: {click.style(os.getcwd(), fg='cyan')}")
|
|
140
|
+
|
|
141
|
+
click.echo("\nRecommendation:")
|
|
142
|
+
click.echo(" Run Indent from the root directory of your codebase.")
|
|
143
|
+
click.echo("\nExample:")
|
|
144
|
+
click.echo(
|
|
145
|
+
f" If your project is in {click.style('~/my-project', fg='cyan')}, run:"
|
|
146
|
+
)
|
|
147
|
+
click.echo(f" {click.style('cd ~/my-project && exponent run', fg='green')}")
|
|
148
|
+
|
|
149
|
+
# Tell the user they can run exponent config --no-git-warning to disable this check
|
|
150
|
+
click.echo(
|
|
151
|
+
f"\nYou can run {click.style('indent config --set-git-warning-disabled', fg='green')} to disable this check."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if not click.confirm(
|
|
155
|
+
click.style(
|
|
156
|
+
f"\nDo you want to continue running Exponent from {os.getcwd()}?",
|
|
157
|
+
fg="yellow",
|
|
158
|
+
),
|
|
159
|
+
default=True,
|
|
160
|
+
):
|
|
161
|
+
click.echo(click.style("\nOperation aborted.", fg="red"))
|
|
162
|
+
raise click.Abort()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def check_running_from_home_directory(require_confirmation: bool = True) -> bool:
|
|
166
|
+
if os.path.expanduser("~") == os.getcwd():
|
|
167
|
+
click.echo(
|
|
168
|
+
click.style(
|
|
169
|
+
"\nWarning: Running Indent from Home Directory",
|
|
170
|
+
fg="yellow",
|
|
171
|
+
bold=True,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
click.echo(
|
|
175
|
+
"Running Indent from your home directory can cause unexpected issues."
|
|
176
|
+
)
|
|
177
|
+
click.echo("\nRecommendation:")
|
|
178
|
+
click.echo(" Run Indent from the root directory of your codebase.")
|
|
179
|
+
click.echo("\nExample:")
|
|
180
|
+
click.echo(
|
|
181
|
+
f" If your project is in {click.style('~/my-project', fg='cyan')}, run:"
|
|
182
|
+
)
|
|
183
|
+
click.echo(f" {click.style('cd ~/my-project && indent run', fg='green')}")
|
|
184
|
+
|
|
185
|
+
if require_confirmation:
|
|
186
|
+
if not click.confirm(
|
|
187
|
+
click.style(
|
|
188
|
+
f"\nDo you want to continue running indent from {os.getcwd()}?",
|
|
189
|
+
fg="yellow",
|
|
190
|
+
),
|
|
191
|
+
default=True,
|
|
192
|
+
):
|
|
193
|
+
click.echo(click.style("\nOperation aborted.", fg="red"))
|
|
194
|
+
raise click.Abort()
|
|
195
|
+
else:
|
|
196
|
+
click.echo("\n") # Newline to separate from next command
|
|
197
|
+
|
|
198
|
+
return True
|
|
199
|
+
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def run_until_complete(coro: Coroutine[Any, Any, Any]) -> Any:
|
|
204
|
+
loop = asyncio.get_event_loop()
|
|
205
|
+
task = loop.create_task(coro)
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
loop.run_until_complete(task)
|
|
209
|
+
except KeyboardInterrupt:
|
|
210
|
+
task.cancel()
|
|
211
|
+
try:
|
|
212
|
+
loop.run_until_complete(task)
|
|
213
|
+
except asyncio.CancelledError:
|
|
214
|
+
pass
|
|
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
|
|
223
|
+
click.secho(f"Encountered error: {e}", fg="red")
|
|
224
|
+
click.secho(
|
|
225
|
+
"The Indent team has been notified, "
|
|
226
|
+
"please try again and reach out if the problem persists.",
|
|
227
|
+
fg="yellow",
|
|
228
|
+
)
|
|
229
|
+
sys.exit(1)
|
|
230
|
+
except HandledExponentError as e:
|
|
231
|
+
click.secho(str(e), fg="red")
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
async def create_cloud_chat(
|
|
236
|
+
api_key: str, base_api_url: str, base_ws_url: str, config_uuid: str
|
|
237
|
+
) -> str:
|
|
238
|
+
graphql_client = GraphQLClient(
|
|
239
|
+
api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
variables = {
|
|
243
|
+
"configId": config_uuid,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
result = await graphql_client.execute(
|
|
247
|
+
CREATE_CLOUD_CHAT_MUTATION, variables, "CreateCloudChat", timeout=120
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
data = result["createCloudChat"]
|
|
251
|
+
|
|
252
|
+
if data["__typename"] != "Chat":
|
|
253
|
+
raise HandledExponentError(f"Error creating cloud chat: {data['message']}")
|
|
254
|
+
|
|
255
|
+
return str(data["chatUuid"])
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
async def start_chat_turn(
|
|
259
|
+
api_key: str, base_api_url: str, base_ws_url: str, chat_uuid: str, prompt: str
|
|
260
|
+
) -> None:
|
|
261
|
+
graphql_client = GraphQLClient(
|
|
262
|
+
api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
variables = {
|
|
266
|
+
"chatInput": {"prompt": {"message": prompt, "attachments": []}},
|
|
267
|
+
"parentUuid": None,
|
|
268
|
+
"chatConfig": {
|
|
269
|
+
"chatUuid": chat_uuid,
|
|
270
|
+
"exponentModel": "PREMIUM",
|
|
271
|
+
"requireConfirmation": False,
|
|
272
|
+
"readOnly": False,
|
|
273
|
+
"depthLimit": 20,
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
result = await graphql_client.execute(
|
|
277
|
+
START_CHAT_TURN_MUTATION, variables, "StartChatTurnMutation"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
data = result["startChatReply"]
|
|
281
|
+
|
|
282
|
+
if data["__typename"] != "Chat":
|
|
283
|
+
raise HandledExponentError(f"Error starting chat turn: {data['message']}")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
async def run_workflow(
|
|
287
|
+
base_url: str,
|
|
288
|
+
client: RemoteExecutionClient,
|
|
289
|
+
chat_uuid: str,
|
|
290
|
+
workflow_id: str,
|
|
291
|
+
) -> None:
|
|
292
|
+
click.secho("Running workflow...")
|
|
293
|
+
workflow_data = await client.run_workflow(chat_uuid, workflow_id)
|
|
294
|
+
click.secho("Workflow started.")
|
|
295
|
+
if workflow_data and "workflow_run_uuid" in workflow_data:
|
|
296
|
+
click.echo(
|
|
297
|
+
" - Link: "
|
|
298
|
+
+ click.style(
|
|
299
|
+
f"{base_url}/workflow/{workflow_data['workflow_run_uuid']}",
|
|
300
|
+
fg=(100, 200, 255),
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
async def start_client(
|
|
306
|
+
api_key: str,
|
|
307
|
+
base_url: str,
|
|
308
|
+
base_api_url: str,
|
|
309
|
+
base_ws_url: str,
|
|
310
|
+
chat_uuid: str,
|
|
311
|
+
file_cache: FileCache | None = None,
|
|
312
|
+
prompt: str | None = None,
|
|
313
|
+
workflow_id: str | None = None,
|
|
314
|
+
connection_tracker: ConnectionTracker | None = None,
|
|
315
|
+
timeout_seconds: int | None = None,
|
|
316
|
+
) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO:
|
|
317
|
+
async with RemoteExecutionClient.session(
|
|
318
|
+
api_key=api_key,
|
|
319
|
+
base_url=base_api_url,
|
|
320
|
+
base_ws_url=base_ws_url,
|
|
321
|
+
working_directory=os.getcwd(),
|
|
322
|
+
file_cache=file_cache,
|
|
323
|
+
) as client:
|
|
324
|
+
main_coro = client.run_connection(
|
|
325
|
+
chat_uuid, connection_tracker, timeout_seconds
|
|
326
|
+
)
|
|
327
|
+
aux_coros: list[Coroutine[Any, Any, None]] = []
|
|
328
|
+
|
|
329
|
+
if prompt:
|
|
330
|
+
# If given a prompt, we also need to send a request
|
|
331
|
+
# to kick off the initial turn loop for the chat
|
|
332
|
+
aux_coros.append(
|
|
333
|
+
start_chat_turn(api_key, base_api_url, base_ws_url, chat_uuid, prompt)
|
|
334
|
+
)
|
|
335
|
+
elif workflow_id:
|
|
336
|
+
# Similarly, if given a workflow ID, we need to send
|
|
337
|
+
# a request to kick off the workflow
|
|
338
|
+
aux_coros.append(run_workflow(base_url, client, chat_uuid, workflow_id))
|
|
339
|
+
|
|
340
|
+
client_result, *_ = await asyncio.gather(main_coro, *aux_coros)
|
|
341
|
+
return cast(REMOTE_EXECUTION_CLIENT_EXIT_INFO, client_result)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# Helper functions
|
|
345
|
+
async def create_chat(
|
|
346
|
+
api_key: str, base_api_url: str, base_ws_url: str, chat_source: ChatSource
|
|
347
|
+
) -> str | None:
|
|
348
|
+
try:
|
|
349
|
+
async with RemoteExecutionClient.session(
|
|
350
|
+
api_key, base_api_url, base_ws_url, os.getcwd()
|
|
351
|
+
) as client:
|
|
352
|
+
chat = await client.create_chat(chat_source)
|
|
353
|
+
return chat.chat_uuid
|
|
354
|
+
except (httpx.ConnectError, ExponentError) as e:
|
|
355
|
+
click.secho(f"Error: {e}", fg="red")
|
|
356
|
+
return None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
async def set_login_complete(api_key: str, base_api_url: str, base_ws_url: str) -> None:
|
|
360
|
+
graphql_client = GraphQLClient(
|
|
361
|
+
api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
|
|
362
|
+
)
|
|
363
|
+
result = await graphql_client.execute(
|
|
364
|
+
SET_LOGIN_COMPLETE_MUTATION, {}, "SetLoginComplete"
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
data = result["setLoginComplete"]
|
|
368
|
+
|
|
369
|
+
if data["__typename"] == "UnauthenticatedError":
|
|
370
|
+
raise HandledExponentError(f"Verification failed: {data['message']}")
|
|
371
|
+
|
|
372
|
+
if data["userApiKey"] != api_key:
|
|
373
|
+
# We got a user object back, but the api_key is different
|
|
374
|
+
# than the one used in the user's request...
|
|
375
|
+
# This should never happen
|
|
376
|
+
raise HandledExponentError(
|
|
377
|
+
"Invalid API key, login to https://indent.com to find your API key."
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
async def refresh_api_key_task(
|
|
382
|
+
api_key: str,
|
|
383
|
+
base_api_url: str,
|
|
384
|
+
base_ws_url: str,
|
|
385
|
+
) -> None:
|
|
386
|
+
graphql_client = GraphQLClient(api_key, base_api_url, base_ws_url)
|
|
387
|
+
result = await graphql_client.execute(REFRESH_API_KEY_MUTATION)
|
|
388
|
+
|
|
389
|
+
if "refreshApiKey" in result:
|
|
390
|
+
if "message" in result["refreshApiKey"]:
|
|
391
|
+
# Handle error case
|
|
392
|
+
click.secho(f"Error: {result['refreshApiKey']['message']}", fg="red")
|
|
393
|
+
return
|
|
394
|
+
|
|
395
|
+
if "userApiKey" in result["refreshApiKey"]:
|
|
396
|
+
# Handle success case
|
|
397
|
+
new_api_key = result["refreshApiKey"]["userApiKey"]
|
|
398
|
+
settings = get_settings()
|
|
399
|
+
|
|
400
|
+
click.echo(f"Saving new API Key to {settings.config_file_path}")
|
|
401
|
+
settings.update_api_key(new_api_key)
|
|
402
|
+
settings.write_settings_to_config_file()
|
|
403
|
+
|
|
404
|
+
click.secho(
|
|
405
|
+
"API key has been refreshed and saved successfully!", fg="green"
|
|
406
|
+
)
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
# Handle unexpected response
|
|
410
|
+
click.secho("Failed to refresh API key: Unexpected response", fg="red")
|
|
411
|
+
click.echo(result)
|