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
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
import webbrowser
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from git import GitCommandError, Repo
|
|
11
|
+
|
|
12
|
+
from exponent.commands.common import (
|
|
13
|
+
redirect_to_login,
|
|
14
|
+
verify_gh_app_installation,
|
|
15
|
+
)
|
|
16
|
+
from exponent.commands.settings import use_settings
|
|
17
|
+
from exponent.commands.types import exponent_cli_group
|
|
18
|
+
from exponent.core.config import Settings
|
|
19
|
+
from exponent.core.remote_execution.git import get_git_info
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@exponent_cli_group()
|
|
23
|
+
def github_app_cli() -> None:
|
|
24
|
+
"""Run AI-powered chat sessions."""
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@github_app_cli.command()
|
|
29
|
+
@use_settings
|
|
30
|
+
def install_github_app(
|
|
31
|
+
settings: Settings,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Start or reconnect to an Exponent session."""
|
|
34
|
+
if not settings.api_key:
|
|
35
|
+
redirect_to_login(settings)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
loop = asyncio.get_event_loop()
|
|
39
|
+
|
|
40
|
+
api_key = settings.api_key
|
|
41
|
+
base_api_url = settings.get_base_api_url()
|
|
42
|
+
base_ws_url = settings.get_base_ws_url()
|
|
43
|
+
|
|
44
|
+
git_info = asyncio.run(get_git_info("."))
|
|
45
|
+
if not git_info:
|
|
46
|
+
raise RuntimeError("Not running inside of valid git repository")
|
|
47
|
+
|
|
48
|
+
install_url = "https://github.com/apps/indent-com/installations/new"
|
|
49
|
+
webbrowser.open(install_url)
|
|
50
|
+
|
|
51
|
+
click.confirm(
|
|
52
|
+
"Press enter once you've installed the github app.",
|
|
53
|
+
default=True,
|
|
54
|
+
abort=True,
|
|
55
|
+
prompt_suffix="",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
click.secho("Verifying installation...", fg="yellow")
|
|
59
|
+
verified = loop.run_until_complete(
|
|
60
|
+
verify_gh_app_installation(api_key, base_api_url, base_ws_url, git_info)
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if verified:
|
|
64
|
+
click.secho("Verified!", fg="green")
|
|
65
|
+
else:
|
|
66
|
+
click.secho("No verification found :(", fg="red")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
click.secho("Creating workflow file...", fg="yellow")
|
|
70
|
+
_create_workflow_yaml()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
WORKFLOW_YAML = """
|
|
74
|
+
name: Indent Action
|
|
75
|
+
|
|
76
|
+
on:
|
|
77
|
+
issue_comment:
|
|
78
|
+
types: [created]
|
|
79
|
+
pull_request_review_comment:
|
|
80
|
+
types: [created]
|
|
81
|
+
issues:
|
|
82
|
+
types: [opened, assigned]
|
|
83
|
+
pull_request_review:
|
|
84
|
+
types: [submitted]
|
|
85
|
+
|
|
86
|
+
jobs:
|
|
87
|
+
indent:
|
|
88
|
+
if: |
|
|
89
|
+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@indent')) ||
|
|
90
|
+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@indent')) ||
|
|
91
|
+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@indent')) ||
|
|
92
|
+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@indent') || contains(github.event.issue.title, '@indent')))
|
|
93
|
+
runs-on: ubuntu-latest
|
|
94
|
+
|
|
95
|
+
steps:
|
|
96
|
+
- name: Generate token for Indent app
|
|
97
|
+
id: generate_token
|
|
98
|
+
uses: actions/create-github-app-token@v1
|
|
99
|
+
with:
|
|
100
|
+
app-id: ${{ secrets.INDENT_APP_ID }}
|
|
101
|
+
private-key: ${{ secrets.INDENT_APP_PRIVATE_KEY }}
|
|
102
|
+
|
|
103
|
+
- name: Respond to mention
|
|
104
|
+
uses: actions/github-script@v7
|
|
105
|
+
with:
|
|
106
|
+
github-token: ${{ steps.generate_token.outputs.token }}
|
|
107
|
+
script: |
|
|
108
|
+
const issue_number = context.payload.issue?.number ||
|
|
109
|
+
context.payload.pull_request?.number ||
|
|
110
|
+
context.payload.review?.pull_request?.number;
|
|
111
|
+
|
|
112
|
+
await github.rest.issues.createComment({
|
|
113
|
+
owner: context.repo.owner,
|
|
114
|
+
repo: context.repo.repo,
|
|
115
|
+
issue_number: issue_number,
|
|
116
|
+
body: 'Hi it\'s me, Indent'
|
|
117
|
+
});
|
|
118
|
+
""".lstrip()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _create_workflow_yaml() -> None:
|
|
122
|
+
git_branch = f"indent-workflow-{uuid4()}"
|
|
123
|
+
workflow_file = "indent-review.yml"
|
|
124
|
+
|
|
125
|
+
# 1. Locate the repository (searches upward until it finds .git).
|
|
126
|
+
repo = Repo(Path.cwd(), search_parent_directories=True)
|
|
127
|
+
if repo.bare or not repo.working_tree_dir:
|
|
128
|
+
sys.exit("Error: cannot operate inside a bare repository.")
|
|
129
|
+
|
|
130
|
+
original_branch = repo.active_branch.name if not repo.head.is_detached else None
|
|
131
|
+
|
|
132
|
+
# 2. Create or reuse the branch, then check it out.
|
|
133
|
+
try:
|
|
134
|
+
branch_ref = repo.create_head(git_branch)
|
|
135
|
+
except GitCommandError:
|
|
136
|
+
branch_ref = repo.heads[git_branch]
|
|
137
|
+
branch_ref.checkout()
|
|
138
|
+
|
|
139
|
+
# 3. Ensure workflow directory exists.
|
|
140
|
+
wf_dir = Path(repo.working_tree_dir) / ".github" / "workflows"
|
|
141
|
+
wf_dir.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
|
|
143
|
+
yml = wf_dir / workflow_file
|
|
144
|
+
if not yml.exists():
|
|
145
|
+
yml.write_text(WORKFLOW_YAML)
|
|
146
|
+
# 5. Stage & commit.
|
|
147
|
+
repo.index.add([str(yml)])
|
|
148
|
+
repo.index.commit("Add Indent workflow template")
|
|
149
|
+
print(
|
|
150
|
+
f"Created {yml.relative_to(repo.working_tree_dir)} "
|
|
151
|
+
f"on branch '{git_branch}'."
|
|
152
|
+
)
|
|
153
|
+
else:
|
|
154
|
+
print(
|
|
155
|
+
f"{yml.relative_to(repo.working_tree_dir)} already exists; nothing to do."
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
subprocess.run(
|
|
159
|
+
["git", "push", "-u", "origin", git_branch],
|
|
160
|
+
cwd=repo.working_tree_dir,
|
|
161
|
+
check=True,
|
|
162
|
+
capture_output=False,
|
|
163
|
+
text=True,
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
pr_url: str | None = None
|
|
167
|
+
pr_title = f"Add Indent workflow ({workflow_file})"
|
|
168
|
+
pr_body = "This PR introduces an Indent github action workflow"
|
|
169
|
+
|
|
170
|
+
def run_gh(cmd: list[str]) -> subprocess.CompletedProcess[str]:
|
|
171
|
+
return subprocess.run(
|
|
172
|
+
["gh", *cmd],
|
|
173
|
+
cwd=repo.working_tree_dir,
|
|
174
|
+
check=True,
|
|
175
|
+
capture_output=True,
|
|
176
|
+
text=True,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Does a PR already exist for this head branch?
|
|
181
|
+
result = run_gh(["pr", "view", git_branch, "--json", "url"])
|
|
182
|
+
pr_url = json.loads(result.stdout)["url"]
|
|
183
|
+
except subprocess.CalledProcessError:
|
|
184
|
+
# No PR yet → create one
|
|
185
|
+
base = original_branch or repo.remotes.origin.refs[0].name.split("/")[-1]
|
|
186
|
+
run_gh(
|
|
187
|
+
[
|
|
188
|
+
"pr",
|
|
189
|
+
"create",
|
|
190
|
+
"--head",
|
|
191
|
+
git_branch,
|
|
192
|
+
"--base",
|
|
193
|
+
base,
|
|
194
|
+
"--title",
|
|
195
|
+
pr_title,
|
|
196
|
+
"--body",
|
|
197
|
+
pr_body,
|
|
198
|
+
]
|
|
199
|
+
)
|
|
200
|
+
# Fetch the newly created URL
|
|
201
|
+
result = run_gh(["pr", "view", git_branch, "--json", "url"])
|
|
202
|
+
pr_url = json.loads(result.stdout)["url"]
|
|
203
|
+
|
|
204
|
+
if pr_url:
|
|
205
|
+
click.secho(f"PR: {pr_url}", fg="green")
|
|
206
|
+
webbrowser.open(pr_url)
|
|
207
|
+
else:
|
|
208
|
+
click.secho("Failed to create PR!", fg="red")
|
|
209
|
+
|
|
210
|
+
if original_branch:
|
|
211
|
+
repo.git.checkout(original_branch)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from websockets.exceptions import ConnectionClosed
|
|
5
|
+
|
|
6
|
+
from exponent.commands.settings import use_settings
|
|
7
|
+
from exponent.commands.shell_commands import LiveView
|
|
8
|
+
from exponent.commands.theme import get_theme
|
|
9
|
+
from exponent.commands.types import exponent_cli_group
|
|
10
|
+
from exponent.core.config import Settings
|
|
11
|
+
from exponent.core.graphql.client import GraphQLClient
|
|
12
|
+
from exponent.core.graphql.subscriptions import INDENT_CHAT_EVENT_STREAM_SUBSCRIPTION
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@exponent_cli_group(hidden=True)
|
|
16
|
+
def listen_cli() -> None:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@listen_cli.command()
|
|
21
|
+
@click.option("--chat-id", help="ID of the chat to listen to", required=True)
|
|
22
|
+
@click.option(
|
|
23
|
+
"--known-event-uuids",
|
|
24
|
+
help="Comma-separated list of known event UUIDs to skip",
|
|
25
|
+
default="",
|
|
26
|
+
)
|
|
27
|
+
@use_settings
|
|
28
|
+
def listen(settings: Settings, chat_id: str, known_event_uuids: str) -> None:
|
|
29
|
+
"""Listen to events from an indent chat session."""
|
|
30
|
+
api_key = settings.api_key
|
|
31
|
+
if not api_key:
|
|
32
|
+
raise click.UsageError("API key is not set")
|
|
33
|
+
|
|
34
|
+
base_api_url = settings.get_base_api_url()
|
|
35
|
+
base_ws_url = settings.get_base_ws_url()
|
|
36
|
+
|
|
37
|
+
gql_client = GraphQLClient(api_key, base_api_url, base_ws_url)
|
|
38
|
+
|
|
39
|
+
# Parse known event UUIDs
|
|
40
|
+
known_uuids = []
|
|
41
|
+
if known_event_uuids:
|
|
42
|
+
known_uuids = [
|
|
43
|
+
uuid.strip() for uuid in known_event_uuids.split(",") if uuid.strip()
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
# Set up the live view with theme
|
|
47
|
+
theme = get_theme(settings.options.use_default_colors)
|
|
48
|
+
live_view = LiveView(theme, render_user_messages=True)
|
|
49
|
+
|
|
50
|
+
asyncio.run(_listen(gql_client, chat_id, known_uuids, live_view))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def _listen(
|
|
54
|
+
gql_client: GraphQLClient,
|
|
55
|
+
chat_id: str,
|
|
56
|
+
known_event_uuids: list[str],
|
|
57
|
+
live_view: LiveView,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Internal listen function that handles the WebSocket subscription."""
|
|
60
|
+
while True:
|
|
61
|
+
try:
|
|
62
|
+
variables = {
|
|
63
|
+
"chatUuid": chat_id,
|
|
64
|
+
"lastKnownFullEventUuid": known_event_uuids[-1]
|
|
65
|
+
if known_event_uuids
|
|
66
|
+
else None,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async for response in gql_client.subscribe(
|
|
70
|
+
INDENT_CHAT_EVENT_STREAM_SUBSCRIPTION, variables
|
|
71
|
+
):
|
|
72
|
+
event = response["indentChatEventStream"]
|
|
73
|
+
kind = event["__typename"]
|
|
74
|
+
|
|
75
|
+
# Handle different event types
|
|
76
|
+
if kind in ["UserEvent", "AssistantEvent", "SystemEvent"]:
|
|
77
|
+
live_view.render_event(kind, event)
|
|
78
|
+
elif kind == "Error":
|
|
79
|
+
print(f"Error: {event.get('message', 'Unknown error')}")
|
|
80
|
+
elif kind == "UnauthenticatedError":
|
|
81
|
+
print(
|
|
82
|
+
f"Authentication error: {event.get('message', 'Unauthenticated')}"
|
|
83
|
+
)
|
|
84
|
+
break
|
|
85
|
+
else:
|
|
86
|
+
print(f"Unknown event type: {kind}")
|
|
87
|
+
|
|
88
|
+
except ConnectionClosed:
|
|
89
|
+
print("WebSocket disconnected, reconnecting...")
|
|
90
|
+
await asyncio.sleep(1)
|
|
91
|
+
except KeyboardInterrupt:
|
|
92
|
+
print("\nDisconnecting...")
|
|
93
|
+
break
|
|
94
|
+
except Exception as e:
|
|
95
|
+
print(f"Error: {e}")
|
|
96
|
+
await asyncio.sleep(1)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from exponent.commands.common import (
|
|
8
|
+
check_inside_git_repo,
|
|
9
|
+
check_running_from_home_directory,
|
|
10
|
+
check_ssl,
|
|
11
|
+
create_chat,
|
|
12
|
+
inside_ssh_session,
|
|
13
|
+
redirect_to_login,
|
|
14
|
+
start_client,
|
|
15
|
+
)
|
|
16
|
+
from exponent.commands.settings import use_settings
|
|
17
|
+
from exponent.commands.types import exponent_cli_group
|
|
18
|
+
from exponent.commands.utils import (
|
|
19
|
+
ConnectionTracker,
|
|
20
|
+
Spinner,
|
|
21
|
+
launch_exponent_browser,
|
|
22
|
+
print_exponent_message,
|
|
23
|
+
)
|
|
24
|
+
from exponent.core.config import Settings
|
|
25
|
+
from exponent.core.remote_execution.client import (
|
|
26
|
+
REMOTE_EXECUTION_CLIENT_EXIT_INFO,
|
|
27
|
+
SwitchCLIChat,
|
|
28
|
+
WSDisconnected,
|
|
29
|
+
)
|
|
30
|
+
from exponent.core.remote_execution.types import ChatSource
|
|
31
|
+
from exponent.core.remote_execution.utils import assert_unreachable
|
|
32
|
+
from exponent.utils.version import check_exponent_version_and_upgrade
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
# this is an optional dependency for python <3.11
|
|
36
|
+
from async_timeout import timeout
|
|
37
|
+
except ImportError: # pragma: no cover
|
|
38
|
+
from asyncio import timeout
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@exponent_cli_group()
|
|
42
|
+
def run_cli() -> None:
|
|
43
|
+
"""Run AI-powered chat sessions."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@run_cli.command()
|
|
48
|
+
@click.option(
|
|
49
|
+
"--chat-id",
|
|
50
|
+
help="ID of an existing chat session to reconnect",
|
|
51
|
+
required=False,
|
|
52
|
+
prompt_required=False,
|
|
53
|
+
prompt="Enter the chat ID to reconnect to",
|
|
54
|
+
)
|
|
55
|
+
@click.option(
|
|
56
|
+
"--prompt",
|
|
57
|
+
help="Start a chat with a given prompt.",
|
|
58
|
+
)
|
|
59
|
+
@click.option(
|
|
60
|
+
"--workflow-id",
|
|
61
|
+
hidden=True,
|
|
62
|
+
required=False,
|
|
63
|
+
)
|
|
64
|
+
@use_settings
|
|
65
|
+
def run(
|
|
66
|
+
settings: Settings,
|
|
67
|
+
chat_id: str | None = None,
|
|
68
|
+
prompt: str | None = None,
|
|
69
|
+
workflow_id: str | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Start or reconnect to an Exponent session."""
|
|
72
|
+
check_exponent_version_and_upgrade(settings)
|
|
73
|
+
|
|
74
|
+
if not settings.api_key:
|
|
75
|
+
redirect_to_login(settings)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
loop = asyncio.get_event_loop()
|
|
79
|
+
|
|
80
|
+
check_running_from_home_directory()
|
|
81
|
+
check_ssl()
|
|
82
|
+
loop.run_until_complete(check_inside_git_repo(settings))
|
|
83
|
+
|
|
84
|
+
api_key = settings.api_key
|
|
85
|
+
base_url = settings.base_url
|
|
86
|
+
base_api_url = settings.get_base_api_url()
|
|
87
|
+
base_ws_url = settings.get_base_ws_url()
|
|
88
|
+
|
|
89
|
+
chat_uuid = chat_id or loop.run_until_complete(
|
|
90
|
+
create_chat(api_key, base_api_url, base_ws_url, ChatSource.CLI_RUN)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
if chat_uuid is None:
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
if (
|
|
97
|
+
not prompt
|
|
98
|
+
and (not inside_ssh_session())
|
|
99
|
+
and (not workflow_id)
|
|
100
|
+
# If the user specified a chat ID, they probably don't want to re-launch the chat
|
|
101
|
+
and (not chat_id)
|
|
102
|
+
):
|
|
103
|
+
# Open the chat in the browser
|
|
104
|
+
launch_exponent_browser(settings.environment, base_url, chat_uuid)
|
|
105
|
+
|
|
106
|
+
while True:
|
|
107
|
+
result = run_chat(loop, api_key, chat_uuid, settings, prompt, workflow_id)
|
|
108
|
+
if result is None or isinstance(result, WSDisconnected):
|
|
109
|
+
# NOTE: None here means that handle_connection_changes exited
|
|
110
|
+
# first. We should likely have a different message for this.
|
|
111
|
+
if result and result.error_message:
|
|
112
|
+
click.secho(f"Error: {result.error_message}", fg="red")
|
|
113
|
+
sys.exit(10)
|
|
114
|
+
else:
|
|
115
|
+
print("Disconnected upon user request, shutting down...")
|
|
116
|
+
break
|
|
117
|
+
elif isinstance(result, SwitchCLIChat):
|
|
118
|
+
chat_uuid = result.new_chat_uuid
|
|
119
|
+
print("\nSwitching chats...")
|
|
120
|
+
else:
|
|
121
|
+
assert_unreachable(result)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def run_chat(
|
|
125
|
+
loop: asyncio.AbstractEventLoop,
|
|
126
|
+
api_key: str,
|
|
127
|
+
chat_uuid: str,
|
|
128
|
+
settings: Settings,
|
|
129
|
+
prompt: str | None,
|
|
130
|
+
workflow_id: str | None,
|
|
131
|
+
) -> REMOTE_EXECUTION_CLIENT_EXIT_INFO | None:
|
|
132
|
+
start_ts = time.time()
|
|
133
|
+
base_url = settings.base_url
|
|
134
|
+
base_api_url = settings.get_base_api_url()
|
|
135
|
+
base_ws_url = settings.get_base_ws_url()
|
|
136
|
+
|
|
137
|
+
print_exponent_message(base_url, chat_uuid)
|
|
138
|
+
print()
|
|
139
|
+
|
|
140
|
+
connection_tracker = ConnectionTracker()
|
|
141
|
+
|
|
142
|
+
client_fut = loop.create_task(
|
|
143
|
+
start_client(
|
|
144
|
+
api_key,
|
|
145
|
+
base_url,
|
|
146
|
+
base_api_url,
|
|
147
|
+
base_ws_url,
|
|
148
|
+
chat_uuid,
|
|
149
|
+
prompt=prompt,
|
|
150
|
+
workflow_id=workflow_id,
|
|
151
|
+
connection_tracker=connection_tracker,
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
conn_fut = loop.create_task(handle_connection_changes(connection_tracker, start_ts))
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
done, _ = loop.run_until_complete(
|
|
159
|
+
asyncio.wait({client_fut, conn_fut}, return_when=asyncio.FIRST_COMPLETED)
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
if client_fut in done:
|
|
163
|
+
return client_fut.result()
|
|
164
|
+
else:
|
|
165
|
+
return None
|
|
166
|
+
finally:
|
|
167
|
+
for task in asyncio.all_tasks(loop):
|
|
168
|
+
task.cancel()
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
loop.run_until_complete(asyncio.wait(asyncio.all_tasks(loop)))
|
|
172
|
+
except asyncio.CancelledError:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def handle_connection_changes(
|
|
177
|
+
connection_tracker: ConnectionTracker, start_ts: float
|
|
178
|
+
) -> None:
|
|
179
|
+
try:
|
|
180
|
+
async with timeout(5):
|
|
181
|
+
assert await connection_tracker.next_change()
|
|
182
|
+
print(ready_message(start_ts))
|
|
183
|
+
except TimeoutError:
|
|
184
|
+
spinner = Spinner("Connecting...")
|
|
185
|
+
spinner.show()
|
|
186
|
+
assert await connection_tracker.next_change()
|
|
187
|
+
spinner.hide()
|
|
188
|
+
print(ready_message(start_ts))
|
|
189
|
+
|
|
190
|
+
while True:
|
|
191
|
+
assert not await connection_tracker.next_change()
|
|
192
|
+
|
|
193
|
+
print("Disconnected...", end="")
|
|
194
|
+
await asyncio.sleep(1)
|
|
195
|
+
spinner = Spinner("Reconnecting...")
|
|
196
|
+
spinner.show()
|
|
197
|
+
assert await connection_tracker.next_change()
|
|
198
|
+
spinner.hide()
|
|
199
|
+
print("\x1b[1;32m✓ Reconnected", end="")
|
|
200
|
+
sys.stdout.flush()
|
|
201
|
+
await asyncio.sleep(1)
|
|
202
|
+
print("\r\x1b[0m\x1b[2K", end="")
|
|
203
|
+
sys.stdout.flush()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def ready_message(start_ts: float) -> str:
|
|
207
|
+
elapsed = round(time.time() - start_ts, 2)
|
|
208
|
+
return f"\x1b[32m✓\x1b[0m Ready in {elapsed}s"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from exponent.commands.utils import (
|
|
8
|
+
print_editable_install_forced_prod_warning,
|
|
9
|
+
print_editable_install_warning,
|
|
10
|
+
)
|
|
11
|
+
from exponent.core.config import (
|
|
12
|
+
Environment,
|
|
13
|
+
get_settings,
|
|
14
|
+
is_editable_install,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def use_settings(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
19
|
+
@click.option(
|
|
20
|
+
"--prod",
|
|
21
|
+
is_flag=True,
|
|
22
|
+
hidden=True,
|
|
23
|
+
help="Use production URLs even if in editable mode",
|
|
24
|
+
)
|
|
25
|
+
@click.option(
|
|
26
|
+
"--staging",
|
|
27
|
+
is_flag=True,
|
|
28
|
+
hidden=True,
|
|
29
|
+
help="Use staging URLs even if in editable mode",
|
|
30
|
+
)
|
|
31
|
+
@wraps(f)
|
|
32
|
+
def decorated_function(*args: Any, **kwargs: Any) -> Any:
|
|
33
|
+
prod = kwargs.pop("prod", False)
|
|
34
|
+
staging = kwargs.pop("staging", False)
|
|
35
|
+
settings = get_settings(use_prod=prod, use_staging=staging)
|
|
36
|
+
|
|
37
|
+
if is_editable_install() and not (prod or staging):
|
|
38
|
+
assert settings.environment == Environment.development
|
|
39
|
+
print_editable_install_warning(settings)
|
|
40
|
+
|
|
41
|
+
return f(*args, settings=settings, **kwargs)
|
|
42
|
+
|
|
43
|
+
return decorated_function
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def use_prod_settings(f: Callable[..., Any]) -> Callable[..., Any]:
|
|
47
|
+
@wraps(f)
|
|
48
|
+
def decorated_function(*args: Any, **kwargs: Any) -> Any:
|
|
49
|
+
settings = get_settings(use_prod=True)
|
|
50
|
+
|
|
51
|
+
if is_editable_install():
|
|
52
|
+
print_editable_install_forced_prod_warning(settings)
|
|
53
|
+
|
|
54
|
+
return f(*args, settings=settings, **kwargs)
|
|
55
|
+
|
|
56
|
+
return decorated_function
|