├── src ├── __init__.py ├── ask.py ├── tts.py ├── chat.py ├── file_upload.py ├── vector_store.py └── oai.py ├── oai.py ├── requirements.txt ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.yml │ └── bug_report.yml ├── pull_request_template.md └── workflows │ └── ci.yml ├── .gitignore ├── SECURITY.md ├── README.md └── CONTRIBUTING.md /src/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /oai.py: -------------------------------------------------------------------------------- 1 | from src.oai import main 2 | 3 | if __name__ == "__main__": 4 | main() -------------------------------------------------------------------------------- /src/ask.py: -------------------------------------------------------------------------------- 1 | # 질문/응답 기능 2 | import os 3 | import sys 4 | from openai import OpenAI 5 | 6 | OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") 7 | 8 | # ask command has been removed. 9 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | annotated-types==0.7.0 2 | anyio==4.9.0 3 | certifi==2025.4.26 4 | colorama==0.4.6 5 | distro==1.9.0 6 | h11==0.16.0 7 | httpcore==1.0.9 8 | httpx==0.28.1 9 | idna==3.10 10 | jiter==0.10.0 11 | openai==1.86.0 12 | pydantic==2.11.5 13 | pydantic_core==2.33.2 14 | python-dotenv==1.1.0 15 | sniffio==1.3.1 16 | tqdm==4.67.1 17 | typing-inspection==0.4.1 18 | typing_extensions==4.14.0 19 | -------------------------------------------------------------------------------- /src/tts.py: -------------------------------------------------------------------------------- 1 | # TTS(Text-to-Speech) 기능 2 | import openai 3 | import sys 4 | import os 5 | 6 | OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") 7 | openai.api_key = OPENAI_API_KEY 8 | 9 | def tts(text=None, input_file=None, output="speech_output.mp3", model="tts-1-hd", voice="echo"): 10 | if input_file: 11 | with open(input_file, "r", encoding="utf-8") as f: 12 | text = f.read().strip() 13 | if not text: 14 | print("Text is required.") 15 | sys.exit(1) 16 | response = openai.audio.speech.create( 17 | model=model, 18 | voice=voice, 19 | input=text 20 | ) 21 | with open(output, "wb") as f: 22 | f.write(response.content) 23 | print(f"Audio file saved: {output}") 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Suggest an idea for this project 3 | title: "[Feature]: " 4 | labels: ["enhancement"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for suggesting a new feature! 10 | 11 | - type: textarea 12 | id: problem 13 | attributes: 14 | label: Is your feature request related to a problem? Please describe. 15 | description: A clear and concise description of what the problem is. 16 | placeholder: I'm always frustrated when [...] 17 | validations: 18 | required: true 19 | 20 | - type: textarea 21 | id: solution 22 | attributes: 23 | label: Describe the solution you'd like 24 | description: A clear and concise description of what you want to happen. 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: alternatives 30 | attributes: 31 | label: Describe alternatives you've considered 32 | description: A clear and concise description of any alternative solutions or features you've considered. 33 | validations: 34 | required: false 35 | 36 | - type: dropdown 37 | id: component 38 | attributes: 39 | label: Which component would this feature affect? 40 | options: 41 | - CLI interface 42 | - File upload 43 | - Text-to-speech 44 | - Chat functionality 45 | - Vector store management 46 | - Documentation 47 | - Other 48 | validations: 49 | required: true 50 | 51 | - type: textarea 52 | id: additional-context 53 | attributes: 54 | label: Additional context 55 | description: Add any other context or screenshots about the feature request here. 56 | validations: 57 | required: false 58 | -------------------------------------------------------------------------------- /src/chat.py: -------------------------------------------------------------------------------- 1 | # Chat 기능 2 | import openai 3 | import os 4 | 5 | OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") 6 | openai.api_key = OPENAI_API_KEY 7 | 8 | def chat(model="gpt-4o", system=None, user=None, stream=False, vector_store_id=None): 9 | import openai 10 | 11 | if not user: 12 | user = input("User: ") 13 | 14 | messages = [] 15 | if system: 16 | messages.append({"role": "system", "content": system}) 17 | messages.append({"role": "user", "content": user}) 18 | 19 | # vector_store_id가 있으면 file_search tool 사용 20 | if vector_store_id: 21 | client = openai.OpenAI() 22 | response = client.responses.create( 23 | model=model, 24 | input=user, 25 | tools=[{ 26 | "type": "file_search", 27 | "vector_store_ids": [vector_store_id] 28 | }], 29 | stream=stream 30 | ) 31 | if stream: 32 | for chunk in response: 33 | if hasattr(chunk, "delta") and chunk.delta: 34 | print(chunk.delta, end="", flush=True) 35 | print() 36 | else: 37 | # 비스트림 응답 38 | print(response) 39 | else: 40 | # 기존 방식 (tools 없이 일반 chat) 41 | completion = openai.chat.completions.create( 42 | model=model, 43 | messages=messages, 44 | stream=stream 45 | ) 46 | if stream: 47 | for chunk in completion: 48 | if hasattr(chunk, "choices") and chunk.choices: 49 | delta = chunk.choices[0].delta 50 | if hasattr(delta, "content") and delta.content: 51 | print(delta.content, end="", flush=True) 52 | print() 53 | else: 54 | print(completion.choices[0].message.content) 55 | -------------------------------------------------------------------------------- /src/file_upload.py: -------------------------------------------------------------------------------- 1 | # 파일 업로드 기능 (OpenAI file search tool 가이드에 맞춰 재구현) 2 | import glob 3 | import os 4 | import openai 5 | import sys 6 | 7 | OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") 8 | openai.api_key = OPENAI_API_KEY 9 | 10 | def upload_files(file, vector_store_id=None, purpose="assistants"): 11 | """ 12 | 파일 또는 와일드카드 패턴을 받아 OpenAI 파일 업로드 또는 벡터스토어 파일 업로드를 수행합니다. 13 | vector_store_id가 있으면 vector store에 업로드, 없으면 일반 파일 업로드. 14 | """ 15 | # 와일드카드 및 폴더 지원 16 | matched_files = glob.glob(file, recursive=True) 17 | files_to_upload = [] 18 | for path in matched_files: 19 | if os.path.isdir(path): 20 | for root, _, files in os.walk(path): 21 | for fname in files: 22 | files_to_upload.append(os.path.join(root, fname)) 23 | elif os.path.isfile(path): 24 | files_to_upload.append(path) 25 | if not files_to_upload: 26 | print(f"No files matched the pattern: {file}") 27 | sys.exit(1) 28 | 29 | for file_path in files_to_upload: 30 | if not os.path.isfile(file_path): 31 | print(f"Not a file: {file_path}") 32 | continue 33 | with open(file_path, "rb") as f: 34 | if vector_store_id: 35 | # 벡터스토어에 파일 업로드 (file_search 용) 36 | file_obj = openai.vector_stores.files.upload( 37 | vector_store_id=vector_store_id, 38 | file=f 39 | ) 40 | print(f"Uploaded to Vector Store: {file_obj.id} ({file_path})") 41 | else: 42 | # 일반 파일 업로드 (purpose: assistants 등) 43 | file_obj = openai.files.create( 44 | file=f, 45 | purpose=purpose 46 | ) 47 | print(f"File uploaded: {file_obj.id} ({file_path})") 48 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Pull Request 2 | 3 | ### Description 4 | Brief description of the changes made in this PR. 5 | 6 | ### Type of Change 7 | - [ ] Bug fix (non-breaking change which fixes an issue) 8 | - [ ] New feature (non-breaking change which adds functionality) 9 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 10 | - [ ] Documentation update 11 | - [ ] Code refactoring 12 | - [ ] Performance improvement 13 | - [ ] Other (please describe): 14 | 15 | ### Related Issues 16 | Fixes #(issue number) 17 | Closes #(issue number) 18 | Related to #(issue number) 19 | 20 | ### Changes Made 21 | - List the specific changes made 22 | - Be as detailed as necessary 23 | - Include any new dependencies 24 | 25 | ### Testing 26 | - [ ] I have performed a self-review of my own code 27 | - [ ] I have commented my code, particularly in hard-to-understand areas 28 | - [ ] I have made corresponding changes to the documentation 29 | - [ ] My changes generate no new warnings 30 | - [ ] I have added tests that prove my fix is effective or that my feature works 31 | - [ ] New and existing unit tests pass locally with my changes 32 | 33 | ### Screenshots (if applicable) 34 | Add screenshots to help explain your changes. 35 | 36 | ### Additional Notes 37 | Any additional information that reviewers should know about this PR. 38 | 39 | ### Checklist 40 | - [ ] My code follows the style guidelines of this project 41 | - [ ] I have performed a self-review of my own code 42 | - [ ] I have commented my code, particularly in hard-to-understand areas 43 | - [ ] I have made corresponding changes to the documentation 44 | - [ ] My changes generate no new warnings 45 | - [ ] I have added tests that prove my fix is effective or that my feature works 46 | - [ ] New and existing unit tests pass locally with my changes 47 | - [ ] Any dependent changes have been merged and published in downstream modules 48 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | pull_request: 7 | branches: [ main, master ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: [3.8, 3.9, '3.10', '3.11', '3.12'] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - name: Set up Python ${{ matrix.python-version }} 20 | uses: actions/setup-python@v4 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install -r requirements.txt 28 | pip install pytest pytest-cov flake8 29 | 30 | - name: Lint with flake8 31 | run: | 32 | # stop the build if there are Python syntax errors or undefined names 33 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 34 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 35 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 36 | 37 | - name: Test with pytest 38 | run: | 39 | pytest --cov=src --cov-report=xml 40 | env: 41 | OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} 42 | 43 | - name: Upload coverage reports to Codecov 44 | uses: codecov/codecov-action@v3 45 | with: 46 | file: ./coverage.xml 47 | fail_ci_if_error: false 48 | 49 | lint: 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | 54 | - name: Set up Python 55 | uses: actions/setup-python@v4 56 | with: 57 | python-version: '3.11' 58 | 59 | - name: Install dependencies 60 | run: | 61 | python -m pip install --upgrade pip 62 | pip install black isort flake8 63 | 64 | - name: Check code formatting with black 65 | run: black --check . 66 | 67 | - name: Check import sorting with isort 68 | run: isort --check-only . 69 | 70 | - name: Lint with flake8 71 | run: flake8 . 72 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug", "triage"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thanks for taking the time to fill out this bug report! 10 | 11 | - type: input 12 | id: contact 13 | attributes: 14 | label: Contact Details 15 | description: How can we get in touch with you if we need more info? 16 | placeholder: ex. email@example.com 17 | validations: 18 | required: false 19 | 20 | - type: textarea 21 | id: what-happened 22 | attributes: 23 | label: What happened? 24 | description: Also tell us, what did you expect to happen? 25 | placeholder: Tell us what you see! 26 | value: "A bug happened!" 27 | validations: 28 | required: true 29 | 30 | - type: dropdown 31 | id: version 32 | attributes: 33 | label: Version 34 | description: What version of our software are you running? 35 | options: 36 | - latest (default) 37 | - v1.0.0 38 | - other 39 | validations: 40 | required: true 41 | 42 | - type: dropdown 43 | id: command 44 | attributes: 45 | label: Command 46 | description: Which command were you using when the bug occurred? 47 | options: 48 | - file-upload 49 | - tts 50 | - chat 51 | - vector-store 52 | - vector-store-file 53 | - other 54 | validations: 55 | required: true 56 | 57 | - type: textarea 58 | id: environment 59 | attributes: 60 | label: Environment 61 | description: Please provide your environment details 62 | placeholder: | 63 | OS: Windows 11 64 | Python: 3.11.0 65 | OpenAI Package: 1.3.0 66 | validations: 67 | required: true 68 | 69 | - type: textarea 70 | id: logs 71 | attributes: 72 | label: Relevant log output 73 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 74 | render: shell 75 | 76 | - type: checkboxes 77 | id: terms 78 | attributes: 79 | label: Code of Conduct 80 | description: By submitting this issue, you agree to follow our [Code of Conduct](https://example.com) 81 | options: 82 | - label: I agree to follow this project's Code of Conduct 83 | required: true 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.pyc 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .nox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | *.py,cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | cover/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | .pybuilder/ 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | Pipfile.lock 89 | 90 | # poetry 91 | poetry.lock 92 | 93 | # pdm 94 | .pdm.toml 95 | 96 | # PEP 582 97 | __pypackages__/ 98 | 99 | # Celery stuff 100 | celerybeat-schedule 101 | celerybeat.pid 102 | 103 | # SageMath parsed files 104 | *.sage.py 105 | 106 | # Environments 107 | .env 108 | .venv/ 109 | venv/ 110 | env/ 111 | ENV/ 112 | env.bak/ 113 | venv.bak/ 114 | 115 | # Spyder project settings 116 | .spyderproject 117 | .spyproject 118 | 119 | # Rope project settings 120 | .ropeproject 121 | 122 | # mkdocs documentation 123 | /site 124 | 125 | # mypy 126 | .mypy_cache/ 127 | .dmypy.json 128 | dmypy.json 129 | 130 | # Pyre type checker 131 | .pyre/ 132 | 133 | # pytype static type analyzer 134 | .pytype/ 135 | 136 | # Cython debug symbols 137 | cython_debug/ 138 | 139 | # PyCharm 140 | .idea/ 141 | 142 | # VS Code 143 | .vscode/ 144 | 145 | # Output files 146 | *.mp3 147 | speech_output.mp3 148 | 149 | # API Keys 150 | .env.local 151 | .env.production 152 | .env.development 153 | 154 | # Temporary files 155 | *.tmp 156 | *.temp 157 | temp/ 158 | tmp/ 159 | 160 | # Log files 161 | logs/ 162 | 163 | # OS specific files 164 | .DS_Store 165 | Thumbs.db -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We release patches for security vulnerabilities. Which versions are eligible for receiving such patches depends on the CVSS v3.0 Rating: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.x.x | :white_check_mark: | 10 | | < 1.0 | :x: | 11 | 12 | ## Reporting a Vulnerability 13 | 14 | Please report (suspected) security vulnerabilities to **[security@yourproject.com]**. You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days. 15 | 16 | ### What to include in your report 17 | 18 | - A description of the vulnerability 19 | - Steps to reproduce the issue 20 | - Possible impact of the vulnerability 21 | - Any suggested mitigation or remediation steps 22 | 23 | ### What to expect 24 | 25 | - We will acknowledge receipt of your vulnerability report within 48 hours 26 | - We will provide an estimated timeline for addressing the vulnerability 27 | - We will notify you when the vulnerability is fixed 28 | - We may ask for additional information or guidance during the resolution process 29 | 30 | ### Responsible Disclosure 31 | 32 | We ask that you: 33 | 34 | - Give us reasonable time to address the issue before making any information public 35 | - Make a good faith effort to avoid privacy violations, destruction of data, and interruption or degradation of our service 36 | - Only interact with accounts you own or with explicit permission of the account holder 37 | 38 | ### Recognition 39 | 40 | We appreciate your help in keeping OpenAI CLI Tool secure. If you responsibly disclose a security vulnerability, we will: 41 | 42 | - Acknowledge your contribution in the project's changelog (unless you prefer to remain anonymous) 43 | - Work with you to understand and resolve the issue quickly 44 | 45 | ## Security Best Practices 46 | 47 | When using this tool: 48 | 49 | 1. **API Keys**: Never commit API keys to version control. Use environment variables. 50 | 2. **File Uploads**: Be cautious when uploading sensitive files to OpenAI services. 51 | 3. **Output**: Be aware that generated content may be logged by OpenAI for their service improvement. 52 | 4. **Network**: Use secure networks when transmitting data. 53 | 54 | ## Known Security Considerations 55 | 56 | - This tool transmits data to OpenAI's API endpoints 57 | - Uploaded files are processed by OpenAI's services 58 | - Chat conversations may be logged by OpenAI according to their data usage policy 59 | - API keys provide access to your OpenAI account and should be protected 60 | 61 | For more information about OpenAI's security practices, please refer to their [security documentation](https://platform.openai.com/docs/guides/safety-best-practices). 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAI CLI Tool 2 | 3 | A command-line interface for OpenAI API: upload files, manage vector stores, TTS, Q&A, and chat. 4 | 5 | ## Requirements 6 | 7 | - Python 3.8+ 8 | - `openai` Python package 9 | - Set the `OPENAI_API_KEY` environment variable 10 | 11 | ## Installation 12 | 13 | ```bash 14 | pip install -r requirements.txt 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```bash 20 | python src/oai.py [options] 21 | ``` 22 | 23 | ### Commands 24 | 25 | #### file-upload 26 | 27 | Upload files to OpenAI. Supports wildcards and vector store upload. 28 | 29 | **Examples:** 30 | ```bash 31 | python src/oai.py file-upload --file '*.md' 32 | python src/oai.py file-upload --file 'data.txt' --vector-store-id 33 | ``` 34 | 35 | **Options:** 36 | - `--file` (required): File name or wildcard pattern to upload. Example: `*.md`, `data.txt` 37 | - `--vector-store-id`: (Optional) Vector Store ID to upload files into. 38 | - `--purpose`: Purpose of the file. Default: `assistants`. 39 | 40 | --- 41 | 42 | #### tts 43 | 44 | Convert text or a file to speech (mp3). 45 | 46 | **Examples:** 47 | ```bash 48 | python src/oai.py tts --text 'Hello' 49 | python src/oai.py tts --input-file input.txt --output output.mp3 50 | ``` 51 | 52 | **Options:** 53 | - `--text`: Text to convert to speech. 54 | - `--input-file`: Path to a text file to convert to speech. 55 | - `--output`: Output mp3 file name. Default: `speech_output.mp3` 56 | - `--model`: TTS model name. Default: `tts-1-hd` 57 | - `--voice`: TTS voice name. Default: `echo` 58 | 59 | --- 60 | 61 | #### chat 62 | 63 | Chat with GPT models. 64 | 65 | **Examples:** 66 | ```bash 67 | python src/oai.py chat --user 'Tell me a joke.' 68 | python src/oai.py chat --system 'You are a helpful assistant.' --user 'Summarize this.' 69 | python src/oai.py chat --user 'Stream this answer.' --stream 70 | ``` 71 | 72 | **Options:** 73 | - `--model`: Model name. Default: `gpt-4o` 74 | - `--system`: System prompt (optional). 75 | - `--user`: User prompt (optional). 76 | - `--stream`: Use streaming output. 77 | - `--vector-store-id`: (Optional) Vector Store ID to use file_search tool in chat. 78 | 79 | --- 80 | 81 | #### vector-store 82 | 83 | Manage vector stores: create, list, get, delete. 84 | 85 | **Examples:** 86 | ```bash 87 | python src/oai.py vector-store create --name 'MyStore' 88 | python src/oai.py vector-store list 89 | python src/oai.py vector-store get --id 90 | python src/oai.py vector-store delete --id 91 | ``` 92 | 93 | **Subcommands:** 94 | - `create`: Create a new vector store (`--name`) 95 | - `list`: List all vector stores 96 | - `get`: Get details of a vector store (`--id`) 97 | - `delete`: Delete a vector store (`--id`) 98 | 99 | --- 100 | 101 | #### vector-store-file 102 | 103 | Manage files in a vector store: list, retrieve-file, retrieve-file-content, update-file-attribute, delete-file. 104 | 105 | **Examples:** 106 | ```bash 107 | python src/oai.py vector-store-file list --vector-store-id 108 | python src/oai.py vector-store-file retrieve-file --vector-store-id --file-id 109 | python src/oai.py vector-store-file retrieve-file-content --vector-store-id --file-id 110 | python src/oai.py vector-store-file update-file-attribute --vector-store-id --file-id --attribute metadata --value '{"key":"value"}' 111 | python src/oai.py vector-store-file delete-file --vector-store-id --file-id 112 | ``` 113 | 114 | **Subcommands:** 115 | - `list`: List files in a vector store (`--vector-store-id`, optional: `--filter`, `--order`, `--limit`) 116 | - `retrieve-file`: Retrieve a file object (`--vector-store-id`, `--file-id`) 117 | - `retrieve-file-content`: Retrieve file content (`--vector-store-id`, `--file-id`) 118 | - `update-file-attribute`: Update file attribute (`--vector-store-id`, `--file-id`, `--attribute`, `--value`) 119 | - `delete-file`: Delete a file (`--vector-store-id`, `--file-id`) 120 | 121 | **Options for `list`:** 122 | - `--filter`: Filter by file status (`in_progress`, `completed`, `failed`, `cancelled`) 123 | - `--order`: Sort order by created_at (`asc` or `desc`, default: `desc`) 124 | - `--limit`: Number of files per page (1-100, default: 100) 125 | 126 | --- 127 | 128 | ## Development 129 | 130 | Standard Python project layout. Main CLI entrypoint: `src/oai.py`. 131 | 132 | ## License 133 | 134 | MIT 135 | -------------------------------------------------------------------------------- /src/vector_store.py: -------------------------------------------------------------------------------- 1 | # Vector Store 관리 기능 2 | import openai 3 | import os 4 | import json 5 | 6 | OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") 7 | openai.api_key = OPENAI_API_KEY 8 | 9 | def vector_store_create(name, description=None): 10 | result = openai.vector_stores.create( 11 | name=name 12 | ) 13 | print(f"Created vector store: {result.id} (name: {result.name})") 14 | 15 | def vector_store_list(): 16 | result = openai.vector_stores.list() 17 | for vs in result.data: 18 | print(f"ID: {vs.id}, Name: {vs.name}, Created: {vs.created_at}") 19 | 20 | def vector_store_get(vector_store_id): 21 | try: 22 | result = openai.vector_stores.retrieve(vector_store_id) 23 | # result가 OpenAIObject일 경우 dict로 변환 24 | if hasattr(result, 'to_dict'): 25 | result = result.to_dict() 26 | print(json.dumps(result, indent=2, ensure_ascii=False)) 27 | except Exception as e: 28 | print(f"Error retrieving vector store: {e}") 29 | 30 | def vector_store_delete(vs_id): 31 | openai.vector_stores.delete(vs_id) 32 | print(f"Deleted vector store: {vs_id}") 33 | 34 | def vector_store_file_list(vector_store_id, filter=None, order="desc", limit=100): 35 | """ 36 | List all files in a vector store, handling full pagination using 'after' cursor. 37 | Optional arguments: 38 | - filter: Filter by file status (in_progress, completed, failed, cancelled) 39 | - order: asc or desc (default desc) 40 | - limit: number of files per page (default 100, max 100) 41 | """ 42 | try: 43 | after = None 44 | total = 0 45 | while True: 46 | params = { 47 | "vector_store_id": vector_store_id, 48 | "order": order, 49 | "limit": limit 50 | } 51 | if after: 52 | params["after"] = after 53 | if filter: 54 | params["filter"] = filter 55 | result = openai.vector_stores.files.list(**params) 56 | files = getattr(result, "data", result) 57 | if not files: 58 | if total == 0: 59 | print("No files found in the vector store.") 60 | break 61 | for file in files: 62 | file_dict = file.to_dict() if hasattr(file, "to_dict") else file 63 | print( 64 | f"ID: {file_dict.get('id')}, " 65 | f"Filename: {file_dict.get('filename')}, " 66 | f"Created: {file_dict.get('created_at')}, " 67 | f"Status: {file_dict.get('status')}" 68 | ) 69 | total += 1 70 | # Use the id of the last file as the 'after' cursor for the next page 71 | if hasattr(files, '__getitem__') and len(files) > 0: 72 | after = files[-1].id if hasattr(files[-1], 'id') else files[-1].get('id') 73 | else: 74 | after = None 75 | if not after or (hasattr(result, 'has_more') and not result.has_more): 76 | break 77 | except Exception as e: 78 | print(f"Error listing vector store files: {e}") 79 | 80 | def vector_store_file_retrieve(vector_store_id, file_id): 81 | """ 82 | Retrieve a file object from a vector store. 83 | """ 84 | try: 85 | file = openai.vector_stores.files.retrieve( 86 | vector_store_id=vector_store_id, 87 | file_id=file_id 88 | ) 89 | print(json.dumps(file.to_dict() if hasattr(file, 'to_dict') else file, indent=2, ensure_ascii=False)) 90 | except Exception as e: 91 | print(f"Error retrieving vector store file: {e}") 92 | 93 | def vector_store_file_retrieve_content(vector_store_id, file_id): 94 | """ 95 | Retrieve the content of a file from a vector store. 96 | """ 97 | try: 98 | content = openai.vector_stores.files.content( 99 | vector_store_id=vector_store_id, 100 | file_id=file_id 101 | ) 102 | # content is a binary stream, print as text if possible 103 | if hasattr(content, "read"): 104 | print(content.read().decode("utf-8", errors="replace")) 105 | else: 106 | print(content) 107 | except Exception as e: 108 | print(f"Error retrieving vector store file content: {e}") 109 | 110 | def vector_store_file_update_attribute(vector_store_id, file_id, **kwargs): 111 | """ 112 | Update file attributes in a vector store. 113 | kwargs: attributes to update (e.g., metadata) 114 | """ 115 | try: 116 | file = openai.vector_stores.files.update( 117 | vector_store_id=vector_store_id, 118 | file_id=file_id, 119 | **kwargs 120 | ) 121 | print(json.dumps(file.to_dict() if hasattr(file, 'to_dict') else file, indent=2, ensure_ascii=False)) 122 | except Exception as e: 123 | print(f"Error updating vector store file attribute: {e}") 124 | 125 | def vector_store_file_delete(vector_store_id, file_id): 126 | """ 127 | Delete a file from a vector store. 128 | """ 129 | try: 130 | openai.vector_stores.files.delete( 131 | vector_store_id=vector_store_id, 132 | file_id=file_id 133 | ) 134 | print(f"Deleted file {file_id} from vector store {vector_store_id}") 135 | except Exception as e: 136 | print(f"Error deleting vector store file: {e}") 137 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to OpenAI CLI Tool 2 | 3 | First off, thanks for taking the time to contribute! 🎉 4 | 5 | The following is a set of guidelines for contributing to OpenAI CLI Tool. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request. 6 | 7 | ## Table of Contents 8 | 9 | - [Code of Conduct](#code-of-conduct) 10 | - [How Can I Contribute?](#how-can-i-contribute) 11 | - [Development Setup](#development-setup) 12 | - [Pull Request Process](#pull-request-process) 13 | - [Style Guidelines](#style-guidelines) 14 | 15 | ## Code of Conduct 16 | 17 | This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. 18 | 19 | ## How Can I Contribute? 20 | 21 | ### Reporting Bugs 22 | 23 | Before creating bug reports, please check the existing issues to see if the bug has already been reported. When you are creating a bug report, please include as many details as possible: 24 | 25 | - **Use a clear and descriptive title** 26 | - **Describe the exact steps to reproduce the problem** 27 | - **Provide specific examples to demonstrate the steps** 28 | - **Describe the behavior you observed and what behavior you expected** 29 | - **Include your environment details** (OS, Python version, package versions) 30 | 31 | ### Suggesting Enhancements 32 | 33 | Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, please include: 34 | 35 | - **Use a clear and descriptive title** 36 | - **Provide a detailed description of the suggested enhancement** 37 | - **Explain why this enhancement would be useful** 38 | - **List some other tools where this enhancement exists, if applicable** 39 | 40 | ### Your First Code Contribution 41 | 42 | Unsure where to begin contributing? You can start by looking through these `beginner` and `help-wanted` issues: 43 | 44 | - Beginner issues - issues which should only require a few lines of code 45 | - Help wanted issues - issues which should be a bit more involved 46 | 47 | ### Pull Requests 48 | 49 | - Fill in the required template 50 | - Do not include issue numbers in the PR title 51 | - Include screenshots and animated GIFs in your pull request whenever possible 52 | - Follow the Python style guidelines 53 | - Include thoughtfully-worded, well-structured tests 54 | - Document new code based on the Documentation Style Guide 55 | - End all files with a newline 56 | 57 | ## Development Setup 58 | 59 | 1. Fork the repository 60 | 2. Clone your fork: 61 | ```bash 62 | git clone https://github.com/your-username/openaicli.git 63 | cd openaicli 64 | ``` 65 | 66 | 3. Create a virtual environment: 67 | ```bash 68 | python -m venv .venv 69 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 70 | ``` 71 | 72 | 4. Install dependencies: 73 | ```bash 74 | pip install -r requirements.txt 75 | pip install -r requirements-dev.txt # If available 76 | ``` 77 | 78 | 5. Set up your environment: 79 | ```bash 80 | export OPENAI_API_KEY=your_api_key_here 81 | ``` 82 | 83 | 6. Create a branch for your changes: 84 | ```bash 85 | git checkout -b feature/your-feature-name 86 | ``` 87 | 88 | ## Pull Request Process 89 | 90 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build 91 | 2. Update the README.md with details of changes to the interface, including new environment variables, exposed ports, useful file locations, and container parameters 92 | 3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent 93 | 4. You may merge the Pull Request once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you 94 | 95 | ## Style Guidelines 96 | 97 | ### Git Commit Messages 98 | 99 | - Use the present tense ("Add feature" not "Added feature") 100 | - Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 101 | - Limit the first line to 72 characters or less 102 | - Reference issues and pull requests liberally after the first line 103 | - Consider starting the commit message with an applicable emoji: 104 | - 🎨 `:art:` when improving the format/structure of the code 105 | - 🐎 `:racehorse:` when improving performance 106 | - 📝 `:memo:` when writing docs 107 | - 🐛 `:bug:` when fixing a bug 108 | - 🔥 `:fire:` when removing code or files 109 | - ✅ `:white_check_mark:` when adding tests 110 | - 🔒 `:lock:` when dealing with security 111 | 112 | ### Python Style Guide 113 | 114 | - Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) 115 | - Use [Black](https://black.readthedocs.io/) for code formatting 116 | - Use [isort](https://pycqa.github.io/isort/) for import sorting 117 | - Use type hints where appropriate 118 | - Write docstrings for all public functions and classes 119 | - Keep line length to 88 characters (Black default) 120 | 121 | ### Documentation Style Guide 122 | 123 | - Use [Markdown](https://daringfireball.net/projects/markdown/) for documentation 124 | - Reference functions, classes, and modules using backticks: `function_name` 125 | - Include code examples in documentation where helpful 126 | - Keep language clear and concise 127 | 128 | ## Testing 129 | 130 | - Write tests for all new functionality 131 | - Ensure all tests pass before submitting a PR 132 | - Include both positive and negative test cases 133 | - Mock external API calls in tests 134 | 135 | ## Questions? 136 | 137 | Don't hesitate to ask questions! Create an issue with the question label, or reach out to the maintainers. 138 | 139 | Thank you for contributing! 🚀 140 | -------------------------------------------------------------------------------- /src/oai.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import sys 3 | import openai 4 | import glob 5 | import os 6 | from src.file_upload import upload_files 7 | from src.tts import tts 8 | from src.chat import chat 9 | from src.vector_store import ( 10 | vector_store_create, vector_store_list, vector_store_get, vector_store_delete, 11 | vector_store_file_list, vector_store_file_retrieve, vector_store_file_retrieve_content, 12 | vector_store_file_update_attribute, vector_store_file_delete 13 | ) 14 | 15 | # Get API key from environment variable 16 | OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") 17 | if not OPENAI_API_KEY: 18 | print("Error: Please set the OPENAI_API_KEY environment variable.") 19 | sys.exit(1) 20 | openai.api_key = OPENAI_API_KEY 21 | 22 | def main(): 23 | parser = argparse.ArgumentParser( 24 | prog="openai-cli", 25 | description="OpenAI CLI Tool\n\nA command-line interface for OpenAI API: upload files, manage vector stores, TTS, Q&A, and chat.", 26 | formatter_class=argparse.RawTextHelpFormatter 27 | ) 28 | subparsers = parser.add_subparsers(dest="command") 29 | 30 | # 파일 업로드 31 | upload_parser = subparsers.add_parser( 32 | "file-upload", 33 | help="Upload files (wildcard supported)", 34 | description="Upload files to OpenAI. Supports wildcards and vector store upload.\n\nExamples:\n python oai.py file-upload --file '*.md'\n python oai.py file-upload --file 'data.txt' --vector-store-id \n" 35 | ) 36 | upload_parser.add_argument( 37 | "--file", required=True, 38 | help="File name or wildcard pattern to upload. Example: *.md, data.txt" 39 | ) 40 | upload_parser.add_argument( 41 | "--vector-store-id", 42 | help="(Optional) Vector Store ID to upload files into. If omitted, uploads as a general file." 43 | ) 44 | upload_parser.add_argument( 45 | "--purpose", default="assistants", 46 | help="Purpose of the file. Default: assistants. Options: assistants, fine-tune, etc." 47 | ) 48 | 49 | # TTS 50 | tts_parser = subparsers.add_parser( 51 | "tts", 52 | help="Convert text to speech (TTS)", 53 | description="Convert text or a file to speech (mp3).\n\nExamples:\n python oai.py tts --text 'Hello'\n python oai.py tts --input-file input.txt --output output.mp3\n" 54 | ) 55 | tts_parser.add_argument( 56 | "--text", 57 | help="Text to convert to speech. If omitted, --input-file must be provided." 58 | ) 59 | tts_parser.add_argument( 60 | "--input-file", 61 | help="Path to a text file to convert to speech." 62 | ) 63 | tts_parser.add_argument( 64 | "--output", default="speech_output.mp3", 65 | help="Output mp3 file name. Default: speech_output.mp3" 66 | ) 67 | tts_parser.add_argument( 68 | "--model", default="tts-1-hd", 69 | help="TTS model name. Default: tts-1-hd" 70 | ) 71 | tts_parser.add_argument( 72 | "--voice", default="echo", 73 | help="TTS voice name. Default: echo" 74 | ) 75 | 76 | # Chat Completion 77 | chat_parser = subparsers.add_parser( 78 | "chat", 79 | help="Chat Completion (GPT model conversation)", 80 | description="Chat with GPT models.\n\nExamples:\n python oai.py chat --user 'Tell me a joke.'\n python oai.py chat --system 'You are a helpful assistant.' --user 'Summarize this.'\n python oai.py chat --user 'Stream this answer.' --stream\n" 81 | ) 82 | chat_parser.add_argument( 83 | "--model", default="gpt-4o", 84 | help="Model name. Default: gpt-4o. Options: gpt-4o, gpt-3.5-turbo, etc." 85 | ) 86 | chat_parser.add_argument( 87 | "--system", 88 | help="System prompt (optional). Sets the assistant's behavior." 89 | ) 90 | chat_parser.add_argument( 91 | "--user", 92 | help="User prompt (optional). If omitted, will prompt for input." 93 | ) 94 | chat_parser.add_argument( 95 | "--stream", action="store_true", 96 | help="Use streaming output. Prints tokens as they are generated." 97 | ) 98 | chat_parser.add_argument( 99 | "--vector-store-id", 100 | help="(Optional) Vector Store ID to use file_search tool in chat." 101 | ) 102 | 103 | # Vector Store management 104 | vs_parser = subparsers.add_parser( 105 | "vector-store", 106 | help="Manage vector stores", 107 | description="Manage vector stores: create, list, get, delete.\n\nExamples:\n python oai.py vector-store create --name 'MyStore' --description 'Test'\n python oai.py vector-store list\n python oai.py vector-store get --id \n python oai.py vector-store delete --id \n" 108 | ) 109 | vs_subparsers = vs_parser.add_subparsers(dest="vs_command") 110 | 111 | # Create vector store 112 | vs_create = vs_subparsers.add_parser( 113 | "create", 114 | help="Create a new vector store", 115 | description="Create a new vector store. Requires a name. Optionally add a description." 116 | ) 117 | vs_create.add_argument( 118 | "--name", required=True, 119 | help="Name for the vector store." 120 | ) 121 | vs_create.add_argument( 122 | "--description", 123 | help="Description for the vector store (optional)." 124 | ) 125 | 126 | # List vector stores 127 | vs_list = vs_subparsers.add_parser( 128 | "list", 129 | help="List all vector stores", 130 | description="List all vector stores in your account." 131 | ) 132 | 133 | # Retrieve vector store 134 | vs_get = vs_subparsers.add_parser( 135 | "get", 136 | help="Get details of a vector store", 137 | description="Get details of a vector store by ID." 138 | ) 139 | vs_get.add_argument( 140 | "--id", required=True, 141 | help="Vector Store ID to retrieve." 142 | ) 143 | 144 | # Delete vector store 145 | vs_delete = vs_subparsers.add_parser( 146 | "delete", 147 | help="Delete a vector store", 148 | description="Delete a vector store by ID. This action is irreversible." 149 | ) 150 | vs_delete.add_argument( 151 | "--id", required=True, 152 | help="Vector Store ID to delete." 153 | ) 154 | 155 | # Vector Store File management 156 | vsf_parser = subparsers.add_parser( 157 | "vector-store-file", 158 | help="Manage files in a vector store", 159 | description="Manage files in a vector store: list, retrieve-file, retrieve-file-content, update-file-attribute, delete-file." 160 | ) 161 | vsf_subparsers = vsf_parser.add_subparsers(dest="vsf_command") 162 | 163 | # List files 164 | vsf_list = vsf_subparsers.add_parser( 165 | "list", 166 | help="List files in a vector store" 167 | ) 168 | vsf_list.add_argument( 169 | "--vector-store-id", required=True, 170 | help="Vector Store ID" 171 | ) 172 | 173 | # Retrieve file object 174 | vsf_retrieve = vsf_subparsers.add_parser( 175 | "retrieve-file", 176 | help="Retrieve a file object from a vector store" 177 | ) 178 | vsf_retrieve.add_argument( 179 | "--vector-store-id", required=True, 180 | help="Vector Store ID" 181 | ) 182 | vsf_retrieve.add_argument( 183 | "--file-id", required=True, 184 | help="File ID" 185 | ) 186 | 187 | # Retrieve file content 188 | vsf_content = vsf_subparsers.add_parser( 189 | "retrieve-file-content", 190 | help="Retrieve the content of a file from a vector store" 191 | ) 192 | vsf_content.add_argument( 193 | "--vector-store-id", required=True, 194 | help="Vector Store ID" 195 | ) 196 | vsf_content.add_argument( 197 | "--file-id", required=True, 198 | help="File ID" 199 | ) 200 | 201 | # Update file attribute 202 | vsf_update = vsf_subparsers.add_parser( 203 | "update-file-attribute", 204 | help="Update file attributes in a vector store" 205 | ) 206 | vsf_update.add_argument( 207 | "--vector-store-id", required=True, 208 | help="Vector Store ID" 209 | ) 210 | vsf_update.add_argument( 211 | "--file-id", required=True, 212 | help="File ID" 213 | ) 214 | vsf_update.add_argument( 215 | "--attribute", required=True, 216 | help="Attribute name to update (e.g. 'metadata')" 217 | ) 218 | vsf_update.add_argument( 219 | "--value", required=True, 220 | help="New value for the attribute (JSON string if complex type)" 221 | ) 222 | 223 | # Delete file 224 | vsf_delete = vsf_subparsers.add_parser( 225 | "delete-file", 226 | help="Delete a file from a vector store" 227 | ) 228 | vsf_delete.add_argument( 229 | "--vector-store-id", required=True, 230 | help="Vector Store ID" 231 | ) 232 | vsf_delete.add_argument( 233 | "--file-id", required=True, 234 | help="File ID" 235 | ) 236 | 237 | args = parser.parse_args() 238 | 239 | if args.command == "file-upload": 240 | upload_files(args.file, args.vector_store_id, args.purpose) 241 | elif args.command == "tts": 242 | tts(text=args.text, input_file=args.input_file, output=args.output, model=args.model, voice=args.voice) 243 | elif args.command == "ask": 244 | print("The 'ask' command has been removed.") 245 | elif args.command == "chat": 246 | chat( 247 | model=args.model, 248 | system=args.system, 249 | user=args.user, 250 | stream=args.stream, 251 | vector_store_id=args.vector_store_id 252 | ) 253 | elif args.command == "vector-store": 254 | if args.vs_command == "create": 255 | vector_store_create(args.name, args.description) 256 | elif args.vs_command == "list": 257 | vector_store_list() 258 | elif args.vs_command == "get": 259 | vector_store_get(args.id) 260 | elif args.vs_command == "delete": 261 | vector_store_delete(args.id) 262 | else: 263 | print("No vector store subcommand specified. Use create, list, get, or delete.") 264 | elif args.command == "vector-store-file": 265 | if args.vsf_command == "list": 266 | vector_store_file_list(args.vector_store_id) 267 | elif args.vsf_command == "retrieve-file": 268 | vector_store_file_retrieve(args.vector_store_id, args.file_id) 269 | elif args.vsf_command == "retrieve-file-content": 270 | vector_store_file_retrieve_content(args.vector_store_id, args.file_id) 271 | elif args.vsf_command == "update-file-attribute": 272 | import json 273 | try: 274 | value = json.loads(args.value) 275 | except Exception: 276 | value = args.value 277 | vector_store_file_update_attribute( 278 | args.vector_store_id, 279 | args.file_id, 280 | **{args.attribute: value} 281 | ) 282 | elif args.vsf_command == "delete-file": 283 | vector_store_file_delete(args.vector_store_id, args.file_id) 284 | else: 285 | print("No vector store file subcommand specified. Use list, retrieve-file, retrieve-file-content, update-file-attribute, or delete-file.") 286 | else: 287 | parser.print_help() 288 | 289 | if __name__ == "__main__": 290 | main() 291 | --------------------------------------------------------------------------------