indent 0.0.8__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 +1 -0
- exponent/cli.py +112 -0
- exponent/commands/cloud_commands.py +85 -0
- exponent/commands/common.py +434 -0
- exponent/commands/config_commands.py +581 -0
- exponent/commands/github_app_commands.py +211 -0
- exponent/commands/listen_commands.py +96 -0
- exponent/commands/run_commands.py +208 -0
- exponent/commands/settings.py +56 -0
- exponent/commands/shell_commands.py +2840 -0
- exponent/commands/theme.py +246 -0
- exponent/commands/types.py +111 -0
- exponent/commands/upgrade.py +29 -0
- exponent/commands/utils.py +236 -0
- exponent/core/config.py +180 -0
- exponent/core/graphql/__init__.py +0 -0
- exponent/core/graphql/client.py +59 -0
- exponent/core/graphql/cloud_config_queries.py +77 -0
- exponent/core/graphql/get_chats_query.py +47 -0
- exponent/core/graphql/github_config_queries.py +56 -0
- exponent/core/graphql/mutations.py +75 -0
- exponent/core/graphql/queries.py +110 -0
- exponent/core/graphql/subscriptions.py +452 -0
- exponent/core/remote_execution/checkpoints.py +212 -0
- exponent/core/remote_execution/cli_rpc_types.py +214 -0
- exponent/core/remote_execution/client.py +545 -0
- exponent/core/remote_execution/code_execution.py +58 -0
- exponent/core/remote_execution/command_execution.py +105 -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 +410 -0
- exponent/core/remote_execution/files.py +415 -0
- exponent/core/remote_execution/git.py +268 -0
- exponent/core/remote_execution/languages/python_execution.py +239 -0
- exponent/core/remote_execution/languages/shell_streaming.py +221 -0
- exponent/core/remote_execution/languages/types.py +20 -0
- exponent/core/remote_execution/session.py +128 -0
- exponent/core/remote_execution/system_context.py +54 -0
- exponent/core/remote_execution/tool_execution.py +289 -0
- exponent/core/remote_execution/truncation.py +284 -0
- exponent/core/remote_execution/types.py +670 -0
- exponent/core/remote_execution/utils.py +600 -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 +225 -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.0.8.dist-info/METADATA +36 -0
- indent-0.0.8.dist-info/RECORD +56 -0
- indent-0.0.8.dist-info/WHEEL +4 -0
- indent-0.0.8.dist-info/entry_points.txt +2 -0
exponent/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.8" # Keep in sync with pyproject.toml
|
exponent/cli.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from click import Context, HelpFormatter
|
|
3
|
+
|
|
4
|
+
from exponent.commands.cloud_commands import cloud_cli
|
|
5
|
+
from exponent.commands.common import set_log_level
|
|
6
|
+
from exponent.commands.config_commands import config_cli
|
|
7
|
+
from exponent.commands.github_app_commands import github_app_cli
|
|
8
|
+
from exponent.commands.listen_commands import listen_cli
|
|
9
|
+
from exponent.commands.run_commands import run_cli
|
|
10
|
+
from exponent.commands.shell_commands import shell_cli
|
|
11
|
+
from exponent.commands.types import ExponentGroup, exponent_cli_group
|
|
12
|
+
from exponent.commands.upgrade import upgrade_cli
|
|
13
|
+
from exponent.utils.version import (
|
|
14
|
+
get_installed_version,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@exponent_cli_group()
|
|
19
|
+
@click.version_option(get_installed_version(), prog_name="Indent CLI")
|
|
20
|
+
@click.pass_context
|
|
21
|
+
def cli(ctx: Context) -> None:
|
|
22
|
+
"""
|
|
23
|
+
Indent: Your AI pair programmer.
|
|
24
|
+
|
|
25
|
+
Run indent run to start
|
|
26
|
+
"""
|
|
27
|
+
set_log_level()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
sources: list[ExponentGroup] = [
|
|
31
|
+
config_cli, # Configuration commands
|
|
32
|
+
run_cli, # Run AI chat commands
|
|
33
|
+
shell_cli, # Shell interaction commands
|
|
34
|
+
upgrade_cli, # Upgrade-related commands
|
|
35
|
+
cloud_cli, # Cloud commands
|
|
36
|
+
listen_cli, # Listen to chat events
|
|
37
|
+
github_app_cli, # Setup github app installation
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
for source in sources:
|
|
41
|
+
for command in source.commands.values():
|
|
42
|
+
cli.add_command(command)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def format_commands(
|
|
46
|
+
ctx: Context, formatter: HelpFormatter, include_hidden: bool = False
|
|
47
|
+
) -> None:
|
|
48
|
+
commands = []
|
|
49
|
+
hidden_commands = []
|
|
50
|
+
for cmd_name in cli.list_commands(ctx):
|
|
51
|
+
cmd = cli.get_command(ctx, cmd_name)
|
|
52
|
+
if cmd is None:
|
|
53
|
+
continue
|
|
54
|
+
if cmd.hidden:
|
|
55
|
+
hidden_commands.append((cmd_name, cmd))
|
|
56
|
+
else:
|
|
57
|
+
commands.append((cmd_name, cmd))
|
|
58
|
+
|
|
59
|
+
if commands:
|
|
60
|
+
max_cmd_length = (
|
|
61
|
+
max(len(cmd_name) for cmd_name, _ in commands) if commands else 0
|
|
62
|
+
)
|
|
63
|
+
limit = (
|
|
64
|
+
(formatter.width or 80) - 6 - max_cmd_length
|
|
65
|
+
) # Default width to 80 if None
|
|
66
|
+
rows = []
|
|
67
|
+
for cmd_name, cmd in commands:
|
|
68
|
+
help_text = cmd.get_short_help_str(limit)
|
|
69
|
+
rows.append((cmd_name, help_text))
|
|
70
|
+
|
|
71
|
+
with formatter.section("Commands"):
|
|
72
|
+
formatter.write_dl(rows)
|
|
73
|
+
|
|
74
|
+
if include_hidden and hidden_commands:
|
|
75
|
+
max_cmd_length = (
|
|
76
|
+
max(len(cmd_name) for cmd_name, _ in hidden_commands)
|
|
77
|
+
if hidden_commands
|
|
78
|
+
else 0
|
|
79
|
+
)
|
|
80
|
+
limit = (
|
|
81
|
+
(formatter.width or 80) - 6 - max_cmd_length
|
|
82
|
+
) # Default width to 80 if None
|
|
83
|
+
hidden_rows = []
|
|
84
|
+
for cmd_name, cmd in hidden_commands:
|
|
85
|
+
help_text = cmd.get_short_help_str(limit)
|
|
86
|
+
hidden_rows.append((cmd_name, help_text))
|
|
87
|
+
|
|
88
|
+
with formatter.section("Hidden Commands"):
|
|
89
|
+
formatter.write_dl(hidden_rows)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@cli.command(hidden=True)
|
|
93
|
+
@click.pass_context
|
|
94
|
+
def hidden(ctx: Context) -> None:
|
|
95
|
+
"""Show all commands, including hidden ones."""
|
|
96
|
+
formatter = ctx.make_formatter()
|
|
97
|
+
with formatter.section("Usage"):
|
|
98
|
+
if ctx.parent and ctx.parent.command:
|
|
99
|
+
formatter.write_usage(
|
|
100
|
+
ctx.parent.command.name or "exponent", "COMMAND [ARGS]..."
|
|
101
|
+
)
|
|
102
|
+
formatter.write_paragraph()
|
|
103
|
+
with formatter.indentation():
|
|
104
|
+
if cli.help:
|
|
105
|
+
formatter.write_text(cli.help)
|
|
106
|
+
formatter.write_paragraph()
|
|
107
|
+
format_commands(ctx, formatter, include_hidden=True)
|
|
108
|
+
click.echo(formatter.getvalue().rstrip("\n"))
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
cli()
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
from exponent.commands.common import (
|
|
7
|
+
check_inside_git_repo,
|
|
8
|
+
check_running_from_home_directory,
|
|
9
|
+
check_ssl,
|
|
10
|
+
create_cloud_chat,
|
|
11
|
+
redirect_to_login,
|
|
12
|
+
start_chat_turn,
|
|
13
|
+
)
|
|
14
|
+
from exponent.commands.settings import use_settings
|
|
15
|
+
from exponent.commands.types import exponent_cli_group
|
|
16
|
+
from exponent.commands.utils import (
|
|
17
|
+
launch_exponent_browser,
|
|
18
|
+
print_exponent_message,
|
|
19
|
+
)
|
|
20
|
+
from exponent.core.config import Settings
|
|
21
|
+
from exponent.utils.version import check_exponent_version_and_upgrade
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@exponent_cli_group(hidden=True)
|
|
25
|
+
def cloud_cli() -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@cloud_cli.command(hidden=True)
|
|
30
|
+
@click.option(
|
|
31
|
+
"--cloud-config-id",
|
|
32
|
+
help="ID of an existing cloud config to reconnect",
|
|
33
|
+
required=True,
|
|
34
|
+
)
|
|
35
|
+
@click.option(
|
|
36
|
+
"--prompt",
|
|
37
|
+
help="Prompt to kick off the cloud session.",
|
|
38
|
+
required=True,
|
|
39
|
+
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--background",
|
|
42
|
+
"-b",
|
|
43
|
+
help="Start the cloud session without launching the Exponent UI",
|
|
44
|
+
is_flag=True,
|
|
45
|
+
default=False,
|
|
46
|
+
)
|
|
47
|
+
@use_settings
|
|
48
|
+
def cloud(
|
|
49
|
+
settings: Settings,
|
|
50
|
+
cloud_config_id: str,
|
|
51
|
+
prompt: str,
|
|
52
|
+
background: bool,
|
|
53
|
+
) -> None:
|
|
54
|
+
check_exponent_version_and_upgrade(settings)
|
|
55
|
+
|
|
56
|
+
if not settings.api_key:
|
|
57
|
+
redirect_to_login(settings)
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
loop = asyncio.get_event_loop()
|
|
61
|
+
|
|
62
|
+
check_running_from_home_directory()
|
|
63
|
+
loop.run_until_complete(check_inside_git_repo(settings))
|
|
64
|
+
check_ssl()
|
|
65
|
+
|
|
66
|
+
api_key = settings.api_key
|
|
67
|
+
base_url = settings.base_url
|
|
68
|
+
base_api_url = settings.get_base_api_url()
|
|
69
|
+
base_ws_url = settings.get_base_ws_url()
|
|
70
|
+
|
|
71
|
+
chat_uuid = loop.run_until_complete(
|
|
72
|
+
create_cloud_chat(api_key, base_api_url, base_ws_url, cloud_config_id)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if chat_uuid is None:
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
loop.run_until_complete(
|
|
79
|
+
start_chat_turn(api_key, base_api_url, base_ws_url, chat_uuid, prompt)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
print_exponent_message(base_url, chat_uuid)
|
|
83
|
+
|
|
84
|
+
if not background:
|
|
85
|
+
launch_exponent_browser(settings.environment, base_url, chat_uuid)
|
|
@@ -0,0 +1,434 @@
|
|
|
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.types import ChatSource, GitInfo
|
|
41
|
+
|
|
42
|
+
load_dotenv()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def set_log_level() -> None:
|
|
46
|
+
settings = get_settings()
|
|
47
|
+
logging.basicConfig(level=getattr(logging, settings.log_level), stream=sys.stdout)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def redirect_to_login(settings: Settings, cause: str = "detected") -> None:
|
|
51
|
+
if inside_ssh_session():
|
|
52
|
+
click.echo(f"No API Key {cause}, run 'indent login --key <API-KEY>'")
|
|
53
|
+
else:
|
|
54
|
+
click.echo("No API Key detected, redirecting to login...")
|
|
55
|
+
webbrowser.open(f"{settings.base_url}/settings")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def inside_ssh_session() -> bool:
|
|
59
|
+
return (os.environ.get("SSH_TTY") or os.environ.get("SSH_TTY")) is not None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def inside_git_repo() -> bool:
|
|
63
|
+
git_info = await get_git_info(os.getcwd())
|
|
64
|
+
|
|
65
|
+
return git_info is not None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def missing_ssl_certs() -> bool:
|
|
69
|
+
if platform.system().lower() != "darwin":
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
openssl_dir, openssl_cafile = os.path.split(
|
|
73
|
+
ssl.get_default_verify_paths().openssl_cafile
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return not os.path.exists(os.path.join(openssl_dir, openssl_cafile))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def install_ssl_certs() -> None:
|
|
80
|
+
STAT_0o775 = (
|
|
81
|
+
stat.S_IRUSR
|
|
82
|
+
| stat.S_IWUSR
|
|
83
|
+
| stat.S_IXUSR
|
|
84
|
+
| stat.S_IRGRP
|
|
85
|
+
| stat.S_IWGRP
|
|
86
|
+
| stat.S_IXGRP
|
|
87
|
+
| stat.S_IROTH
|
|
88
|
+
| stat.S_IXOTH
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
openssl_dir, openssl_cafile = os.path.split(
|
|
92
|
+
ssl.get_default_verify_paths().openssl_cafile
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
cwd = os.getcwd()
|
|
96
|
+
# change working directory to the default SSL directory
|
|
97
|
+
os.chdir(openssl_dir)
|
|
98
|
+
relpath_to_certifi_cafile = os.path.relpath(certifi.where())
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
os.remove(openssl_cafile)
|
|
102
|
+
except FileNotFoundError:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
click.echo(" -- creating symlink to certifi certificate bundle")
|
|
106
|
+
os.symlink(relpath_to_certifi_cafile, openssl_cafile)
|
|
107
|
+
click.echo(" -- setting permissions")
|
|
108
|
+
os.chmod(openssl_cafile, STAT_0o775)
|
|
109
|
+
click.echo(" -- update complete")
|
|
110
|
+
os.chdir(cwd)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def check_ssl() -> None:
|
|
114
|
+
if missing_ssl_certs():
|
|
115
|
+
click.confirm(
|
|
116
|
+
"Missing root SSL certs required for python to make HTTP requests, "
|
|
117
|
+
"install certifi certificates now?",
|
|
118
|
+
abort=True,
|
|
119
|
+
default=True,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
install_ssl_certs()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
async def check_inside_git_repo(settings: Settings) -> None:
|
|
126
|
+
if not settings.options.git_warning_disabled and not (await inside_git_repo()):
|
|
127
|
+
click.echo(
|
|
128
|
+
click.style(
|
|
129
|
+
"\nWarning: Running from a folder that is not a git repository",
|
|
130
|
+
fg="yellow",
|
|
131
|
+
bold=True,
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
click.echo(
|
|
135
|
+
"This is a check to make sure you are running Exponent from the root of your project."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
click.echo(f"\nCurrent directory: {click.style(os.getcwd(), fg='cyan')}")
|
|
139
|
+
|
|
140
|
+
click.echo("\nRecommendation:")
|
|
141
|
+
click.echo(" Run Exponent from the root directory of your codebase.")
|
|
142
|
+
click.echo("\nExample:")
|
|
143
|
+
click.echo(
|
|
144
|
+
f" If your project is in {click.style('~/my-project', fg='cyan')}, run:"
|
|
145
|
+
)
|
|
146
|
+
click.echo(f" {click.style('cd ~/my-project && exponent run', fg='green')}")
|
|
147
|
+
|
|
148
|
+
# Tell the user they can run exponent config --no-git-warning to disable this check
|
|
149
|
+
click.echo(
|
|
150
|
+
f"\nYou can run {click.style('exponent config --set-git-warning-disabled', fg='green')} to disable this check."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
if not click.confirm(
|
|
154
|
+
click.style(
|
|
155
|
+
f"\nDo you want to continue running Exponent from {os.getcwd()}?",
|
|
156
|
+
fg="yellow",
|
|
157
|
+
),
|
|
158
|
+
default=True,
|
|
159
|
+
):
|
|
160
|
+
click.echo(click.style("\nOperation aborted.", fg="red"))
|
|
161
|
+
raise click.Abort()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def check_running_from_home_directory(require_confirmation: bool = True) -> bool:
|
|
165
|
+
if os.path.expanduser("~") == os.getcwd():
|
|
166
|
+
click.echo(
|
|
167
|
+
click.style(
|
|
168
|
+
"\nWarning: Running Exponent from Home Directory",
|
|
169
|
+
fg="yellow",
|
|
170
|
+
bold=True,
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
click.echo(
|
|
174
|
+
"Running Exponent from your home directory can cause unexpected issues."
|
|
175
|
+
)
|
|
176
|
+
click.echo("\nRecommendation:")
|
|
177
|
+
click.echo(" Run Exponent from the root directory of your codebase.")
|
|
178
|
+
click.echo("\nExample:")
|
|
179
|
+
click.echo(
|
|
180
|
+
f" If your project is in {click.style('~/my-project', fg='cyan')}, run:"
|
|
181
|
+
)
|
|
182
|
+
click.echo(f" {click.style('cd ~/my-project && exponent run', fg='green')}")
|
|
183
|
+
|
|
184
|
+
if require_confirmation:
|
|
185
|
+
if not click.confirm(
|
|
186
|
+
click.style(
|
|
187
|
+
f"\nDo you want to continue running Exponent from {os.getcwd()}?",
|
|
188
|
+
fg="yellow",
|
|
189
|
+
),
|
|
190
|
+
default=True,
|
|
191
|
+
):
|
|
192
|
+
click.echo(click.style("\nOperation aborted.", fg="red"))
|
|
193
|
+
raise click.Abort()
|
|
194
|
+
else:
|
|
195
|
+
click.echo("\n") # Newline to separate from next command
|
|
196
|
+
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def run_until_complete(coro: Coroutine[Any, Any, Any]) -> Any:
|
|
203
|
+
loop = asyncio.get_event_loop()
|
|
204
|
+
task = loop.create_task(coro)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
loop.run_until_complete(task)
|
|
208
|
+
except KeyboardInterrupt:
|
|
209
|
+
task.cancel()
|
|
210
|
+
try:
|
|
211
|
+
loop.run_until_complete(task)
|
|
212
|
+
except asyncio.CancelledError:
|
|
213
|
+
pass
|
|
214
|
+
except ExponentError as e:
|
|
215
|
+
click.secho(f"Encountered error: {e}", fg="red")
|
|
216
|
+
click.secho(
|
|
217
|
+
"The Exponent team has been notified, "
|
|
218
|
+
"please try again and reach out if the problem persists.",
|
|
219
|
+
fg="yellow",
|
|
220
|
+
)
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
except HandledExponentError as e:
|
|
223
|
+
click.secho(str(e), fg="red")
|
|
224
|
+
sys.exit(1)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
async def run_client_connection(
|
|
228
|
+
client: RemoteExecutionClient,
|
|
229
|
+
chat_uuid: str,
|
|
230
|
+
connection_tracker: ConnectionTracker | None = None,
|
|
231
|
+
) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO:
|
|
232
|
+
return await client.run_connection(chat_uuid, connection_tracker)
|
|
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
|
+
) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO:
|
|
316
|
+
async with RemoteExecutionClient.session(
|
|
317
|
+
api_key=api_key,
|
|
318
|
+
base_url=base_api_url,
|
|
319
|
+
base_ws_url=base_ws_url,
|
|
320
|
+
working_directory=os.getcwd(),
|
|
321
|
+
file_cache=file_cache,
|
|
322
|
+
) as client:
|
|
323
|
+
main_coro = run_client_connection(client, chat_uuid, connection_tracker)
|
|
324
|
+
aux_coros: list[Coroutine[Any, Any, None]] = []
|
|
325
|
+
|
|
326
|
+
if prompt:
|
|
327
|
+
# If given a prompt, we also need to send a request
|
|
328
|
+
# to kick off the initial turn loop for the chat
|
|
329
|
+
raise NotImplementedError("Kicking off with initial prompt not implemented")
|
|
330
|
+
elif workflow_id:
|
|
331
|
+
# Similarly, if given a workflow ID, we need to send
|
|
332
|
+
# a request to kick off the workflow
|
|
333
|
+
aux_coros.append(run_workflow(base_url, client, chat_uuid, workflow_id))
|
|
334
|
+
|
|
335
|
+
client_result, *_ = await asyncio.gather(main_coro, *aux_coros)
|
|
336
|
+
return cast(REMOTE_EXECUTION_CLIENT_EXIT_INFO, client_result)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# Helper functions
|
|
340
|
+
async def create_chat(
|
|
341
|
+
api_key: str, base_api_url: str, base_ws_url: str, chat_source: ChatSource
|
|
342
|
+
) -> str | None:
|
|
343
|
+
try:
|
|
344
|
+
async with RemoteExecutionClient.session(
|
|
345
|
+
api_key, base_api_url, base_ws_url, os.getcwd()
|
|
346
|
+
) as client:
|
|
347
|
+
chat = await client.create_chat(chat_source)
|
|
348
|
+
return chat.chat_uuid
|
|
349
|
+
except (httpx.ConnectError, ExponentError) as e:
|
|
350
|
+
click.secho(f"Error: {e}", fg="red")
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
async def get_gh_app_installation_token(
|
|
355
|
+
api_key: str, base_api_url: str, base_ws_url: str, git_info: GitInfo
|
|
356
|
+
) -> dict[str, Any] | None:
|
|
357
|
+
try:
|
|
358
|
+
async with RemoteExecutionClient.session(
|
|
359
|
+
api_key, base_api_url, base_ws_url, os.getcwd()
|
|
360
|
+
) as client:
|
|
361
|
+
return await client.get_gh_installation_token(git_info)
|
|
362
|
+
except (httpx.ConnectError, ExponentError) as e:
|
|
363
|
+
click.secho(f"Error: {e}", fg="red")
|
|
364
|
+
return None
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
async def verify_gh_app_installation(
|
|
368
|
+
api_key: str, base_api_url: str, base_ws_url: str, git_info: GitInfo
|
|
369
|
+
) -> bool:
|
|
370
|
+
try:
|
|
371
|
+
async with RemoteExecutionClient.session(
|
|
372
|
+
api_key, base_api_url, base_ws_url, os.getcwd()
|
|
373
|
+
) as client:
|
|
374
|
+
res = await client.get_gh_installation_token(git_info)
|
|
375
|
+
if "token" in res:
|
|
376
|
+
return True
|
|
377
|
+
except (httpx.ConnectError, ExponentError) as e:
|
|
378
|
+
click.secho(f"Error: {e}", fg="red")
|
|
379
|
+
return False
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
async def set_login_complete(api_key: str, base_api_url: str, base_ws_url: str) -> None:
|
|
383
|
+
graphql_client = GraphQLClient(
|
|
384
|
+
api_key=api_key, base_api_url=base_api_url, base_ws_url=base_ws_url
|
|
385
|
+
)
|
|
386
|
+
result = await graphql_client.execute(
|
|
387
|
+
SET_LOGIN_COMPLETE_MUTATION, {}, "SetLoginComplete"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
data = result["setLoginComplete"]
|
|
391
|
+
|
|
392
|
+
if data["__typename"] == "UnauthenticatedError":
|
|
393
|
+
raise HandledExponentError(f"Verification failed: {data['message']}")
|
|
394
|
+
|
|
395
|
+
if data["userApiKey"] != api_key:
|
|
396
|
+
# We got a user object back, but the api_key is different
|
|
397
|
+
# than the one used in the user's request...
|
|
398
|
+
# This should never happen
|
|
399
|
+
raise HandledExponentError(
|
|
400
|
+
"Invalid API key, login to https://exponent.run to find your API key."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
async def refresh_api_key_task(
|
|
405
|
+
api_key: str,
|
|
406
|
+
base_api_url: str,
|
|
407
|
+
base_ws_url: str,
|
|
408
|
+
) -> None:
|
|
409
|
+
graphql_client = GraphQLClient(api_key, base_api_url, base_ws_url)
|
|
410
|
+
result = await graphql_client.execute(REFRESH_API_KEY_MUTATION)
|
|
411
|
+
|
|
412
|
+
if "refreshApiKey" in result:
|
|
413
|
+
if "message" in result["refreshApiKey"]:
|
|
414
|
+
# Handle error case
|
|
415
|
+
click.secho(f"Error: {result['refreshApiKey']['message']}", fg="red")
|
|
416
|
+
return
|
|
417
|
+
|
|
418
|
+
if "userApiKey" in result["refreshApiKey"]:
|
|
419
|
+
# Handle success case
|
|
420
|
+
new_api_key = result["refreshApiKey"]["userApiKey"]
|
|
421
|
+
settings = get_settings()
|
|
422
|
+
|
|
423
|
+
click.echo(f"Saving new API Key to {settings.config_file_path}")
|
|
424
|
+
settings.update_api_key(new_api_key)
|
|
425
|
+
settings.write_settings_to_config_file()
|
|
426
|
+
|
|
427
|
+
click.secho(
|
|
428
|
+
"API key has been refreshed and saved successfully!", fg="green"
|
|
429
|
+
)
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
# Handle unexpected response
|
|
433
|
+
click.secho("Failed to refresh API key: Unexpected response", fg="red")
|
|
434
|
+
click.echo(result)
|