Skip to content

Commit 3aad968

Browse files
authored
Merge pull request #33 from code-yeongyu/feature/architecture
Add reconfigure feature when `session_key` is not usable
2 parents cd782ab + e0717de commit 3aad968

11 files changed

+227
-109
lines changed

aishell/cli.py

Lines changed: 0 additions & 94 deletions
This file was deleted.

aishell/cli/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
from .aishell import aishell_command as _ # noqa
2+
from .cli_app import cli_app as cli_app

aishell/cli/aishell.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import os
2+
import time
3+
from typing import Optional
4+
5+
import rich
6+
import typer
7+
from rich.console import Console
8+
9+
from aishell.models import AiShellConfigModel
10+
from aishell.models.language_model import LanguageModel
11+
from aishell.query_clients import GPT3Client, OfficialChatGPTClient, ReverseEngineeredChatGPTClient
12+
from aishell.utils import AiShellConfigManager
13+
14+
from .cli_app import cli_app
15+
from .config_aishell import config_aishell
16+
17+
18+
@cli_app.command()
19+
def aishell_command(question: str, language_model: Optional[LanguageModel] = None):
20+
config_manager = _get_config_manager()
21+
config_manager.config_model.language_model = language_model or config_manager.config_model.language_model
22+
23+
query_client = _get_query_client(
24+
language_model=config_manager.config_model.language_model,
25+
config_model=config_manager.config_model,
26+
)
27+
28+
console = Console()
29+
30+
try:
31+
with console.status(f'''
32+
[green] AiShell is thinking of `{question}` ...[/green]
33+
34+
[dim]AiShell is not responsible for any damage caused by the command executed by the user.[/dim]'''[1:]):
35+
start_time = time.time()
36+
response = query_client.query(question)
37+
end_time = time.time()
38+
39+
execution_time = end_time - start_time
40+
console.print(f'AiShell: {response}\n\n[dim]Took {execution_time:.2f} seconds to think the command.[/dim]')
41+
42+
will_execute = typer.confirm('Execute this command?')
43+
if will_execute:
44+
os.system(response)
45+
except KeyError:
46+
rich.print('It looks like the `session_token` is expired. Please reconfigure AiShell.')
47+
typer.confirm('Reconfigure AiShell?', abort=True)
48+
config_aishell()
49+
aishell_command(question=question, language_model=language_model)
50+
typer.Exit()
51+
52+
53+
def _get_config_manager():
54+
is_config_file_available = AiShellConfigManager.is_config_file_available(AiShellConfigManager.DEFAULT_CONFIG_PATH)
55+
if is_config_file_available:
56+
return AiShellConfigManager(load_config=True)
57+
else:
58+
return config_aishell()
59+
60+
61+
def _get_query_client(language_model: LanguageModel, config_model: AiShellConfigModel):
62+
if language_model == LanguageModel.REVERSE_ENGINEERED_CHATGPT:
63+
return ReverseEngineeredChatGPTClient(config=config_model.chatgpt_config)
64+
65+
if not config_model.openai_api_key:
66+
raise RuntimeError('OpenAI API key is not provided. Please provide it in the config file.')
67+
68+
if language_model == LanguageModel.GPT3:
69+
return GPT3Client(openai_api_key=config_model.openai_api_key)
70+
if language_model == LanguageModel.OFFICIAL_CHATGPT:
71+
return OfficialChatGPTClient(openai_api_key=config_model.openai_api_key)
72+
raise NotImplementedError(f'Language model {language_model} is not implemented yet.')

aishell/cli/cli_app.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from typer import Typer
2+
3+
cli_app = Typer()

