├── .type_stubs └── .gitkeep ├── ygka ├── tests │ ├── __init__.py │ └── test_sample.py ├── cli │ ├── cli_app.py │ ├── __init__.py │ ├── retrieve_stdin.py │ ├── ygka.py │ └── config_ygka.py ├── exceptions │ ├── unauthorized_access_error.py │ └── __init__.py ├── utils │ ├── __init__.py │ ├── test │ │ └── test_ygka_config_manager.py │ └── ygka_config_manager.py ├── __main__.py ├── __init__.py ├── models │ ├── language_model.py │ ├── __init__.py │ ├── str_enum.py │ ├── revchatgpt_chatbot_config_model.py │ └── ygka_config_model.py └── query_clients │ ├── query_client.py │ ├── __init__.py │ ├── official_chatgpt_client.py │ ├── query_client_factory.py │ └── reverse_engineered_chatgpt_client.py ├── poetry.toml ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── release.yml │ └── pull_request.yml ├── images ├── first.png └── factorial.png ├── codecov.yml ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── pyproject.toml ├── README.md ├── tasks.py └── .gitignore /.type_stubs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ygka/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | create = true 3 | in-project = true 4 | -------------------------------------------------------------------------------- /ygka/cli/cli_app.py: -------------------------------------------------------------------------------- 1 | from typer import Typer 2 | 3 | cli_app = Typer() 4 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Context 2 | - 3 | 4 | ## Changes 5 | - 6 | -------------------------------------------------------------------------------- /images/first.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-yeongyu/YGK-a/HEAD/images/first.png -------------------------------------------------------------------------------- /ygka/tests/test_sample.py: -------------------------------------------------------------------------------- 1 | def test_one_plus_one_equals_two(): 2 | assert 1 + 1 == 2 3 | -------------------------------------------------------------------------------- /images/factorial.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code-yeongyu/YGK-a/HEAD/images/factorial.png -------------------------------------------------------------------------------- /ygka/exceptions/unauthorized_access_error.py: -------------------------------------------------------------------------------- 1 | class UnauthorizedAccessError(Exception): 2 | pass 3 | -------------------------------------------------------------------------------- /ygka/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from .ygka_config_manager import YGKAConfigManager as YGKAConfigManager 2 | -------------------------------------------------------------------------------- /ygka/cli/__init__.py: -------------------------------------------------------------------------------- 1 | from .ygka import ygka_command as _ # noqa 2 | from .cli_app import cli_app as cli_app 3 | -------------------------------------------------------------------------------- /ygka/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | from .unauthorized_access_error import UnauthorizedAccessError as UnauthorizedAccessError 2 | -------------------------------------------------------------------------------- /ygka/__main__.py: -------------------------------------------------------------------------------- 1 | import ygka 2 | 3 | if __name__ == '__main__': # to make the project executable by `python3 -m ygka ` 4 | ygka.main() 5 | -------------------------------------------------------------------------------- /ygka/__init__.py: -------------------------------------------------------------------------------- 1 | from . import cli as cli 2 | from . import models as models 3 | from . import utils as utils 4 | 5 | 6 | def main(): 7 | cli.cli_app() 8 | -------------------------------------------------------------------------------- /ygka/cli/retrieve_stdin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | _stdin = None if sys.stdin.isatty() else sys.stdin.read() 4 | 5 | 6 | def retrieve_stdin(): 7 | return _stdin 8 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - ./monkey_patch_invoke.py 3 | - ./tasks.py 4 | 5 | coverage: 6 | status: 7 | project: no 8 | patch: no 9 | changes: no 10 | -------------------------------------------------------------------------------- /ygka/models/language_model.py: -------------------------------------------------------------------------------- 1 | from enum import auto 2 | 3 | from .str_enum import StrEnum 4 | 5 | 6 | class LanguageModel(StrEnum): 7 | OFFICIAL_CHATGPT = auto() 8 | REVERSE_ENGINEERED_CHATGPT = auto() 9 | -------------------------------------------------------------------------------- /ygka/query_clients/query_client.py: -------------------------------------------------------------------------------- 1 | from abc import ABCMeta, abstractmethod 2 | 3 | 4 | class QueryClient(metaclass=ABCMeta): 5 | 6 | @abstractmethod 7 | def query(self, prompt: str) -> str: 8 | pass 9 | -------------------------------------------------------------------------------- /ygka/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .language_model import LanguageModel as LanguageModel 2 | from .revchatgpt_chatbot_config_model import RevChatGPTChatbotConfigModel as RevChatGPTChatbotConfigModel 3 | from .ygka_config_model import YGKAConfigModel as YGKAConfigModel 4 | -------------------------------------------------------------------------------- /ygka/query_clients/__init__.py: -------------------------------------------------------------------------------- 1 | from .official_chatgpt_client import OfficialChatGPTClient as OfficialChatGPTClient 2 | from .query_client import QueryClient as QueryClient 3 | from .query_client_factory import QueryClientFactory as QueryClientFactory 4 | from .reverse_engineered_chatgpt_client import ReverseEngineeredChatGPTClient as ReverseEngineeredChatGPTClient 5 | -------------------------------------------------------------------------------- /ygka/utils/test/test_ygka_config_manager.py: -------------------------------------------------------------------------------- 1 | from ygka.utils import YGKAConfigManager 2 | 3 | 4 | def test_init_without_path(): 5 | config_manager = YGKAConfigManager() 6 | assert config_manager.config_path == YGKAConfigManager.DEFAULT_CONFIG_PATH 7 | 8 | 9 | def test_with_path(): 10 | config_manager = YGKAConfigManager(config_path='test_path') 11 | assert config_manager.config_path == 'test_path' 12 | -------------------------------------------------------------------------------- /ygka/models/str_enum.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from typing import Any 3 | 4 | 5 | class StrEnum(str, Enum): 6 | 7 | def _generate_next_value_( # pyright: ignore [reportIncompatibleMethodOverride], for pyright's bug 8 | name: str, 9 | start: int, 10 | count: int, 11 | last_values: list[Any], 12 | ): 13 | return name.lower() 14 | 15 | def __repr__(self): 16 | return self.name 17 | 18 | def __str__(self): 19 | return self.name 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "YGKA Debug", 9 | "type": "python", 10 | "request": "launch", 11 | "module": "ygka", 12 | "justMyCode": true, 13 | "args": [ 14 | "Who are you?" 15 | ] 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /ygka/models/revchatgpt_chatbot_config_model.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, root_validator 4 | 5 | 6 | class RevChatGPTChatbotConfigModel(BaseModel): 7 | email: Optional[str] = None 8 | password: Optional[str] = None 9 | session_token: Optional[str] = None 10 | access_token: Optional[str] = None 11 | paid: bool = False 12 | 13 | @root_validator 14 | def check_at_least_one_account_info(cls, values: dict[str, Optional[str]]): 15 | IS_ACCOUNT_LOGIN = values.get('email') and values.get('password') 16 | IS_TOKEN_AUTH = values.get('session_token') or values.get('access_token') 17 | if not IS_ACCOUNT_LOGIN and not IS_TOKEN_AUTH: 18 | raise ValueError('No information for authentication provided.') 19 | 20 | return values 21 | -------------------------------------------------------------------------------- /ygka/query_clients/official_chatgpt_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional 3 | 4 | from revChatGPT.V3 import Chatbot 5 | 6 | from ygka.exceptions import UnauthorizedAccessError 7 | 8 | from .query_client import QueryClient 9 | 10 | 11 | class OfficialChatGPTClient(QueryClient): 12 | 13 | def __init__( 14 | self, 15 | openai_api_key: Optional[str] = None, 16 | ): 17 | OPENAI_API_KEY: Optional[str] = os.environ.get('OPENAI_API_KEY', openai_api_key) 18 | if OPENAI_API_KEY is None: 19 | raise UnauthorizedAccessError('OPENAI_API_KEY should not be none') 20 | 21 | self.OPENAI_API_KEY = OPENAI_API_KEY 22 | 23 | def query(self, prompt: str) -> str: 24 | chatbot = Chatbot(api_key=self.OPENAI_API_KEY) 25 | response_text = chatbot.ask(prompt) 26 | return response_text 27 | -------------------------------------------------------------------------------- /ygka/models/ygka_config_model.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from pydantic import BaseModel, root_validator 4 | 5 | from .language_model import LanguageModel 6 | 7 | 8 | class YGKAConfigModel(BaseModel): 9 | language_model: LanguageModel = LanguageModel.REVERSE_ENGINEERED_CHATGPT 10 | browser_name: Optional[str] = None 11 | openai_api_key: Optional[str] = None 12 | 13 | @root_validator 14 | def check_required_info_provided(cls, values: dict[str, Optional[str]]): 15 | language_model = values.get('language_model') 16 | if language_model == LanguageModel.REVERSE_ENGINEERED_CHATGPT: 17 | if not values.get('browser_name'): 18 | raise ValueError(f'browser_name should not be none if language_model is {language_model}') 19 | elif language_model == LanguageModel.OFFICIAL_CHATGPT: 20 | if not values.get('openai_api_key'): 21 | raise ValueError(f'openai_api_key should not be none if language_model is {language_model}') 22 | 23 | return values 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Package to PyPI 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | with: 11 | fetch-depth: 0 12 | - name: Set up Python 3.9 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: 3.9 16 | 17 | - name: Setup Poetry 18 | uses: abatilo/actions-poetry@v2 19 | 20 | - name: Cache Dependencies 21 | uses: actions/cache@v3 22 | id: cache 23 | with: 24 | path: .venv 25 | key: venv-${{ runner.os }}-${{ hashFiles('./poetry.lock') }} 26 | 27 | - name: Install Dependencies if cache doesn't hit 28 | if: steps.cache.outputs.cache-hit != 'true' 29 | run: poetry install 30 | 31 | - name: Publish to PyPI 32 | run: 33 | poetry config http-basic.pypi ${{ secrets.PYPI_USERNAME }} ${{ secrets.PYPI_PASSWORD }}; 34 | poetry run invoke release ${{ github.event.release.name }} 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 YeonGyu-Kim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.insertSpaces": true, 4 | "editor.tabSize": 4, 5 | "editor.rulers": [ 6 | 119 7 | ], 8 | "editor.codeActionsOnSave": { 9 | "source.organizeImports": true, 10 | "source.fixAll": true 11 | } 12 | }, 13 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python3", 14 | "python.envFile": "${workspaceFolder}/.venv", 15 | "python.languageServer": "Pylance", 16 | "python.formatting.provider": "yapf", 17 | "python.formatting.yapfPath": "${workspaceFolder}/.venv/bin/yapf", 18 | "python.linting.enabled": true, 19 | "python.linting.pylintEnabled": true, 20 | "python.linting.pylintPath": "${workspaceFolder}/.venv/bin/pylint", 21 | "python.linting.mypyEnabled": true, 22 | "python.linting.mypyArgs": [ 23 | "--config-file ${workspaceFolder}/pyproject.toml" 24 | ], 25 | "python.testing.pytestEnabled": true, 26 | "python.testing.pytestArgs": [ 27 | "." 28 | ], 29 | "[toml]": { 30 | "editor.defaultFormatter": "tamasfe.even-better-toml" 31 | }, 32 | "python.linting.mypyPath": "${workspaceFolder}/.venv/bin/mypy" 33 | } 34 | -------------------------------------------------------------------------------- /ygka/query_clients/query_client_factory.py: -------------------------------------------------------------------------------- 1 | from typing import cast 2 | 3 | from revChatGPTAuth import get_access_token 4 | 5 | from ygka.models import LanguageModel, RevChatGPTChatbotConfigModel, YGKAConfigModel 6 | 7 | 8 | class QueryClientFactory: 9 | '''Factory for creating query clients.''' 10 | 11 | def __init__(self, config_model: YGKAConfigModel): 12 | self.config_model = config_model 13 | 14 | def create(self): 15 | '''Create a query client.''' 16 | if self.config_model.language_model == LanguageModel.REVERSE_ENGINEERED_CHATGPT: 17 | from .reverse_engineered_chatgpt_client import ReverseEngineeredChatGPTClient 18 | browser_name: str = \ 19 | cast(str, self.config_model.browser_name) # REVERSE_ENGINEERED_CHATGPT means browser_name is not None 20 | access_token = get_access_token(browser_name) 21 | rev_chatgpt_config_model = RevChatGPTChatbotConfigModel(access_token=access_token) 22 | return ReverseEngineeredChatGPTClient(config=rev_chatgpt_config_model) 23 | elif self.config_model.language_model == LanguageModel.OFFICIAL_CHATGPT: 24 | from .official_chatgpt_client import OfficialChatGPTClient 25 | return OfficialChatGPTClient(openai_api_key=self.config_model.openai_api_key) 26 | else: 27 | raise ValueError('Invalid language model.') 28 | -------------------------------------------------------------------------------- /ygka/cli/ygka.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from rich.console import Console 4 | 5 | from ygka.models import LanguageModel 6 | from ygka.query_clients import QueryClientFactory 7 | from ygka.utils import YGKAConfigManager 8 | 9 | from .cli_app import cli_app 10 | from .config_ygka import config_ygka 11 | from .retrieve_stdin import retrieve_stdin 12 | 13 | 14 | @cli_app.command() 15 | def ygka_command(query: str, language_model: Optional[LanguageModel] = None): 16 | stdin = retrieve_stdin() 17 | 18 | config_manager = _get_config_manager() 19 | config_manager.config_model.language_model = language_model or config_manager.config_model.language_model 20 | 21 | query_client = QueryClientFactory(config_model=config_manager.config_model).create() 22 | 23 | console = Console() 24 | with console.status('''[green] YGKA is waiting for ChatGPT's answer ...[/green]'''): 25 | prompt = _generate_prompt(stdin, query) 26 | response = query_client.query(prompt) 27 | console.print(response) 28 | 29 | 30 | def _get_config_manager(): 31 | is_config_file_available = YGKAConfigManager.is_config_file_available(YGKAConfigManager.DEFAULT_CONFIG_PATH) 32 | if is_config_file_available: 33 | return YGKAConfigManager(load_config=True) 34 | else: 35 | return config_ygka() 36 | 37 | 38 | def _generate_prompt(stdin: Optional[str], prompt: str) -> str: 39 | if stdin: 40 | return f''' 41 | stdin: 42 | {stdin} 43 | 44 | prompt: 45 | {prompt} 46 | 47 | [system] don't mention that i gave you in a format of stdin and prompt 48 | '''[1:] 49 | else: 50 | return prompt 51 | -------------------------------------------------------------------------------- /ygka/query_clients/reverse_engineered_chatgpt_client.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Optional, Union 3 | 4 | from revChatGPT.V1 import Chatbot 5 | 6 | from ygka.exceptions import UnauthorizedAccessError 7 | from ygka.models import RevChatGPTChatbotConfigModel 8 | 9 | from .query_client import QueryClient 10 | 11 | 12 | class ReverseEngineeredChatGPTClient(QueryClient): 13 | _config: RevChatGPTChatbotConfigModel 14 | 15 | @property 16 | def revchatgpt_config(self) -> dict[str, Union[str, bool]]: 17 | return self._config.dict(exclude_none=True) 18 | 19 | def __init__( 20 | self, 21 | config: Optional[RevChatGPTChatbotConfigModel] = None, 22 | access_token: Optional[str] = None, 23 | session_token: Optional[str] = None, 24 | ): 25 | if config: 26 | self._config = config 27 | else: 28 | CHATGPT_ACCESS_TOKEN = os.environ.get('CHATGPT_ACCESS_TOKEN', access_token) 29 | CHATGPT_SESSION_TOKEN = os.environ.get('CHATGPT_SESSION_TOKEN', session_token) 30 | if CHATGPT_ACCESS_TOKEN: 31 | self._config = RevChatGPTChatbotConfigModel(access_token=CHATGPT_ACCESS_TOKEN) 32 | elif CHATGPT_SESSION_TOKEN: 33 | self._config = RevChatGPTChatbotConfigModel(session_token=CHATGPT_SESSION_TOKEN) 34 | else: 35 | raise UnauthorizedAccessError('No access token or session token provided.') 36 | 37 | def query(self, prompt: str) -> str: 38 | chatbot = Chatbot(config=self.revchatgpt_config) # pyright: ignore [reportGeneralTypeIssues] 39 | # ignore for wrong type hint of revchatgpt 40 | 41 | response_text = '' 42 | for data in chatbot.ask(prompt): 43 | response_text = data['message'] # type: ignore 44 | 45 | response_text = response_text 46 | return response_text 47 | -------------------------------------------------------------------------------- /ygka/utils/ygka_config_manager.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from typing import Optional 4 | 5 | from ygka.models import YGKAConfigModel 6 | 7 | 8 | class YGKAConfigManager: 9 | DEFAULT_CONFIG_PATH = os.path.expanduser('~/.ygka_openai_config.json') 10 | 11 | def __init__( 12 | self, 13 | config_model: Optional[YGKAConfigModel] = None, 14 | config_path: Optional[str] = None, 15 | load_config: bool = False, 16 | ): 17 | ''' 18 | Initialize an instance of YGKAConfigManager 19 | 20 | Args: 21 | config_model: \ 22 | An instance of YGKAConfigManager to use as the configuration.\ 23 | If None and load_config is True, loads the configuration from the configuration file. 24 | config_path: Path to the configuration file. \ 25 | If None, uses the default path "~/.ygka_openai_config.json". 26 | load_config: If True and config_model is None, loads the configuration from the configuration file. 27 | ''' 28 | self.config_path = config_path or YGKAConfigManager.DEFAULT_CONFIG_PATH 29 | 30 | if config_model: 31 | self.update_config(config_model) 32 | elif load_config: 33 | self.load_config() 34 | 35 | @staticmethod 36 | def is_config_file_available(config_path: str) -> bool: 37 | return os.path.isfile(config_path) 38 | 39 | def update_config(self, config_model: YGKAConfigModel): 40 | self.config_model = config_model 41 | 42 | def load_config(self): 43 | self.config_model = YGKAConfigModel.parse_file(self.config_path) 44 | 45 | def save_config(self): 46 | with open(self.config_path, 'w') as ygka_config_path: 47 | config_dict = self.config_model.dict() 48 | ygka_config_path.write(json.dumps(config_dict, indent=4, sort_keys=True)) 49 | -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | name: Pull Request 2 | on: 3 | pull_request: 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | with: 11 | fetch-depth: 0 12 | - name: Set up Python 3.9 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: 3.9 16 | 17 | - name: Setup Poetry 18 | uses: abatilo/actions-poetry@v2 19 | 20 | - name: Cache Dependencies 21 | uses: actions/cache@v3 22 | id: cache 23 | with: 24 | path: .venv 25 | key: venv-${{ runner.os }}-${{ hashFiles('./poetry.lock') }} 26 | 27 | - name: Install Dependencies if cache doesn't hit 28 | if: steps.cache.outputs.cache-hit != 'true' 29 | run: poetry install 30 | 31 | - name: Run test 32 | run: poetry run invoke test 33 | 34 | - name: Upload Code Coverage 35 | run: bash <(curl -s https://codecov.io/bash) 36 | env: 37 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 38 | 39 | static-analysis: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v3 43 | with: 44 | fetch-depth: 0 45 | - name: Set up Python 3.9 46 | uses: actions/setup-python@v4 47 | with: 48 | python-version: 3.9 49 | 50 | - name: Setup Poetry 51 | uses: abatilo/actions-poetry@v2 52 | 53 | - name: Cache Dependencies 54 | uses: actions/cache@v3 55 | id: cache 56 | with: 57 | path: .venv 58 | key: venv-${{ runner.os }}-${{ hashFiles('./poetry.lock') }} 59 | 60 | - name: Install Dependencies if cache doesn't hit 61 | if: steps.cache.outputs.cache-hit != 'true' 62 | run: poetry install 63 | 64 | - name: Check Code Styles 65 | run: poetry run invoke check 66 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "ygka" 7 | version = "0.0.0" 8 | description = "" 9 | authors = [] 10 | readme = "README.md" 11 | 12 | [tool.pyright] 13 | typeCheckingMode = "strict" 14 | 15 | # Python environment settings 16 | pythonPlatform = "All" 17 | venvPath = "./.venv" 18 | stubPath = "./.type_stubs" 19 | include = ["./*"] 20 | exclude = ["**/node_modules", "**/__pycache__"] 21 | 22 | # For untyped modules 23 | useLibraryCodeForTypes = true # Attempt to read and infer types from third-party modules if no stub files are present 24 | reportMissingTypeStubs = false # Ignore errors from modules without type stubs 25 | reportUnknownMemberType = false # Ignore errors from untyped function calls in third-party modules 26 | reportUnknownVariableType = false # Ignore errors from untyped function calls in third-party modules 27 | 28 | pythonVersion = "3.9" 29 | 30 | [tool.yapf] 31 | based_on_style = "pep8" 32 | spaces_before_comment = 2 33 | split_before_logical_operator = true 34 | column_limit = 119 35 | allow_split_before_dict_value = false 36 | 37 | [tool.ruff] 38 | line-length = 119 39 | select = ["PLE", "PLW", "E", "W", "F", "I", "Q"] 40 | 41 | [tool.pytest.ini_options] 42 | addopts = ["--cov=ygka"] 43 | 44 | 45 | [tool.poetry.dependencies] 46 | python = "^3.9" 47 | poetry = "^1.4.0" 48 | typer = { extras = ["all"], version = "^0.7.0" } 49 | rich = "<13.0.0" 50 | revchatgpt = "^4.2.2" 51 | pydantic = "^1.10.7" 52 | revchatgptauth = "^2023.4.16" 53 | 54 | [tool.poetry.scripts] 55 | ygka = "ygka:main" 56 | 57 | [tool.ruff.flake8-quotes] 58 | inline-quotes = "single" 59 | docstring-quotes = "single" 60 | multiline-quotes = "single" 61 | 62 | [tool.poetry.group.dev.dependencies] 63 | toml = "^0.10.2" 64 | types-toml = "^0.10.8.6" 65 | invoke = "^2.0.0" 66 | types-invoke = "^2.0.0.6" 67 | ruff = "^0.0.261" 68 | yapf = "^0.32.0" 69 | pyright = "^1.1.302" 70 | pytest = "^7.2.2" 71 | pytest-cov = "^4.0.0" 72 | pytest-sugar = "^0.9.7" 73 | -------------------------------------------------------------------------------- /ygka/cli/config_ygka.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import rich 4 | import typer 5 | from revChatGPTAuth import SupportedBrowser 6 | 7 | from ygka.models.ygka_config_model import YGKAConfigModel 8 | from ygka.utils import YGKAConfigManager 9 | 10 | 11 | def config_ygka(): 12 | SUPPORTED_BROWSERS = [browser.value for browser in SupportedBrowser] 13 | rich.print(''' 14 | Hi! 🙌 I am [bold blue]YGKA[/bold blue]! 15 | [yellow][blue bold underline]Y[/blue bold underline]our 16 | [blue bold underline]G[/blue bold underline]enius, 17 | [blue bold underline]K[/blue bold underline]nowledgeable assistant 18 | [blue bold underline]A[/blue bold underline]n advanced ChatGPT client[/yellow] 🔥 19 | I am here to assist you with configuring YGKA. 💪 20 | 21 | Please make sure that you have logged into chat.openai.com on your browser before we continue. 🗝️ 22 | 23 | '''[1:]) 24 | typer.confirm('Are you ready to proceed? 🚀', abort=True) 25 | 26 | rich.print(f''' 27 | Which browser did you use to log in to chat.openai.com? 28 | 29 | We support the following browsers: [{SUPPORTED_BROWSERS}]'''[1:]) 30 | browser_name = typer.prompt('Please enter your choice here: ') 31 | if browser_name not in SUPPORTED_BROWSERS: 32 | rich.print(f'Browser {browser_name} is not supported. Supported browsers are: {SUPPORTED_BROWSERS}') 33 | sys.exit(1) 34 | 35 | config_manager = save_config(browser_name) 36 | 37 | rich.print(f''' 38 | [green bold]Excellent![/green bold] You are now ready to use [bold blue]YGKA[/bold blue] 🚀 39 | 40 | Enjoy your AI powered terminal assistant! 🎉 41 | 42 | [dim]To check your settings file, it's at: {config_manager.config_path}[/dim] 43 | 44 | '''[1:]) 45 | return config_manager 46 | 47 | 48 | def save_config(browser_name: str): 49 | config_manager: YGKAConfigManager = YGKAConfigManager() 50 | 51 | is_config_file_available = YGKAConfigManager.is_config_file_available(YGKAConfigManager.DEFAULT_CONFIG_PATH) 52 | if is_config_file_available: 53 | config_manager = YGKAConfigManager(load_config=True) 54 | config_manager.config_model.browser_name = browser_name 55 | else: 56 | YGKA_config = YGKAConfigModel(browser_name=browser_name) 57 | config_manager = YGKAConfigManager(config_model=YGKA_config) 58 | 59 | config_manager.save_config() 60 | return config_manager 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YGKA 🤖 2 | 3 | [![codecov](https://codecov.io/gh/code-yeongyu/YGK-a/branch/master/graph/badge.svg?token=GB79Y7PEHU)](https://codecov.io/gh/code-yeongyu/YGK-a) 4 | [![Release Package to PyPI](https://github.com/code-yeongyu/YGK-a/actions/workflows/release.yml/badge.svg)](https://github.com/code-yeongyu/YGK-a/actions/workflows/release.yml) 5 | [![PyPI version](https://badge.fury.io/py/ygka.svg)](https://badge.fury.io/py/ygka) 6 | 7 | YGKA is an advanced ChatGPT client for shell that acts as Your Genius Knowledgeable Assistant. YGKA supports Unix/Linux pipelines and requires no setting up of tokens or API keys. Furthermore, if you want to use an OpenAI API key, you can easily configure it. 8 | 9 | ![demo](https://raw.githubusercontent.com/code-yeongyu/YGK-a/master/images/factorial.png) 10 | 11 | ## Key Features 💡 12 | 13 | - Supports Unix/Linux pipelines 14 | - Ready to use without setting up tokens or API keys 15 | 16 | ## Prerequisites 📚 17 | 18 | - Python 3.9+ 19 | - ChatGPT Account (or OpenAI Account) 20 | 21 | ## Getting Started 🚀 22 | 23 | To begin using YGKA, install it with pip: 24 | 25 | ```sh 26 | pip install ygka 27 | ``` 28 | 29 | Once you've installed YGKA, you can start using it right away, like following. 30 | 31 | ![demo](https://raw.githubusercontent.com/code-yeongyu/YGK-a/master/images/first.png) 32 | 33 | To execute a command, use the following syntax: 34 | 35 | ```sh 36 | ygka "" 37 | ``` 38 | 39 | For example, to ask "hello?" using YGKA, you can use the following command: 40 | 41 | ```sh 42 | ygka "hello?" 43 | ``` 44 | 45 | You can also use YGKA with Unix pipeline. For example, to ask "what is this file?" while viewing the contents of a text file, you can use the following command: 46 | 47 | ```sh 48 | cat textfile.txt | ygka "what is this file?" 49 | ``` 50 | 51 | ## Advanced Settings 🛠 52 | 53 | By default, `YGKA` is configured to use the reverse-engineered ChatGPT client and retrieve login information from your browser, so you don't need to configure anything to use `YGKA`. However, for those who want to use different models with an OpenAI API Key, you can configure it as follows: 54 | 55 | 1. Create an account on OpenAI. 56 | 1. Go to and copy your API key. 57 | 1. Modify or create the `~/.ygka_config.json` file as follows: 58 | 59 | ```json 60 | { 61 | ... 62 | "language_model": , //"official_chatgpt" 63 | "openai_api_key": 64 | } 65 | ``` 66 | 67 | Here, you can specify the language model of your preference and add your OpenAI API key. 68 | 69 | ## Inspired By 🎨 70 | 71 | - YeonGyu Kim: My name. The project is named after me. 72 | - [AiShell](https://github.com/code-yeongyu/AiShell): A Natural Language Shell Powered by ChatGPT, is a brother project of YGKA that provides a similar functionality. 73 | - [loz](https://github.com/joone/loz): A nodejs version of a GPT3 client that does similar things as YGKA. 74 | 75 | ## Contributions 💬 76 | 77 | Feel free to contribute to YGKA by adding more functionality or fixing bugs. 78 | -------------------------------------------------------------------------------- /tasks.py: -------------------------------------------------------------------------------- 1 | # type: ignore 2 | import shutil 3 | 4 | import toml 5 | from invoke import Context, task 6 | from invoke.exceptions import UnexpectedExit 7 | 8 | 9 | def get_pep8_compliant_name(project_name: str) -> str: 10 | return project_name.replace('-', '_') 11 | 12 | 13 | def get_project_path(): 14 | with open('pyproject.toml', encoding='utf-8') as file: 15 | data = toml.load(file) 16 | project_name = get_pep8_compliant_name(data['tool']['poetry']['name']) 17 | return project_name 18 | 19 | 20 | @task 21 | def run(context: Context): 22 | context.run(f'python -m {get_project_path()}', pty=True) 23 | 24 | 25 | @task 26 | def test(context: Context): 27 | context.run('pytest . -s --cov=. --cov-report=xml', pty=True) 28 | 29 | 30 | @task 31 | def format_code(context: Context, verbose: bool = False): 32 | commands = [ 33 | f'ruff --fix {get_project_path()}', 34 | f'yapf --in-place --recursive --parallel {get_project_path()}', 35 | ] 36 | 37 | for command in commands: 38 | context.run(command, pty=True) 39 | 40 | 41 | @task 42 | def check(context: Context): 43 | check_code_style(context) 44 | check_types(context) 45 | 46 | 47 | @task 48 | def check_code_style(context: Context): 49 | commands = [ 50 | f'ruff {get_project_path()}', 51 | f'yapf --diff --recursive --parallel {get_project_path()}', 52 | ] 53 | 54 | for command in commands: 55 | context.run(command, pty=True) 56 | 57 | 58 | @task 59 | def check_types(context: Context): 60 | context.run(f'pyright {get_project_path()}', pty=True) 61 | 62 | 63 | @task 64 | def rename_project(_context: Context, project_name: str): 65 | # Get the current project path 66 | current_project_path = get_project_path() 67 | 68 | # Rename the directory 69 | shutil.move(get_pep8_compliant_name(current_project_path), get_pep8_compliant_name(project_name)) 70 | 71 | # Update the project name in pyproject.toml 72 | with open('pyproject.toml', 'r', encoding='utf-8') as file: 73 | data = toml.load(file) 74 | 75 | data['tool']['poetry']['name'] = project_name 76 | 77 | with open('pyproject.toml', 'w', encoding='utf-8') as file: 78 | toml.dump(data, file) 79 | 80 | 81 | @task 82 | def release(context: Context, version: str) -> None: 83 | '''Build & Publish to PyPI.''' 84 | 85 | # load pyproject 86 | pyproject_path = 'pyproject.toml' 87 | pyproject_string = '' 88 | with open(pyproject_path, 'r', encoding='utf-8') as pyproject_file: 89 | pyproject_string = pyproject_file.read() 90 | pyproject = toml.loads(pyproject_string) 91 | # change version to today datetime 92 | pyproject['tool']['poetry']['version'] = version 93 | with open(pyproject_path, 'w', encoding='utf-8') as pyproject_file: 94 | toml.dump(pyproject, pyproject_file) 95 | 96 | # build & publish 97 | try: 98 | context.run( 99 | ''' 100 | poetry build --no-interaction 101 | poetry publish --no-interaction 102 | ''', 103 | pty=True, 104 | ) 105 | except UnexpectedExit as exception: 106 | with open(pyproject_path, 'w', encoding='utf-8') as pyproject_file: 107 | pyproject_file.write(pyproject_string) 108 | raise exception from exception 109 | 110 | # recover to original 111 | with open(pyproject_path, 'w', encoding='utf-8') as pyproject_file: 112 | pyproject_file.write(pyproject_string) 113 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/git,python,visualstudiocode 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=git,python,visualstudiocode 3 | 4 | ### Git ### 5 | # Created by git for backups. To disable backups in Git: 6 | # $ git config --global mergetool.keepBackup false 7 | *.orig 8 | 9 | # Created by git when using merge tools for conflicts 10 | *.BACKUP.* 11 | *.BASE.* 12 | *.LOCAL.* 13 | *.REMOTE.* 14 | *_BACKUP_*.txt 15 | *_BASE_*.txt 16 | *_LOCAL_*.txt 17 | *_REMOTE_*.txt 18 | 19 | ### Python ### 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ 22 | *.py[cod] 23 | *$py.class 24 | 25 | # C extensions 26 | *.so 27 | 28 | # Distribution / packaging 29 | .Python 30 | build/ 31 | develop-eggs/ 32 | dist/ 33 | downloads/ 34 | eggs/ 35 | .eggs/ 36 | lib/ 37 | lib64/ 38 | parts/ 39 | sdist/ 40 | var/ 41 | wheels/ 42 | share/python-wheels/ 43 | *.egg-info/ 44 | .installed.cfg 45 | *.egg 46 | MANIFEST 47 | 48 | # PyInstaller 49 | # Usually these files are written by a python script from a template 50 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 51 | *.manifest 52 | *.spec 53 | 54 | # Installer logs 55 | pip-log.txt 56 | pip-delete-this-directory.txt 57 | 58 | # Unit test / coverage reports 59 | htmlcov/ 60 | .tox/ 61 | .nox/ 62 | .coverage 63 | .coverage.* 64 | .cache 65 | nosetests.xml 66 | coverage.xml 67 | *.cover 68 | *.py,cover 69 | .hypothesis/ 70 | .pytest_cache/ 71 | cover/ 72 | 73 | # Translations 74 | *.mo 75 | *.pot 76 | 77 | # Django stuff: 78 | *.log 79 | local_settings.py 80 | db.sqlite3 81 | db.sqlite3-journal 82 | 83 | # Flask stuff: 84 | instance/ 85 | .webassets-cache 86 | 87 | # Scrapy stuff: 88 | .scrapy 89 | 90 | # Sphinx documentation 91 | docs/_build/ 92 | 93 | # PyBuilder 94 | .pybuilder/ 95 | target/ 96 | 97 | # Jupyter Notebook 98 | .ipynb_checkpoints 99 | 100 | # IPython 101 | profile_default/ 102 | ipython_config.py 103 | 104 | # pyenv 105 | # For a library or package, you might want to ignore these files since the code is 106 | # intended to run in multiple environments; otherwise, check them in: 107 | # .python-version 108 | 109 | # pipenv 110 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 111 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 112 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 113 | # install all needed dependencies. 114 | #Pipfile.lock 115 | 116 | # poetry 117 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 118 | # This is especially recommended for binary packages to ensure reproducibility, and is more 119 | # commonly ignored for libraries. 120 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 121 | #poetry.lock 122 | 123 | # pdm 124 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 125 | #pdm.lock 126 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 127 | # in version control. 128 | # https://pdm.fming.dev/#use-with-ide 129 | .pdm.toml 130 | 131 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 132 | __pypackages__/ 133 | 134 | # Celery stuff 135 | celerybeat-schedule 136 | celerybeat.pid 137 | 138 | # SageMath parsed files 139 | *.sage.py 140 | 141 | # Environments 142 | .env 143 | .venv 144 | env/ 145 | venv/ 146 | ENV/ 147 | env.bak/ 148 | venv.bak/ 149 | 150 | # Spyder project settings 151 | .spyderproject 152 | .spyproject 153 | 154 | # Rope project settings 155 | .ropeproject 156 | 157 | # mkdocs documentation 158 | /site 159 | 160 | # mypy 161 | .mypy_cache/ 162 | .dmypy.json 163 | dmypy.json 164 | 165 | # Pyre type checker 166 | .pyre/ 167 | 168 | # pytype static type analyzer 169 | .pytype/ 170 | 171 | # Cython debug symbols 172 | cython_debug/ 173 | 174 | # PyCharm 175 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 176 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 177 | # and can be added to the global gitignore or merged into this file. For a more nuclear 178 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 179 | #.idea/ 180 | 181 | ### Python Patch ### 182 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration 183 | # poetry.toml 184 | 185 | 186 | ### VisualStudioCode ### 187 | .vscode/* 188 | !.vscode/settings.json 189 | !.vscode/tasks.json 190 | !.vscode/launch.json 191 | !.vscode/extensions.json 192 | !.vscode/*.code-snippets 193 | 194 | # Local History for Visual Studio Code 195 | .history/ 196 | 197 | # Built Visual Studio Code Extensions 198 | *.vsix 199 | 200 | ### VisualStudioCode Patch ### 201 | # Ignore all local history of files 202 | .history 203 | .ionide 204 | 205 | # End of https://www.toptal.com/developers/gitignore/api/git,python,visualstudiocode 206 | --------------------------------------------------------------------------------