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.
Files changed (55) hide show
  1. exponent/__init__.py +34 -0
  2. exponent/cli.py +110 -0
  3. exponent/commands/cloud_commands.py +585 -0
  4. exponent/commands/common.py +411 -0
  5. exponent/commands/config_commands.py +334 -0
  6. exponent/commands/run_commands.py +222 -0
  7. exponent/commands/settings.py +56 -0
  8. exponent/commands/types.py +111 -0
  9. exponent/commands/upgrade.py +29 -0
  10. exponent/commands/utils.py +146 -0
  11. exponent/core/config.py +180 -0
  12. exponent/core/graphql/__init__.py +0 -0
  13. exponent/core/graphql/client.py +61 -0
  14. exponent/core/graphql/get_chats_query.py +47 -0
  15. exponent/core/graphql/mutations.py +160 -0
  16. exponent/core/graphql/queries.py +146 -0
  17. exponent/core/graphql/subscriptions.py +16 -0
  18. exponent/core/remote_execution/checkpoints.py +212 -0
  19. exponent/core/remote_execution/cli_rpc_types.py +499 -0
  20. exponent/core/remote_execution/client.py +999 -0
  21. exponent/core/remote_execution/code_execution.py +77 -0
  22. exponent/core/remote_execution/default_env.py +31 -0
  23. exponent/core/remote_execution/error_info.py +45 -0
  24. exponent/core/remote_execution/exceptions.py +10 -0
  25. exponent/core/remote_execution/file_write.py +35 -0
  26. exponent/core/remote_execution/files.py +330 -0
  27. exponent/core/remote_execution/git.py +268 -0
  28. exponent/core/remote_execution/http_fetch.py +94 -0
  29. exponent/core/remote_execution/languages/python_execution.py +239 -0
  30. exponent/core/remote_execution/languages/shell_streaming.py +226 -0
  31. exponent/core/remote_execution/languages/types.py +20 -0
  32. exponent/core/remote_execution/port_utils.py +73 -0
  33. exponent/core/remote_execution/session.py +128 -0
  34. exponent/core/remote_execution/system_context.py +26 -0
  35. exponent/core/remote_execution/terminal_session.py +375 -0
  36. exponent/core/remote_execution/terminal_types.py +29 -0
  37. exponent/core/remote_execution/tool_execution.py +595 -0
  38. exponent/core/remote_execution/tool_type_utils.py +39 -0
  39. exponent/core/remote_execution/truncation.py +296 -0
  40. exponent/core/remote_execution/types.py +635 -0
  41. exponent/core/remote_execution/utils.py +477 -0
  42. exponent/core/types/__init__.py +0 -0
  43. exponent/core/types/command_data.py +206 -0
  44. exponent/core/types/event_types.py +89 -0
  45. exponent/core/types/generated/__init__.py +0 -0
  46. exponent/core/types/generated/strategy_info.py +213 -0
  47. exponent/migration-docs/login.md +112 -0
  48. exponent/py.typed +4 -0
  49. exponent/utils/__init__.py +0 -0
  50. exponent/utils/colors.py +92 -0
  51. exponent/utils/version.py +289 -0
  52. indent-0.1.26.dist-info/METADATA +38 -0
  53. indent-0.1.26.dist-info/RECORD +55 -0
  54. indent-0.1.26.dist-info/WHEEL +4 -0
  55. 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)