aishell/cli/config_aishell.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import sys
2+
3+
import rich
4+
import typer
5+
from yt_dlp.cookies import SUPPORTED_BROWSERS
6+
7+
from aishell.adapters.openai_cookie_adapter import OpenAICookieAdapter
8+
from aishell.models import RevChatGPTChatbotConfigModel
9+
from aishell.models.aishell_config_model import AiShellConfigModel
10+
from aishell.utils import AiShellConfigManager
11+
12+
13+
def config_aishell():
14+
rich.print('''
15+
Hi! 🙌 I am [bold blue]AiShell[/bold blue], [yellow]your powerful terminal assistant[/yellow] 🔥
16+
I am here to assist you with configuring AiShell. 💪
17+
18+
Please make sure that you have logged into chat.openai.com on your browser before we continue. 🗝️
19+
20+
'''[1:])
21+
typer.confirm('Are you ready to proceed? 🚀', abort=True)
22+
23+
rich.print(f'''
24+
Which browser did you use to log in to chat.openai.com?
25+
26+
We support the following browsers: [{SUPPORTED_BROWSERS}]'''[1:])
27+
browser_name = typer.prompt('Please enter your choice here: ')
28+
if browser_name not in SUPPORTED_BROWSERS:
29+
rich.print(f'Browser {browser_name} is not supported. Supported browsers are: {SUPPORTED_BROWSERS}')
30+
sys.exit(1)
31+
32+
adapter = OpenAICookieAdapter(browser_name)
33+
session_token = adapter.get_openai_session_token()
34+
if not session_token:
35+
rich.print('Failed to get session token. 😓 Can you check if you are logged in to https://chat.openai.com?')
36+
sys.exit(1)
37+
38+
config_manager = save_config(session_token)
39+
40+
rich.print(f'''
41+
[green bold]Excellent![/green bold] You are now ready to use [bold blue]AiShell[/bold blue] 🚀
42+
43+
Enjoy your AI powered terminal assistant! 🎉
44+
45+
[dim]To check your settings file, it's at: {config_manager.config_path}[/dim]
46+
47+
'''[1:])
48+
return config_manager
49+
50+
51+
def save_config(session_token: str):
52+
is_config_file_available = AiShellConfigManager.is_config_file_available(AiShellConfigManager.DEFAULT_CONFIG_PATH)
53+
if is_config_file_available:
54+
config_manager = AiShellConfigManager(load_config=True)
55+
is_chatgpt_config_available = config_manager.config_model.chatgpt_config is not None
56+
if is_chatgpt_config_available:
57+
assert config_manager.config_model.chatgpt_config # for type hinting
58+
config_manager.config_model.chatgpt_config.session_token = session_token
59+
else:
60+
config_manager.config_model.chatgpt_config = RevChatGPTChatbotConfigModel(session_token=session_token)
61+
else:
62+
chatgpt_config = RevChatGPTChatbotConfigModel(session_token=session_token)
63+
aishell_config = AiShellConfigModel(chatgpt_config=chatgpt_config)
64+
config_manager = AiShellConfigManager(config_model=aishell_config)
65+
66+
config_manager.save_config()
67+
return config_manager

aishell/cli/test/__init__.py

