├── .env_example ├── test ├── test1.py ├── testcase01-chat.md └── testcase02-r1.md ├── requirements.txt ├── main.py ├── README.md ├── .gitignore ├── LICENSE ├── main-deepseek-chat.py └── main-r1.py /.env_example: -------------------------------------------------------------------------------- 1 | DEEPSEEK_KEY=xxxxxxx -------------------------------------------------------------------------------- /test/test1.py: -------------------------------------------------------------------------------- 1 | def hello_world(): 2 | print("hello openai") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | openai 2 | pydantic 3 | python-dotenv 4 | rich 5 | prompt_toolkit -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | # Please install OpenAI SDK first: `pip3 install openai` 2 | ## 文档地址: 3 | ### https://api-docs.deepseek.com/ 4 | 5 | from openai import OpenAI 6 | 7 | from dotenv import load_dotenv 8 | 9 | import os 10 | load_dotenv() 11 | 12 | def main(): 13 | deepseek_key = os.getenv("DEEPSEEK_KEY") 14 | 15 | client = OpenAI(api_key=os.getenv("DEEPSEEK_KEY"), base_url="https://api.deepseek.com") 16 | 17 | response = client.chat.completions.create( 18 | model="deepseek-chat", 19 | messages=[ 20 | {"role": "system", "content": "You are a helpful assistant"}, 21 | {"role": "user", "content": "Hello"}, 22 | ], 23 | stream=False 24 | ) 25 | 26 | print(response.choices[0].message.content) 27 | 28 | main() -------------------------------------------------------------------------------- /test/testcase01-chat.md: -------------------------------------------------------------------------------- 1 | ## 深度测试用例设计 2 | 3 | 为了充分测试给定代码的各个方面,我们需要设计几个涵盖不同场景的测试用例。这些测试用例将帮助我们观察代码的行为和输出,确保功能正常且符合预期。以下是三个具体的测试用例: 4 | 5 | ### 测试用例1:正常文件创建与编辑 6 | 7 | **输入描述:** 8 | - 创建一个新的文件 `test1.py`,内容为: 9 | ```python 10 | def hello_world(): 11 | print("Hello, World!") 12 | ``` 13 | - 提交一个命令,要求将 `hello_world` 函数的内容修改为: 14 | ```python 15 | def hello_world(): 16 | print("Hello, OpenAI!") 17 | ``` 18 | 19 | **预期输出:** 20 | 1. 首先,代码应正常创建 `test1.py` 文件。 21 | 2. 然后,确认将 `hello_world` 函数的内容更改为新内容 `print("Hello, OpenAI!")`。 22 | 3. 代码在文件被编辑后,能够输出相应的处理结果,并在控制台打印更新后的内容。 23 | 24 | ### 测试用例2:文件编辑失败,原始片段不存在 25 | 26 | **输入描述:** 27 | - 创建一个新的文件 `test2.py`,内容为: 28 | ```python 29 | def greet_user(name): 30 | return f"Hello, {name}!" 31 | ``` 32 | - 提交一个命令,尝试将一个不存在的代码片段 `def goodbye_user(name):` 替换为: 33 | ```python 34 | def goodbye_user(name): 35 | return f"Goodbye, {name}!" 36 | ``` 37 | 38 | **预期输出:** 39 | 1. 当代码尝试替换不存在的片段时,应在控制台输出警告信息,指示未找到原始片段。 40 | 2. 应显示实际文件内容,确保用户看到具体上下文。 41 | 42 | ### 测试用例3:文件路径无效的处理 43 | 44 | **输入描述:** 45 | - 提交一个命令,尝试编辑一个无效路径的文件 `/invalid/path/to/file.py`,并要求更改如下: 46 | ```python 47 | def invalid_function(): 48 | pass 49 | ``` 50 | 51 | **预期输出:** 52 | 1. 代码应在控制台输出错误信息,提示指定的文件路径无效或不可访问。 53 | 2. 不应发生程序崩溃,代码应能优雅地处理此类错误。 54 | 55 | ## 总结 56 | 57 | 上述测试用例涵盖了代码的文件创建、编辑成功与失败的情境,以及对无效文件路径的处理。这些测试将帮助我们确保代码的健壮性和用户体验。通过执行这些用例,可以有效验证系统的各个功能模块是否正常运作。 -------------------------------------------------------------------------------- /test/testcase02-r1.md: -------------------------------------------------------------------------------- 1 | 以下是三个深度测试用例,经过中文翻译,使其更易于理解: 2 | 3 | ### 测试用例 1:**文件创建与验证** 4 | **目标**:确保系统能够正确创建文件、进行安全检查,并记录文件操作。 5 | 6 | **步骤**: 7 | 1. 模拟`create_file()`函数,在有效路径下创建一个文件,并为其指定内容,确保文件大小不超过5MB。 8 | 2. 输入一个有效的文件路径(例如 `/tmp/test_file.py`)和内容,确保文件创建操作成功完成。 9 | 3. 验证以下条件: 10 | - 文件是否被成功创建,并且内容正确。 11 | - 目录结构是否正确,文件是否正常写入。 12 | - 文件操作是否被记录在 `conversation_history` 中。 13 | - 确保路径中不允许使用 `~`(主目录引用)或超过5MB的文件。 14 | 15 | **预期结果**: 16 | - 文件成功创建,且系统正确记录了文件路径和内容。 17 | - 任何尝试创建超出大小限制或包含主目录引用的文件会被拒绝。 18 | 19 | --- 20 | 21 | ### 测试用例 2:**文件编辑与差异处理** 22 | **目标**:验证差异编辑功能,确保文件中的指定代码段能正确替换。 23 | 24 | **步骤**: 25 | 1. 创建一个测试文件(`/tmp/sample.py`),并写入如下内容: 26 | ```python 27 | def add(a, b): 28 | return a + b 29 | ``` 30 | 2. 发起一个差异编辑请求,建议修改`add`函数: 31 | ```json 32 | { 33 | "assistant_reply": "修改 'add' 函数,使其支持字符串拼接。", 34 | "files_to_edit": [ 35 | { 36 | "path": "/tmp/sample.py", 37 | "original_snippet": "return a + b", 38 | "new_snippet": "return str(a) + str(b)" 39 | } 40 | ] 41 | } 42 | ``` 43 | 3. 应用差异编辑,并验证以下内容: 44 | - 文件中的指定代码段是否被正确替换。 45 | - 变更是否正确记录在 `conversation_history` 中。 46 | - 文件是否更新为新代码(`return str(a) + str(b)`)。 47 | 4. 验证当找不到指定代码段或检测到多个匹配时,系统是否会提示用户进行确认。 48 | 49 | **预期结果**: 50 | - 文件`/tmp/sample.py`成功更新。 51 | - 操作记录正确记录了已应用的变更和更新的内容。 52 | - 系统能够识别无法找到代码段或存在歧义的情况,并提示用户处理。 53 | 54 | --- 55 | 56 | ### 测试用例 3:**目录扫描与文件加入** 57 | **目标**:确保系统能够扫描目录,跳过被排除的文件(例如 `.git`、`.env`),并将有效文件添加到会话中。 58 | 59 | **步骤**: 60 | 1. 准备一个测试目录,包含有效文件和被排除的文件: 61 | - 有效文件:`file1.py`、`file2.json`、`readme.md` 62 | - 被排除文件:`.gitignore`、`.env`、`node_modules/` 63 | 2. 执行`/add`命令,添加包含这些文件的目录。 64 | 3. 系统应当: 65 | - 跳过符合排除条件的文件(例如 `.gitignore`、`.env`、`node_modules/`)。 66 | - 将有效文件添加到会话中,并记录它们的内容。 67 | - 确保超过5MB的文件被跳过。 68 | 4. 验证系统是否打印了添加和跳过的文件信息,且添加的文件内容是否正确地被记录到会话中。 69 | 70 | **预期结果**: 71 | - 有效文件成功被添加到会话历史中,并记录了文件内容。 72 | - 被排除的文件未被添加。 73 | - 操作记录被正确记录,并且文件得到了正确处理。 74 | 75 | --- 76 | 77 | 这些测试用例覆盖了文件操作、错误处理和与外部服务的集成,确保系统在文件管理任务中具有稳定性,并能与系统的其他部分无缝集成。 -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### DeepSeek 快速开始教程 🐋 2 | #### 概述 3 | - 这个仓库包含一个强大的编码助手应用,集成 DeepSeek API,能够处理用户对话并生成结构化的 JSON 响应。用户可以通过命令行界面读取本地文件内容、创建新文件,并实时修改现有文件。 4 | 5 | #### 主要功能 6 | 7 | 1. **DeepSeek 客户端配置** 8 | - 自动配置 API 客户端,使用有效的 DEEPSEEK_API_KEY 连接 DeepSeek 服务。 9 | - 根据环境变量指定的 DeepSeek 端点,流式获取 GPT 风格的回复。 10 | 11 | 2. **数据模型** 12 | - 使用 Pydantic 进行类型安全的文件操作,主要包括: 13 | - **FileToCreate**:描述要创建或更新的文件。 14 | - **FileToEdit**:描述要在现有文件中替换的代码片段。 15 | - **AssistantResponse**:结构化的聊天回复和可能的文件操作。 16 | 17 | 3. **系统提示** 18 | - 一个全面的系统提示(system_PROMPT)引导对话,确保所有回复严格符合 JSON 输出格式,并可包括文件的创建或编辑。 19 | 20 | 4. **辅助功能** 21 | - **read_local_file**:读取指定文件路径的内容并返回字符串。 22 | - **create_file**:创建或覆盖文件,内容由用户提供。 23 | - **show_diff_table**:以表格形式展示文件修改建议。 24 | - **apply_diff_edit**:对现有文件应用片段级别的修改。 25 | 26 | 5. **"/add" 命令** 27 | - 使用 `/add path/to/file` 快速读取文件内容并将其插入对话中。 28 | - 使用 `/add path/to/folder` 将目录中的所有文件(排除二进制文件和隐藏文件)添加到对话中。 29 | 30 | 6. **对话流程** 31 | - 维护 `conversation_history` 列表,记录用户与助手的消息。 32 | - 通过 DeepSeek API 流式传输助手的回复,解析为 JSON 格式,确保文本回复和文件修改指令的完整性。 33 | 34 | 7. **交互式会话** 35 | - 运行脚本(如 `python3 main.py`)启动交互循环。 36 | - 输入请求或代码问题,使用 `/add path/to/file` 将文件内容添加到对话中。 37 | - 当助手提出新或编辑后的文件时,可直接确认更改。 38 | - 输入 "exit" 或 "quit" 退出会话。 39 | 40 | #### 快速入门 41 | 0. 如果自己还没有DEEPSEEK_KEY,可以在https://api-docs.deepseek.com/ 花5分钟注册并申请一个,通常官方会提供10元到新账号里。 42 | 43 | 1. 配置 .env 文件,并添加 DeepSeek API 密钥: 44 | ```plaintext 45 | DEEPSEEK_KEY=你的_api_key 46 | ``` 47 | 48 | 2. 安装依赖并运行(选择其中一种方式): 49 | 50 | - **使用 pip 安装**: 51 | ```bash 52 | git clone --depth 1 https://github.com/XiaomingX/deepseek-quickstart 53 | pip install -r requirements.txt 54 | python3 main.py 55 | ``` 56 | 57 | 3. 体验多行流式响应、使用 "/add path/to/file" 读取文件内容,并在批准后精确编辑文件。 58 | 59 | #### 推理版(main-r1.py) 60 | 61 | 新增 `main-r1.py` 脚本,采用 DeepSeek 推理模型(`deepseek-reasoner`),支持“思维链”(CoT)推理: 62 | 63 | - 展示推理过程,然后给出最终答案。 64 | - 保留所有文件操作和差异编辑功能。 65 | - 显示推理过程,并仅记录最终结论。 66 | 67 | 使用 `python3 main-r1.py`启动,享受推理过程增强体验。 68 | 69 | > **注意**:这是 xiaomingx 开发的实验项目,旨在测试 DeepSeek v3 API 的新功能,作为快速原型使用时请注意。 70 | 71 | # 更多官方材料 72 | - 我也在探索中,官方仓库地址是:https://github.com/deepseek-ai -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /main-deepseek-chat.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python3 3 | 4 | import os 5 | import sys 6 | import json 7 | from pathlib import Path 8 | from textwrap import dedent 9 | from typing import List, Dict, Any, Optional 10 | from openai import OpenAI 11 | from pydantic import BaseModel 12 | from dotenv import load_dotenv 13 | from rich.console import Console 14 | from rich.table import Table 15 | from rich.panel import Panel 16 | from rich.style import Style 17 | from dotenv import load_dotenv 18 | load_dotenv() 19 | 20 | console = Console() 21 | 22 | # -------------------------------------------------------------------------------- 23 | # 1. Configure OpenAI client and load environment variables 24 | # -------------------------------------------------------------------------------- 25 | load_dotenv() # Load environment variables from .env file 26 | client = OpenAI( 27 | api_key=os.getenv("DEEPSEEK_KEY"), 28 | base_url="https://api.deepseek.com" 29 | ) # Configure for DeepSeek API 30 | 31 | # -------------------------------------------------------------------------------- 32 | # 2. Define our schema using Pydantic for type safety 33 | # -------------------------------------------------------------------------------- 34 | class FileToCreate(BaseModel): 35 | path: str 36 | content: str 37 | 38 | # NEW: Diff editing structure 39 | class FileToEdit(BaseModel): 40 | path: str 41 | original_snippet: str 42 | new_snippet: str 43 | 44 | class AssistantResponse(BaseModel): 45 | assistant_reply: str 46 | files_to_create: Optional[List[FileToCreate]] = None 47 | # NEW: optionally hold diff edits 48 | files_to_edit: Optional[List[FileToEdit]] = None 49 | 50 | # -------------------------------------------------------------------------------- 51 | # 3. system prompt 52 | # -------------------------------------------------------------------------------- 53 | system_PROMPT = dedent("""\ 54 | You are an elite software engineer called DeepSeek Engineer with decades of experience across all programming domains. 55 | Your expertise spans system design, algorithms, testing, and best practices. 56 | You provide thoughtful, well-structured solutions while explaining your reasoning. 57 | 58 | Core capabilities: 59 | 1. Code Analysis & Discussion 60 | - Analyze code with expert-level insight 61 | - Explain complex concepts clearly 62 | - Suggest optimizations and best practices 63 | - Debug issues with precision 64 | 65 | 2. File Operations: 66 | a) Read existing files 67 | - Access user-provided file contents for context 68 | - Analyze multiple files to understand project structure 69 | 70 | b) Create new files 71 | - Generate complete new files with proper structure 72 | - Create complementary files (tests, configs, etc.) 73 | 74 | c) Edit existing files 75 | - Make precise changes using diff-based editing 76 | - Modify specific sections while preserving context 77 | - Suggest refactoring improvements 78 | 79 | Output Format: 80 | You must provide responses in this JSON structure: 81 | { 82 | "assistant_reply": "Your main explanation or response", 83 | "files_to_create": [ 84 | { 85 | "path": "path/to/new/file", 86 | "content": "complete file content" 87 | } 88 | ], 89 | "files_to_edit": [ 90 | { 91 | "path": "path/to/existing/file", 92 | "original_snippet": "exact code to be replaced", 93 | "new_snippet": "new code to insert" 94 | } 95 | ] 96 | } 97 | 98 | Guidelines: 99 | 1. For normal responses, use 'assistant_reply' 100 | 2. When creating files, include full content in 'files_to_create' 101 | 3. For editing files: 102 | - Use 'files_to_edit' for precise changes 103 | - Include enough context in original_snippet to locate the change 104 | - Ensure new_snippet maintains proper indentation 105 | - Prefer targeted edits over full file replacements 106 | 4. Always explain your changes and reasoning 107 | 5. Consider edge cases and potential impacts 108 | 6. Follow language-specific best practices 109 | 7. Suggest tests or validation steps when appropriate 110 | 111 | Remember: You're a senior engineer - be thorough, precise, and thoughtful in your solutions. 112 | """) 113 | 114 | # -------------------------------------------------------------------------------- 115 | # 4. Helper functions 116 | # -------------------------------------------------------------------------------- 117 | 118 | def read_local_file(file_path: str) -> str: 119 | """Return the text content of a local file.""" 120 | with open(file_path, "r", encoding="utf-8") as f: 121 | return f.read() 122 | 123 | def create_file(path: str, content: str): 124 | """Create (or overwrite) a file at 'path' with the given 'content'.""" 125 | file_path = Path(path) 126 | file_path.parent.mkdir(parents=True, exist_ok=True) # ensures any dirs exist 127 | with open(file_path, "w", encoding="utf-8") as f: 128 | f.write(content) 129 | console.print(f"[green]✓[/green] Created/updated file at '[cyan]{file_path}[/cyan]'") 130 | 131 | # Record the action 132 | conversation_history.append({ 133 | "role": "assistant", 134 | "content": f"✓ Created/updated file at '{file_path}'" 135 | }) 136 | 137 | # NEW: Add the actual content to conversation context 138 | normalized_path = normalize_path(str(file_path)) 139 | conversation_history.append({ 140 | "role": "system", 141 | "content": f"Content of file '{normalized_path}':\n\n{content}" 142 | }) 143 | 144 | # NEW: Show the user a table of proposed edits and confirm 145 | def show_diff_table(files_to_edit: List[FileToEdit]) -> None: 146 | if not files_to_edit: 147 | return 148 | 149 | # Enable multi-line rows by setting show_lines=True 150 | table = Table(title="Proposed Edits", show_header=True, header_style="bold magenta", show_lines=True) 151 | table.add_column("File Path", style="cyan") 152 | table.add_column("Original", style="red") 153 | table.add_column("New", style="green") 154 | 155 | for edit in files_to_edit: 156 | table.add_row(edit.path, edit.original_snippet, edit.new_snippet) 157 | 158 | console.print(table) 159 | 160 | # NEW: Apply diff edits 161 | def apply_diff_edit(path: str, original_snippet: str, new_snippet: str): 162 | """Reads the file at 'path', replaces the first occurrence of 'original_snippet' with 'new_snippet', then overwrites.""" 163 | try: 164 | content = read_local_file(path) 165 | if original_snippet in content: 166 | updated_content = content.replace(original_snippet, new_snippet, 1) 167 | create_file(path, updated_content) # This will now also update conversation context 168 | console.print(f"[green]✓[/green] Applied diff edit to '[cyan]{path}[/cyan]'") 169 | conversation_history.append({ 170 | "role": "assistant", 171 | "content": f"✓ Applied diff edit to '{path}'" 172 | }) 173 | else: 174 | # NEW: Add debug info about the mismatch 175 | console.print(f"[yellow]⚠[/yellow] Original snippet not found in '[cyan]{path}[/cyan]'. No changes made.", style="yellow") 176 | console.print("\nExpected snippet:", style="yellow") 177 | console.print(Panel(original_snippet, title="Expected", border_style="yellow")) 178 | console.print("\nActual file content:", style="yellow") 179 | console.print(Panel(content, title="Actual", border_style="yellow")) 180 | except FileNotFoundError: 181 | console.print(f"[red]✗[/red] File not found for diff editing: '[cyan]{path}[/cyan]'", style="red") 182 | 183 | def try_handle_add_command(user_input: str) -> bool: 184 | """ 185 | If user_input starts with '/add ', read that file and insert its content 186 | into conversation as a system message. Returns True if handled; else False. 187 | """ 188 | prefix = "/add " 189 | if user_input.strip().lower().startswith(prefix): 190 | file_path = user_input[len(prefix):].strip() 191 | try: 192 | content = read_local_file(file_path) 193 | conversation_history.append({ 194 | "role": "system", 195 | "content": f"Content of file '{file_path}':\n\n{content}" 196 | }) 197 | console.print(f"[green]✓[/green] Added file '[cyan]{file_path}[/cyan]' to conversation.\n") 198 | except OSError as e: 199 | console.print(f"[red]✗[/red] Could not add file '[cyan]{file_path}[/cyan]': {e}\n", style="red") 200 | return True 201 | return False 202 | 203 | def ensure_file_in_context(file_path: str) -> bool: 204 | """ 205 | Ensures the file content is in the conversation context. 206 | Returns True if successful, False if file not found. 207 | """ 208 | try: 209 | normalized_path = normalize_path(file_path) 210 | content = read_local_file(normalized_path) 211 | file_marker = f"Content of file '{normalized_path}'" 212 | if not any(file_marker in msg["content"] for msg in conversation_history): 213 | conversation_history.append({ 214 | "role": "system", 215 | "content": f"{file_marker}:\n\n{content}" 216 | }) 217 | return True 218 | except OSError: 219 | console.print(f"[red]✗[/red] Could not read file '[cyan]{file_path}[/cyan]' for editing context", style="red") 220 | return False 221 | 222 | def normalize_path(path_str: str) -> str: 223 | """Return a canonical, absolute version of the path.""" 224 | return str(Path(path_str).resolve()) 225 | 226 | # -------------------------------------------------------------------------------- 227 | # 5. Conversation state 228 | # -------------------------------------------------------------------------------- 229 | conversation_history = [ 230 | {"role": "system", "content": system_PROMPT} 231 | ] 232 | 233 | # -------------------------------------------------------------------------------- 234 | # 6. OpenAI API interaction with streaming 235 | # -------------------------------------------------------------------------------- 236 | 237 | def guess_files_in_message(user_message: str) -> List[str]: 238 | """ 239 | Attempt to guess which files the user might be referencing. 240 | Returns normalized absolute paths. 241 | """ 242 | recognized_extensions = [".css", ".html", ".js", ".py", ".json", ".md"] 243 | potential_paths = [] 244 | for word in user_message.split(): 245 | if any(ext in word for ext in recognized_extensions) or "/" in word: 246 | path = word.strip("',\"") 247 | try: 248 | normalized_path = normalize_path(path) 249 | potential_paths.append(normalized_path) 250 | except (OSError, ValueError): 251 | continue 252 | return potential_paths 253 | 254 | def stream_openai_response(user_message: str): 255 | """ 256 | Streams the DeepSeek chat completion response and handles structured output. 257 | Returns the final AssistantResponse. 258 | """ 259 | # Attempt to guess which file(s) user references 260 | potential_paths = guess_files_in_message(user_message) 261 | 262 | valid_files = {} 263 | 264 | # Try to read all potential files before the API call 265 | for path in potential_paths: 266 | try: 267 | content = read_local_file(path) 268 | valid_files[path] = content # path is already normalized 269 | file_marker = f"Content of file '{path}'" 270 | # Add to conversation if we haven't already 271 | if not any(file_marker in msg["content"] for msg in conversation_history): 272 | conversation_history.append({ 273 | "role": "system", 274 | "content": f"{file_marker}:\n\n{content}" 275 | }) 276 | except OSError: 277 | error_msg = f"Cannot proceed: File '{path}' does not exist or is not accessible" 278 | console.print(f"[red]✗[/red] {error_msg}", style="red") 279 | continue 280 | 281 | # Now proceed with the API call 282 | conversation_history.append({"role": "user", "content": user_message}) 283 | 284 | try: 285 | stream = client.chat.completions.create( 286 | model="deepseek-chat", 287 | messages=conversation_history, 288 | response_format={"type": "json_object"}, 289 | max_completion_tokens=8000, 290 | stream=True 291 | ) 292 | 293 | console.print("\nAssistant> ", style="bold blue", end="") 294 | full_content = "" 295 | 296 | for chunk in stream: 297 | if chunk.choices[0].delta.content: 298 | content_chunk = chunk.choices[0].delta.content 299 | full_content += content_chunk 300 | console.print(content_chunk, end="") 301 | 302 | console.print() 303 | 304 | try: 305 | parsed_response = json.loads(full_content) 306 | 307 | # [NEW] Ensure assistant_reply is present 308 | if "assistant_reply" not in parsed_response: 309 | parsed_response["assistant_reply"] = "" 310 | 311 | # If assistant tries to edit files not in valid_files, remove them 312 | if "files_to_edit" in parsed_response and parsed_response["files_to_edit"]: 313 | new_files_to_edit = [] 314 | for edit in parsed_response["files_to_edit"]: 315 | try: 316 | edit_abs_path = normalize_path(edit["path"]) 317 | # If we have the file in context or can read it now 318 | if edit_abs_path in valid_files or ensure_file_in_context(edit_abs_path): 319 | edit["path"] = edit_abs_path # Use normalized path 320 | new_files_to_edit.append(edit) 321 | except (OSError, ValueError): 322 | console.print(f"[yellow]⚠[/yellow] Skipping invalid path: '{edit['path']}'", style="yellow") 323 | continue 324 | parsed_response["files_to_edit"] = new_files_to_edit 325 | 326 | response_obj = AssistantResponse(**parsed_response) 327 | 328 | # Save the assistant's textual reply to conversation 329 | conversation_history.append({ 330 | "role": "assistant", 331 | "content": response_obj.assistant_reply 332 | }) 333 | 334 | return response_obj 335 | 336 | except json.JSONDecodeError: 337 | error_msg = "Failed to parse JSON response from assistant" 338 | console.print(f"[red]✗[/red] {error_msg}", style="red") 339 | return AssistantResponse( 340 | assistant_reply=error_msg, 341 | files_to_create=[] 342 | ) 343 | 344 | except Exception as e: 345 | error_msg = f"DeepSeek API error: {str(e)}" 346 | console.print(f"\n[red]✗[/red] {error_msg}", style="red") 347 | return AssistantResponse( 348 | assistant_reply=error_msg, 349 | files_to_create=[] 350 | ) 351 | 352 | # -------------------------------------------------------------------------------- 353 | # 7. Main interactive loop 354 | # -------------------------------------------------------------------------------- 355 | 356 | def main(): 357 | console.print(Panel.fit( 358 | "[bold blue]Welcome to Deep Seek Engineer with Structured Output[/bold blue] [green](and streaming)[/green]!🐋", 359 | border_style="blue" 360 | )) 361 | console.print( 362 | "To include a file in the conversation, use '[bold magenta]/add path/to/file[/bold magenta]'.\n" 363 | "Type '[bold red]exit[/bold red]' or '[bold red]quit[/bold red]' to end.\n" 364 | ) 365 | 366 | while True: 367 | try: 368 | user_input = console.input("[bold green]You>[/bold green] ").strip() 369 | except (EOFError, KeyboardInterrupt): 370 | console.print("\n[yellow]Exiting.[/yellow]") 371 | break 372 | 373 | if not user_input: 374 | continue 375 | 376 | if user_input.lower() in ["exit", "quit"]: 377 | console.print("[yellow]Goodbye![/yellow]") 378 | break 379 | 380 | # If user is reading a file 381 | if try_handle_add_command(user_input): 382 | continue 383 | 384 | # Get streaming response from OpenAI (DeepSeek) 385 | response_data = stream_openai_response(user_input) 386 | 387 | # Create any files if requested 388 | if response_data.files_to_create: 389 | for file_info in response_data.files_to_create: 390 | create_file(file_info.path, file_info.content) 391 | 392 | # Show and confirm diff edits if requested 393 | if response_data.files_to_edit: 394 | show_diff_table(response_data.files_to_edit) 395 | confirm = console.input( 396 | "\nDo you want to apply these changes? ([green]y[/green]/[red]n[/red]): " 397 | ).strip().lower() 398 | if confirm == 'y': 399 | for edit_info in response_data.files_to_edit: 400 | apply_diff_edit(edit_info.path, edit_info.original_snippet, edit_info.new_snippet) 401 | else: 402 | console.print("[yellow]ℹ[/yellow] Skipped applying diff edits.", style="yellow") 403 | 404 | console.print("[blue]Session finished.[/blue]") 405 | 406 | if __name__ == "__main__": 407 | main() 408 | -------------------------------------------------------------------------------- /main-r1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import os 4 | import sys 5 | import json 6 | from pathlib import Path 7 | from textwrap import dedent 8 | from typing import List, Dict, Any, Optional 9 | from openai import OpenAI 10 | from pydantic import BaseModel 11 | from dotenv import load_dotenv 12 | from rich.console import Console 13 | from rich.table import Table 14 | from rich.panel import Panel 15 | from rich.style import Style 16 | from prompt_toolkit import PromptSession 17 | from prompt_toolkit.styles import Style as PromptStyle 18 | 19 | # Initialize Rich console and prompt session 20 | console = Console() 21 | prompt_session = PromptSession( 22 | style=PromptStyle.from_dict({ 23 | 'prompt': '#00aa00 bold', # Green prompt 24 | }) 25 | ) 26 | 27 | load_dotenv() # Load environment variables from .env file 28 | client = OpenAI( 29 | api_key=os.getenv("DEEPSEEK_KEY"), 30 | base_url="https://api.deepseek.com" 31 | ) # Configure for DeepSeek API 32 | 33 | class FileToCreate(BaseModel): 34 | path: str 35 | content: str 36 | 37 | class FileToEdit(BaseModel): 38 | path: str 39 | original_snippet: str 40 | new_snippet: str 41 | 42 | class AssistantResponse(BaseModel): 43 | assistant_reply: str 44 | files_to_create: Optional[List[FileToCreate]] = None 45 | files_to_edit: Optional[List[FileToEdit]] = None 46 | 47 | # -------------------------------------------------------------------------------- 48 | # 3. system prompt 49 | # -------------------------------------------------------------------------------- 50 | system_PROMPT = dedent("""\ 51 | You are an elite software engineer called DeepSeek Engineer with decades of experience across all programming domains. 52 | Your expertise spans system design, algorithms, testing, and best practices. 53 | You provide thoughtful, well-structured solutions while explaining your reasoning. 54 | 55 | Core capabilities: 56 | 1. Code Analysis & Discussion 57 | - Analyze code with expert-level insight 58 | - Explain complex concepts clearly 59 | - Suggest optimizations and best practices 60 | - Debug issues with precision 61 | 62 | 2. File Operations: 63 | a) Read existing files 64 | - Access user-provided file contents for context 65 | - Analyze multiple files to understand project structure 66 | 67 | b) Create new files 68 | - Generate complete new files with proper structure 69 | - Create complementary files (tests, configs, etc.) 70 | 71 | c) Edit existing files 72 | - Make precise changes using diff-based editing 73 | - Modify specific sections while preserving context 74 | - Suggest refactoring improvements 75 | 76 | Output Format: 77 | You must provide responses in this JSON structure: 78 | { 79 | "assistant_reply": "Your main explanation or response", 80 | "files_to_create": [ 81 | { 82 | "path": "path/to/new/file", 83 | "content": "complete file content" 84 | } 85 | ], 86 | "files_to_edit": [ 87 | { 88 | "path": "path/to/existing/file", 89 | "original_snippet": "exact code to be replaced", 90 | "new_snippet": "new code to insert" 91 | } 92 | ] 93 | } 94 | 95 | Guidelines: 96 | 1. YOU ONLY RETURN JSON, NO OTHER TEXT OR EXPLANATION OUTSIDE THE JSON!!! 97 | 2. For normal responses, use 'assistant_reply' 98 | 3. When creating files, include full content in 'files_to_create' 99 | 4. For editing files: 100 | - Use 'files_to_edit' for precise changes 101 | - Include enough context in original_snippet to locate the change 102 | - Ensure new_snippet maintains proper indentation 103 | - Prefer targeted edits over full file replacements 104 | 5. Always explain your changes and reasoning 105 | 6. Consider edge cases and potential impacts 106 | 7. Follow language-specific best practices 107 | 8. Suggest tests or validation steps when appropriate 108 | 109 | Remember: You're a senior engineer - be thorough, precise, and thoughtful in your solutions. 110 | """) 111 | 112 | 113 | def read_local_file(file_path: str) -> str: 114 | """Return the text content of a local file.""" 115 | with open(file_path, "r", encoding="utf-8") as f: 116 | return f.read() 117 | 118 | def create_file(path: str, content: str): 119 | """Create (or overwrite) a file at 'path' with the given 'content'.""" 120 | file_path = Path(path) 121 | 122 | # Security checks 123 | if any(part.startswith('~') for part in file_path.parts): 124 | raise ValueError("Home directory references not allowed") 125 | normalized_path = normalize_path(str(file_path)) 126 | 127 | # Validate reasonable file size for operations 128 | if len(content) > 5_000_000: # 5MB limit 129 | raise ValueError("File content exceeds 5MB size limit") 130 | 131 | file_path.parent.mkdir(parents=True, exist_ok=True) 132 | with open(file_path, "w", encoding="utf-8") as f: 133 | f.write(content) 134 | console.print(f"[green]✓[/green] Created/updated file at '[cyan]{file_path}[/cyan]'") 135 | 136 | # Record the action as a system message 137 | conversation_history.append({ 138 | "role": "system", 139 | "content": f"File operation: Created/updated file at '{file_path}'" 140 | }) 141 | 142 | normalized_path = normalize_path(str(file_path)) 143 | conversation_history.append({ 144 | "role": "system", 145 | "content": f"Content of file '{normalized_path}':\n\n{content}" 146 | }) 147 | 148 | def show_diff_table(files_to_edit: List[FileToEdit]) -> None: 149 | if not files_to_edit: 150 | return 151 | 152 | table = Table(title="Proposed Edits", show_header=True, header_style="bold magenta", show_lines=True) 153 | table.add_column("File Path", style="cyan") 154 | table.add_column("Original", style="red") 155 | table.add_column("New", style="green") 156 | 157 | for edit in files_to_edit: 158 | table.add_row(edit.path, edit.original_snippet, edit.new_snippet) 159 | 160 | console.print(table) 161 | 162 | def apply_diff_edit(path: str, original_snippet: str, new_snippet: str): 163 | """Reads the file at 'path', replaces the first occurrence of 'original_snippet' with 'new_snippet', then overwrites.""" 164 | try: 165 | content = read_local_file(path) 166 | 167 | # Verify we're replacing the exact intended occurrence 168 | occurrences = content.count(original_snippet) 169 | if occurrences == 0: 170 | raise ValueError("Original snippet not found") 171 | if occurrences > 1: 172 | console.print(f"[yellow]Multiple matches ({occurrences}) found - requiring line numbers for safety", style="yellow") 173 | console.print("Use format:\n--- original.py (lines X-Y)\n+++ modified.py\n") 174 | raise ValueError(f"Ambiguous edit: {occurrences} matches") 175 | 176 | updated_content = content.replace(original_snippet, new_snippet, 1) 177 | create_file(path, updated_content) 178 | console.print(f"[green]✓[/green] Applied diff edit to '[cyan]{path}[/cyan]'") 179 | # Record the edit as a system message 180 | conversation_history.append({ 181 | "role": "system", 182 | "content": f"File operation: Applied diff edit to '{path}'" 183 | }) 184 | except FileNotFoundError: 185 | console.print(f"[red]✗[/red] File not found for diff editing: '[cyan]{path}[/cyan]'", style="red") 186 | except ValueError as e: 187 | console.print(f"[yellow]⚠[/yellow] {str(e)} in '[cyan]{path}[/cyan]'. No changes made.", style="yellow") 188 | console.print("\nExpected snippet:", style="yellow") 189 | console.print(Panel(original_snippet, title="Expected", border_style="yellow")) 190 | console.print("\nActual file content:", style="yellow") 191 | console.print(Panel(content, title="Actual", border_style="yellow")) 192 | 193 | def try_handle_add_command(user_input: str) -> bool: 194 | prefix = "/add " 195 | if user_input.strip().lower().startswith(prefix): 196 | path_to_add = user_input[len(prefix):].strip() 197 | try: 198 | normalized_path = normalize_path(path_to_add) 199 | if os.path.isdir(normalized_path): 200 | # Handle entire directory 201 | add_directory_to_conversation(normalized_path) 202 | else: 203 | # Handle a single file as before 204 | content = read_local_file(normalized_path) 205 | conversation_history.append({ 206 | "role": "system", 207 | "content": f"Content of file '{normalized_path}':\n\n{content}" 208 | }) 209 | console.print(f"[green]✓[/green] Added file '[cyan]{normalized_path}[/cyan]' to conversation.\n") 210 | except OSError as e: 211 | console.print(f"[red]✗[/red] Could not add path '[cyan]{path_to_add}[/cyan]': {e}\n", style="red") 212 | return True 213 | return False 214 | 215 | def add_directory_to_conversation(directory_path: str): 216 | with console.status("[bold green]Scanning directory...") as status: 217 | excluded_files = { 218 | # Python specific 219 | ".DS_Store", "Thumbs.db", ".gitignore", ".python-version", 220 | "uv.lock", ".uv", "uvenv", ".uvenv", ".venv", "venv", 221 | "__pycache__", ".pytest_cache", ".coverage", ".mypy_cache", 222 | # Node.js / Web specific 223 | "node_modules", "package-lock.json", "yarn.lock", "pnpm-lock.yaml", 224 | ".next", ".nuxt", "dist", "build", ".cache", ".parcel-cache", 225 | ".turbo", ".vercel", ".output", ".contentlayer", 226 | # Build outputs 227 | "out", "coverage", ".nyc_output", "storybook-static", 228 | # Environment and config 229 | ".env", ".env.local", ".env.development", ".env.production", 230 | # Misc 231 | ".git", ".svn", ".hg", "CVS" 232 | } 233 | excluded_extensions = { 234 | # Binary and media files 235 | ".png", ".jpg", ".jpeg", ".gif", ".ico", ".svg", ".webp", ".avif", 236 | ".mp4", ".webm", ".mov", ".mp3", ".wav", ".ogg", 237 | ".zip", ".tar", ".gz", ".7z", ".rar", 238 | ".exe", ".dll", ".so", ".dylib", ".bin", 239 | # Documents 240 | ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx", 241 | # Python specific 242 | ".pyc", ".pyo", ".pyd", ".egg", ".whl", 243 | # UV specific 244 | ".uv", ".uvenv", 245 | # Database and logs 246 | ".db", ".sqlite", ".sqlite3", ".log", 247 | # IDE specific 248 | ".idea", ".vscode", 249 | # Web specific 250 | ".map", ".chunk.js", ".chunk.css", 251 | ".min.js", ".min.css", ".bundle.js", ".bundle.css", 252 | # Cache and temp files 253 | ".cache", ".tmp", ".temp", 254 | # Font files 255 | ".ttf", ".otf", ".woff", ".woff2", ".eot" 256 | } 257 | skipped_files = [] 258 | added_files = [] 259 | total_files_processed = 0 260 | max_files = 1000 # Reasonable limit for files to process 261 | max_file_size = 5_000_000 # 5MB limit 262 | 263 | for root, dirs, files in os.walk(directory_path): 264 | if total_files_processed >= max_files: 265 | console.print(f"[yellow]⚠[/yellow] Reached maximum file limit ({max_files})") 266 | break 267 | 268 | status.update(f"[bold green]Scanning {root}...") 269 | # Skip hidden directories and excluded directories 270 | dirs[:] = [d for d in dirs if not d.startswith('.') and d not in excluded_files] 271 | 272 | for file in files: 273 | if total_files_processed >= max_files: 274 | break 275 | 276 | if file.startswith('.') or file in excluded_files: 277 | skipped_files.append(os.path.join(root, file)) 278 | continue 279 | 280 | _, ext = os.path.splitext(file) 281 | if ext.lower() in excluded_extensions: 282 | skipped_files.append(os.path.join(root, file)) 283 | continue 284 | 285 | full_path = os.path.join(root, file) 286 | 287 | try: 288 | # Check file size before processing 289 | if os.path.getsize(full_path) > max_file_size: 290 | skipped_files.append(f"{full_path} (exceeds size limit)") 291 | continue 292 | 293 | # Check if it's binary 294 | if is_binary_file(full_path): 295 | skipped_files.append(full_path) 296 | continue 297 | 298 | normalized_path = normalize_path(full_path) 299 | content = read_local_file(normalized_path) 300 | conversation_history.append({ 301 | "role": "system", 302 | "content": f"Content of file '{normalized_path}':\n\n{content}" 303 | }) 304 | added_files.append(normalized_path) 305 | total_files_processed += 1 306 | 307 | except OSError: 308 | skipped_files.append(full_path) 309 | 310 | console.print(f"[green]✓[/green] Added folder '[cyan]{directory_path}[/cyan]' to conversation.") 311 | if added_files: 312 | console.print(f"\n[bold]Added files:[/bold] ({len(added_files)} of {total_files_processed})") 313 | for f in added_files: 314 | console.print(f"[cyan]{f}[/cyan]") 315 | if skipped_files: 316 | console.print(f"\n[yellow]Skipped files:[/yellow] ({len(skipped_files)})") 317 | for f in skipped_files: 318 | console.print(f"[yellow]{f}[/yellow]") 319 | console.print() 320 | 321 | def is_binary_file(file_path: str, peek_size: int = 1024) -> bool: 322 | try: 323 | with open(file_path, 'rb') as f: 324 | chunk = f.read(peek_size) 325 | # If there is a null byte in the sample, treat it as binary 326 | if b'\0' in chunk: 327 | return True 328 | return False 329 | except Exception: 330 | # If we fail to read, just treat it as binary to be safe 331 | return True 332 | 333 | def ensure_file_in_context(file_path: str) -> bool: 334 | try: 335 | normalized_path = normalize_path(file_path) 336 | content = read_local_file(normalized_path) 337 | file_marker = f"Content of file '{normalized_path}'" 338 | if not any(file_marker in msg["content"] for msg in conversation_history): 339 | conversation_history.append({ 340 | "role": "system", 341 | "content": f"{file_marker}:\n\n{content}" 342 | }) 343 | return True 344 | except OSError: 345 | console.print(f"[red]✗[/red] Could not read file '[cyan]{file_path}[/cyan]' for editing context", style="red") 346 | return False 347 | 348 | def normalize_path(path_str: str) -> str: 349 | """Return a canonical, absolute version of the path with security checks.""" 350 | path = Path(path_str).resolve() 351 | 352 | # Prevent directory traversal attacks 353 | if ".." in path.parts: 354 | raise ValueError(f"Invalid path: {path_str} contains parent directory references") 355 | 356 | return str(path) 357 | 358 | # -------------------------------------------------------------------------------- 359 | # 5. Conversation state 360 | # -------------------------------------------------------------------------------- 361 | conversation_history = [ 362 | {"role": "system", "content": system_PROMPT} 363 | ] 364 | 365 | # -------------------------------------------------------------------------------- 366 | # 6. OpenAI API interaction with streaming 367 | # -------------------------------------------------------------------------------- 368 | 369 | def guess_files_in_message(user_message: str) -> List[str]: 370 | recognized_extensions = [".css", ".html", ".js", ".py", ".json", ".md"] 371 | potential_paths = [] 372 | for word in user_message.split(): 373 | if any(ext in word for ext in recognized_extensions) or "/" in word: 374 | path = word.strip("',\"") 375 | try: 376 | normalized_path = normalize_path(path) 377 | potential_paths.append(normalized_path) 378 | except (OSError, ValueError): 379 | continue 380 | return potential_paths 381 | 382 | def stream_openai_response(user_message: str): 383 | # First, clean up the conversation history while preserving system messages with file content 384 | system_msgs = [conversation_history[0]] # Keep initial system prompt 385 | file_context = [] 386 | user_assistant_pairs = [] 387 | 388 | for msg in conversation_history[1:]: 389 | if msg["role"] == "system" and "Content of file '" in msg["content"]: 390 | file_context.append(msg) 391 | elif msg["role"] in ["user", "assistant"]: 392 | user_assistant_pairs.append(msg) 393 | 394 | # Only keep complete user-assistant pairs 395 | if len(user_assistant_pairs) % 2 != 0: 396 | user_assistant_pairs = user_assistant_pairs[:-1] 397 | 398 | # Rebuild clean history with files preserved 399 | cleaned_history = system_msgs + file_context 400 | cleaned_history.extend(user_assistant_pairs) 401 | cleaned_history.append({"role": "user", "content": user_message}) 402 | 403 | # Replace conversation_history with cleaned version 404 | conversation_history.clear() 405 | conversation_history.extend(cleaned_history) 406 | 407 | potential_paths = guess_files_in_message(user_message) 408 | valid_files = {} 409 | 410 | for path in potential_paths: 411 | try: 412 | content = read_local_file(path) 413 | valid_files[path] = content 414 | file_marker = f"Content of file '{path}'" 415 | if not any(file_marker in msg["content"] for msg in conversation_history): 416 | conversation_history.append({ 417 | "role": "system", 418 | "content": f"{file_marker}:\n\n{content}" 419 | }) 420 | except OSError: 421 | error_msg = f"Cannot proceed: File '{path}' does not exist or is not accessible" 422 | console.print(f"[red]✗[/red] {error_msg}", style="red") 423 | continue 424 | 425 | try: 426 | stream = client.chat.completions.create( 427 | model="deepseek-reasoner", 428 | messages=conversation_history, 429 | max_completion_tokens=8000, 430 | stream=True 431 | ) 432 | 433 | console.print("\nThinking...", style="bold yellow") 434 | reasoning_started = False 435 | reasoning_content = "" 436 | final_content = "" 437 | 438 | for chunk in stream: 439 | if chunk.choices[0].delta.reasoning_content: 440 | if not reasoning_started: 441 | console.print("\nReasoning:", style="bold yellow") 442 | reasoning_started = True 443 | console.print(chunk.choices[0].delta.reasoning_content, end="") 444 | reasoning_content += chunk.choices[0].delta.reasoning_content 445 | elif chunk.choices[0].delta.content: 446 | if reasoning_started: 447 | console.print("\n") # Add spacing after reasoning 448 | console.print("\nAssistant> ", style="bold blue", end="") 449 | reasoning_started = False # Reset so we don't add extra spacing 450 | final_content += chunk.choices[0].delta.content 451 | console.print(chunk.choices[0].delta.content, end="") 452 | 453 | console.print() # New line after streaming 454 | 455 | try: 456 | parsed_response = json.loads(final_content) 457 | 458 | if "assistant_reply" not in parsed_response: 459 | parsed_response["assistant_reply"] = "" 460 | 461 | if "files_to_edit" in parsed_response and parsed_response["files_to_edit"]: 462 | new_files_to_edit = [] 463 | for edit in parsed_response["files_to_edit"]: 464 | try: 465 | edit_abs_path = normalize_path(edit["path"]) 466 | if edit_abs_path in valid_files or ensure_file_in_context(edit_abs_path): 467 | edit["path"] = edit_abs_path 468 | new_files_to_edit.append(edit) 469 | except (OSError, ValueError): 470 | console.print(f"[yellow]⚠[/yellow] Skipping invalid path: '{edit['path']}'", style="yellow") 471 | continue 472 | parsed_response["files_to_edit"] = new_files_to_edit 473 | 474 | response_obj = AssistantResponse(**parsed_response) 475 | 476 | # Store the complete JSON response in conversation history 477 | conversation_history.append({ 478 | "role": "assistant", 479 | "content": final_content # Store the full JSON response string 480 | }) 481 | 482 | return response_obj 483 | 484 | except json.JSONDecodeError: 485 | error_msg = "Failed to parse JSON response from assistant" 486 | console.print(f"[red]✗[/red] {error_msg}", style="red") 487 | return AssistantResponse( 488 | assistant_reply=error_msg, 489 | files_to_create=[] 490 | ) 491 | 492 | except Exception as e: 493 | error_msg = f"DeepSeek API error: {str(e)}" 494 | console.print(f"\n[red]✗[/red] {error_msg}", style="red") 495 | return AssistantResponse( 496 | assistant_reply=error_msg, 497 | files_to_create=[] 498 | ) 499 | 500 | def trim_conversation_history(): 501 | """Trim conversation history to prevent token limit issues""" 502 | max_pairs = 10 # Adjust based on your needs 503 | system_msgs = [msg for msg in conversation_history if msg["role"] == "system"] 504 | other_msgs = [msg for msg in conversation_history if msg["role"] != "system"] 505 | 506 | # Keep only the last max_pairs of user-assistant interactions 507 | if len(other_msgs) > max_pairs * 2: 508 | other_msgs = other_msgs[-max_pairs * 2:] 509 | 510 | conversation_history.clear() 511 | conversation_history.extend(system_msgs + other_msgs) 512 | 513 | # -------------------------------------------------------------------------------- 514 | # 7. Main interactive loop 515 | # -------------------------------------------------------------------------------- 516 | 517 | def main(): 518 | console.print(Panel.fit( 519 | "[bold blue]Welcome to Deep Seek Engineer with Structured Output[/bold blue] [green](and CoT reasoning)[/green]!🐋", 520 | border_style="blue" 521 | )) 522 | console.print( 523 | "Use '[bold magenta]/add[/bold magenta]' to include files in the conversation:\n" 524 | " • '[bold magenta]/add path/to/file[/bold magenta]' for a single file\n" 525 | " • '[bold magenta]/add path/to/folder[/bold magenta]' for all files in a folder\n" 526 | " • You can add multiple files one by one using /add for each file\n" 527 | "Type '[bold red]exit[/bold red]' or '[bold red]quit[/bold red]' to end.\n" 528 | ) 529 | 530 | while True: 531 | try: 532 | user_input = prompt_session.prompt("You> ").strip() 533 | except (EOFError, KeyboardInterrupt): 534 | console.print("\n[yellow]Exiting.[/yellow]") 535 | break 536 | 537 | if not user_input: 538 | continue 539 | 540 | if user_input.lower() in ["exit", "quit"]: 541 | console.print("[yellow]Goodbye![/yellow]") 542 | break 543 | 544 | if try_handle_add_command(user_input): 545 | continue 546 | 547 | response_data = stream_openai_response(user_input) 548 | 549 | if response_data.files_to_create: 550 | for file_info in response_data.files_to_create: 551 | create_file(file_info.path, file_info.content) 552 | 553 | if response_data.files_to_edit: 554 | show_diff_table(response_data.files_to_edit) 555 | confirm = prompt_session.prompt( 556 | "Do you want to apply these changes? (y/n): " 557 | ).strip().lower() 558 | if confirm == 'y': 559 | for edit_info in response_data.files_to_edit: 560 | apply_diff_edit(edit_info.path, edit_info.original_snippet, edit_info.new_snippet) 561 | else: 562 | console.print("[yellow]ℹ[/yellow] Skipped applying diff edits.", style="yellow") 563 | 564 | console.print("[blue]Session finished.[/blue]") 565 | 566 | if __name__ == "__main__": 567 | main() --------------------------------------------------------------------------------