Whitespace-only changes.
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
from unittest.mock import MagicMock, patch
2+
3+
from aishell.cli.config_aishell import save_config
4+
from aishell.models import AiShellConfigModel, LanguageModel, RevChatGPTChatbotConfigModel
5+
from aishell.utils import AiShellConfigManager
6+
7+
8+
class TestSaveConfig:
9+
10+
@patch('aishell.cli.config_aishell.AiShellConfigManager', spec=AiShellConfigManager)
11+
def test_success_when_config_file_not_available(
12+
self,
13+
mocked_config_manager_class: MagicMock,
14+
):
15+
'''config file 이 없는 경우 테스트 성공해야 한다'''
16+
# given
17+
mocked_config_manager_class.is_config_file_available.return_value = False
18+
19+
# when
20+
save_config('valid_session_token')
21+
22+
# then
23+
mocked_config_manager_class.return_value.save_config.assert_called_once()
24+
25+
@patch('aishell.cli.config_aishell.AiShellConfigManager', spec=AiShellConfigManager)
26+
def test_success_when_config_file_available_with_chatgpt_config(
27+
self,
28+
mocked_config_manager_class: MagicMock,
29+
):
30+
'''config file 이 있고, chatgpt_config 가 있는 경우 테스트 성공해야 한다.'''
31+
# given
32+
mocked_config_manager_class.is_config_file_available.return_value = True
33+
mocked_config_manager_instance = mocked_config_manager_class.return_value
34+
mocked_config_manager_instance.config_model =\
35+
AiShellConfigModel(chatgpt_config=RevChatGPTChatbotConfigModel(session_token='invalid_session_token'))
36+
37+
# when
38+
save_config('valid_session_token')
39+
40+
# then
41+
mocked_config_manager_class.return_value.save_config.assert_called_once()
42+
43+
@patch('aishell.cli.config_aishell.AiShellConfigManager', spec=AiShellConfigManager)
44+
def test_success_when_config_file_available_without_chatgpt_config(
45+
self,
46+
mocked_config_manager_class: MagicMock,
47+
):
48+
'''config file 이 있고, chatgpt_config 가 없는 경우 테스트 성공해야 한다.'''
49+
# given
50+
mocked_config_manager_class.is_config_file_available.return_value = True
51+
mocked_config_manager_instance = mocked_config_manager_class.return_value
52+
mocked_config_manager_instance.config_model =\
53+
AiShellConfigModel(language_model=LanguageModel.GPT3, openai_api_key='valid_openai_api_key')
54+
55+
# when
56+
save_config('valid_session_token')
57+
58+
# then
59+
mocked_config_manager_class.return_value.save_config.assert_called_once()
60+
61+
# when config file available - with chatgpt_config
62+
# when config file available - without chatgpt_config
63+
# when config file not available

aishell/models/revchatgpt_chatbot_config_model.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,3 @@ def check_at_least_one_account_info(cls, values: dict[str, Optional[str]]):
1818
raise ValueError('No information for authentication provided.')
1919

2020
return values
21-
22-
class Config:
23-
frozen = True

aishell/query_clients/gpt3_client.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@
1111

1212
class GPT3Client(QueryClient):
1313

14+
def __init__(
15+
self,
16+
openai_api_key: str,
17+
):
18+
openai.api_key = openai_api_key
19+
1420
def query(self, prompt: str) -> str:
1521
prompt = self._construct_prompt(prompt)
1622
completion: Final[OpenAIResponseModel] = cast(
@@ -29,8 +35,9 @@ def query(self, prompt: str) -> str:
2935
return make_executable_command(response_text)
3036

3137
def _construct_prompt(self, text: str) -> str:
32-
return f'''User: You are now a translater from human language to {os.uname()[0]} shell command.
33-
No explanation required, respond with only the raw shell command.
34-
What should I type to shell for: {text}, in one line.
38+
return f'''
39+
User: You are now a translater from human language to {os.uname()[0]} shell command.
40+
No explanation required, respond with only the raw shell command.
41+
What should I type to shell for: {text}, in one line.
3542
36-
You: '''
43+
You: '''[1:]

aishell/query_clients/official_chatgpt_client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ class OfficialChatGPTClient(QueryClient):
1313

1414
def __init__(
1515
self,
16-
openai_api_key: Optional[str] = None,
16+
openai_api_key: str,
1717
):
18-
super().__init__()
1918
OPENAI_API_KEY: Optional[str] = os.environ.get('OPENAI_API_KEY', openai_api_key)
2019
if OPENAI_API_KEY is None:
2120
raise UnauthorizedAccessError('OPENAI_API_KEY should not be none')
@@ -31,6 +30,7 @@ def query(self, prompt: str) -> str:
3130
return executable_command
3231

3332
def _construct_prompt(self, text: str) -> str:
34-
return f'''You are now a translater from human language to {os.uname()[0]} shell command.
35-
No explanation required, respond with only the raw shell command.
36-
What should I type to shell for: {text}, in one line.'''
33+
return f'''
34+
You are now a translater from human language to {os.uname()[0]} shell command.
35+
No explanation required, respond with only the raw shell command.
36+
What should I type to shell for: {text}, in one line.'''[1:]

0 commit comments

Comments
 (0)