├── .github ├── dependabot.yml └── workflows │ ├── build.yaml │ ├── codeql.yml │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── cmd └── kube-copilot │ ├── analyze.go │ ├── audit.go │ ├── diagnose.go │ ├── execute.go │ ├── generate.go │ ├── main.go │ └── version.go ├── examples └── mcp-config.json ├── go.mod ├── go.sum └── pkg ├── assistants └── simple.go ├── kubernetes ├── apply.go └── get.go ├── llms ├── openai.go ├── tokens.go └── tokens_test.go ├── tools ├── googlesearch.go ├── kubectl.go ├── mcp.go ├── python.go ├── python_test.go ├── tool.go └── trivy.go ├── utils ├── term.go └── yaml.go └── workflows ├── analyze.go ├── audit.go ├── generate.go ├── prompts.go ├── reactflow.go ├── simpleflow.go └── swarm.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | labels: 13 | - "bot" 14 | open-pull-requests-limit: 5 15 | 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: daily 20 | open-pull-requests-limit: 5 21 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "master" 8 | - "main" 9 | 10 | env: 11 | REGISTRY: ghcr.io 12 | 13 | jobs: 14 | build-push-image: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Set up QEMU 22 | uses: docker/setup-qemu-action@v3 23 | - name: Set up Docker Buildx 24 | uses: docker/setup-buildx-action@v3 25 | - name: Log in to the Container registry 26 | uses: docker/login-action@v3 27 | with: 28 | registry: ${{ env.REGISTRY }} 29 | username: ${{ github.repository_owner }} 30 | password: ${{ secrets.GITHUB_TOKEN }} 31 | - name: Build Docker image 32 | run: | 33 | version=$(git describe --tags --abbrev) 34 | docker build -t ${{ env.REGISTRY }}/${{ github.repository_owner }}/kube-copilot:${version} . 35 | docker push ${{ env.REGISTRY }}/${{ github.repository_owner }}/kube-copilot:${version} 36 | if [ ${GITHUB_REF_NAME} = "master" ]; then 37 | docker tag ${{ env.REGISTRY }}/${{ github.repository_owner }}/kube-copilot:${version} ${{ env.REGISTRY }}/${{ github.repository_owner }}/kube-copilot:latest 38 | docker push ${{ env.REGISTRY }}/${{ github.repository_owner }}/kube-copilot:latest 39 | fi 40 | if [ ${GITHUB_REF_NAME} = "main" ]; then 41 | docker tag ${{ env.REGISTRY }}/${{ github.repository_owner }}/kube-copilot:${version} ${{ env.REGISTRY }}/${{ github.repository_owner }}/kube-copilot:py 42 | docker push ${{ env.REGISTRY }}/${{ github.repository_owner }}/kube-copilot:py 43 | fi 44 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | name: "CodeQL" 8 | 9 | on: 10 | push: 11 | branches: [ "master" ] 12 | pull_request: 13 | branches: [ "master"] 14 | 15 | jobs: 16 | analyze: 17 | name: Analyze (${{ matrix.language }}) 18 | # Runner size impacts CodeQL analysis time. To learn more, please see: 19 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 20 | # - https://gh.io/supported-runners-and-hardware-resources 21 | # - https://gh.io/using-larger-runners (GitHub.com only) 22 | # Consider using larger runners or machines with greater resources for possible analysis time improvements. 23 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 24 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 25 | permissions: 26 | # required for all workflows 27 | security-events: write 28 | 29 | # required to fetch internal or private CodeQL packs 30 | packages: read 31 | 32 | # only required for workflows in private repositories 33 | actions: read 34 | contents: read 35 | 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | include: 40 | - language: go 41 | build-mode: autobuild 42 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' 43 | # Use `c-cpp` to analyze code written in C, C++ or both 44 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both 45 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 46 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, 47 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. 48 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how 49 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages 50 | steps: 51 | - name: Checkout repository 52 | uses: actions/checkout@v4 53 | 54 | # Initializes the CodeQL tools for scanning. 55 | - name: Initialize CodeQL 56 | uses: github/codeql-action/init@v3 57 | with: 58 | languages: ${{ matrix.language }} 59 | build-mode: ${{ matrix.build-mode }} 60 | # If you wish to specify custom queries, you can do so here or in a config file. 61 | # By default, queries listed here will override any specified in a config file. 62 | # Prefix the list here with "+" to use these queries and those in the config file. 63 | 64 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 65 | # queries: security-extended,security-and-quality 66 | 67 | # If the analyze step fails for one of the languages you are analyzing with 68 | # "We were unable to automatically build your code", modify the matrix above 69 | # to set the build mode to "manual" for that language. Then modify this step 70 | # to build your code. 71 | # ℹ️ Command-line programs to run using the OS shell. 72 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 73 | - if: matrix.build-mode == 'manual' 74 | run: | 75 | echo 'If you are using a "manual" build mode for one or more of the' \ 76 | 'languages you are analyzing, replace this with the commands to build' \ 77 | 'your code, for example:' 78 | echo ' make bootstrap' 79 | echo ' make release' 80 | exit 1 81 | 82 | - name: Perform CodeQL Analysis 83 | uses: github/codeql-action/analyze@v3 84 | with: 85 | category: "/language:${{matrix.language}}" 86 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | 11 | jobs: 12 | build-push-image: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v3 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v3 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@v3 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.repository_owner }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | - name: Build Docker image 30 | run: | 31 | docker build -t ${{ env.REGISTRY }}/${{ github.repository_owner }}/kube-copilot:${{ github.ref_name }} . 32 | docker push ${{ env.REGISTRY }}/${{ github.repository_owner }}/kube-copilot:${{ github.ref_name }} 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - "master" 8 | 9 | jobs: 10 | build: 11 | name: Test 12 | runs-on: ubuntu-latest 13 | steps: 14 | 15 | - name: Set up Go 1.x 16 | uses: actions/setup-go@v5 17 | 18 | - name: Check out code into the Go module directory 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Test 24 | run: go test -v ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | .DS_Store 6 | 7 | # Helm files 8 | .cr-index 9 | .cr-release-packages 10 | index.yaml 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 16 | *.o 17 | *.a 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # Folders 42 | _obj 43 | _test 44 | 45 | # Architecture specific extensions/prefixes 46 | *.[568vq] 47 | [568vq].out 48 | 49 | *.cgo1.go 50 | *.cgo2.c 51 | _cgo_defun.c 52 | _cgo_gotypes.go 53 | _cgo_export.* 54 | 55 | _testmain.go 56 | 57 | *.exe 58 | *.test 59 | *.prof 60 | _out 61 | 62 | # PyInstaller 63 | # Usually these files are written by a python script from a template 64 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 65 | *.manifest 66 | *.spec 67 | 68 | # Installer logs 69 | pip-log.txt 70 | pip-delete-this-directory.txt 71 | 72 | # Unit test / coverage reports 73 | htmlcov/ 74 | .tox/ 75 | .nox/ 76 | .coverage 77 | .coverage.* 78 | .cache 79 | nosetests.xml 80 | coverage.xml 81 | *.cover 82 | *.py,cover 83 | .hypothesis/ 84 | .pytest_cache/ 85 | 86 | # Translations 87 | *.mo 88 | *.pot 89 | 90 | # Django stuff: 91 | *.log 92 | local_settings.py 93 | db.sqlite3 94 | db.sqlite3-journal 95 | 96 | # Flask stuff: 97 | instance/ 98 | .webassets-cache 99 | 100 | # Scrapy stuff: 101 | .scrapy 102 | 103 | # Sphinx documentation 104 | docs/_build/ 105 | 106 | # PyBuilder 107 | target/ 108 | 109 | # Jupyter Notebook 110 | .ipynb_checkpoints 111 | 112 | # IPython 113 | profile_default/ 114 | ipython_config.py 115 | 116 | # pyenv 117 | .python-version 118 | 119 | # pipenv 120 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 121 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 122 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 123 | # install all needed dependencies. 124 | #Pipfile.lock 125 | 126 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 127 | __pypackages__/ 128 | 129 | # Celery stuff 130 | celerybeat-schedule 131 | celerybeat.pid 132 | 133 | # SageMath parsed files 134 | *.sage.py 135 | 136 | # Environments 137 | .vscode 138 | .env 139 | .venv 140 | env/ 141 | venv/ 142 | ENV/ 143 | env.bak/ 144 | venv.bak/ 145 | 146 | # Spyder project settings 147 | .spyderproject 148 | .spyproject 149 | 150 | # Rope project settings 151 | .ropeproject 152 | 153 | # mkdocs documentation 154 | /site 155 | 156 | # mypy 157 | .mypy_cache/ 158 | .dmypy.json 159 | dmypy.json 160 | 161 | # Pyre type checker 162 | .pyre/ 163 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2023 - Present, Pengfei Ni 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # Build stage 16 | FROM golang:alpine AS builder 17 | ADD . /go/src/github.com/feiskyer/kube-copilot 18 | RUN cd /go/src/github.com/feiskyer/kube-copilot && \ 19 | apk update && apk add --no-cache gcc musl-dev openssl && \ 20 | CGO_ENABLED=0 go build -o _out/kube-copilot ./cmd/kube-copilot 21 | 22 | # Final image 23 | FROM alpine 24 | # EXPOSE 80 25 | WORKDIR / 26 | 27 | RUN apk add --update curl wget python3 py3-pip curl && \ 28 | pip install --break-system-packages kubernetes && \ 29 | curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \ 30 | chmod +x kubectl && mv kubectl /usr/local/bin && \ 31 | curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin v0.60.0 && \ 32 | rm -rf /var/cache/apk/* && \ 33 | mkdir -p /etc/kube-copilot 34 | 35 | COPY --from=builder /go/src/github.com/feiskyer/kube-copilot/_out/kube-copilot /usr/local/bin/ 36 | 37 | USER copilot 38 | ENTRYPOINT [ "/usr/local/bin/kube-copilot" ] 39 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes Copilot 2 | 3 | Kubernetes Copilot powered by LLM, which leverages advanced language models to streamline and enhance Kubernetes cluster management. This tool integrates seamlessly with your existing Kubernetes setup, providing intelligent automation, diagnostics, and manifest generation capabilities. By utilizing the power of AI, Kubernetes Copilot simplifies complex operations and helps maintain the health and security of your Kubernetes workloads. 4 | 5 | ## Features 6 | 7 | - Automate Kubernetes cluster operations using large language models. 8 | - Provide your own OpenAI, Azure OpenAI, Anthropic Claude, Google Gemini or other OpenAI-compatible LLM providers. 9 | - Diagnose and analyze potential issues for Kubernetes workloads. 10 | - Generate Kubernetes manifests based on provided prompt instructions. 11 | - Utilize native `kubectl` and `trivy` commands for Kubernetes cluster access and security vulnerability scanning. 12 | - Support for Model Context Protocol (MCP) protocol to integrate with external tools. 13 | 14 | ## Installation 15 | 16 | Install the kube-copilot CLI with the following command: 17 | 18 | ```sh 19 | go install github.com/feiskyer/kube-copilot/cmd/kube-copilot@latest 20 | ``` 21 | 22 | ## Quick Start 23 | 24 | Setup the following environment variables: 25 | 26 | - Ensure [`kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/) is installed on the local machine and the kubeconfig file is configured for Kubernetes cluster access. 27 | - Install [`trivy`](https://github.com/aquasecurity/trivy) to assess container image security issues (only required for the `audit` command). 28 | - Set the OpenAI [API key](https://platform.openai.com/account/api-keys) as the `OPENAI_API_KEY` environment variable to enable LLM AI functionality (refer below for other LLM providers). 29 | 30 | Then run the following commands directly in the terminal: 31 | 32 | ```sh 33 | Kubernetes Copilot powered by OpenAI 34 | 35 | Usage: 36 | kube-copilot [command] 37 | 38 | Available Commands: 39 | analyze Analyze issues for a given resource 40 | audit Audit security issues for a Pod 41 | completion Generate the autocompletion script for the specified shell 42 | diagnose Diagnose problems for a Pod 43 | execute Execute operations based on prompt instructions 44 | generate Generate Kubernetes manifests 45 | help Help about any command 46 | version Print the version of kube-copilot 47 | 48 | Flags: 49 | -c, --count-tokens Print tokens count 50 | -h, --help help for kube-copilot 51 | -x, --max-iterations int Max iterations for the agent running (default 30) 52 | -t, --max-tokens int Max tokens for the GPT model (default 2048) 53 | -m, --model string OpenAI model to use (default "gpt-4o") 54 | -p, --mcp-config string MCP configuration file 55 | -v, --verbose Enable verbose output 56 | --version version for kube-copilot 57 | 58 | Use "kube-copilot [command] --help" for more information about a command. 59 | ``` 60 | 61 | ## LLM Integrations 62 | 63 |
64 | OpenAI 65 | 66 | Set the OpenAI [API key](https://platform.openai.com/account/api-keys) as the `OPENAI_API_KEY` environment variable to enable OpenAI functionality. 67 |
68 | 69 |
70 | 71 |
72 | Anthropic Claude 73 | 74 | Anthropic Claude provides an [OpenAI compatible API](https://docs.anthropic.com/en/api/openai-sdk), so it could be used by using following config: 75 | 76 | - `OPENAI_API_KEY=` 77 | - `OPENAI_API_BASE='https://api.anthropic.com/v1/'` 78 | 79 |
80 | 81 | Azure OpenAI 82 | 83 | For [Azure OpenAI service](https://learn.microsoft.com/en-us/azure/cognitive-services/openai/quickstart?tabs=command-line&pivots=rest-api#retrieve-key-and-endpoint), set the following environment variables: 84 | 85 | - `AZURE_OPENAI_API_KEY=` 86 | - `AZURE_OPENAI_API_BASE=https://.openai.azure.com/` 87 | - `AZURE_OPENAI_API_VERSION=2025-03-01-preview` 88 | 89 |
90 | 91 |
92 | Google Gemini 93 | 94 | Google Gemini provides an OpenAI compatible API, so it could be used by using following config: 95 | 96 | - `OPENAI_API_KEY=` 97 | - `OPENAI_API_BASE='https://generativelanguage.googleapis.com/v1beta/openai/'` 98 | 99 |
100 | 101 |
102 | Ollama or other OpenAI compatible LLMs 103 | 104 | For Ollama or other OpenAI compatible LLMs, set the following environment variables: 105 | 106 | - `OPENAI_API_KEY=` 107 | - `OPENAI_API_BASE='http://localhost:11434/v1'` (or your own base URL) 108 | 109 |
110 | 111 | ## Key Features 112 | 113 |
114 | Analyze issues for a given kubernetes resource 115 | 116 | `kube-copilot analyze [--resource pod] --name [--namespace ]` will analyze potential issues for the given resource object: 117 | 118 | ```sh 119 | Analyze issues for a given resource 120 | 121 | Usage: 122 | kube-copilot analyze [flags] 123 | 124 | Flags: 125 | -h, --help help for analyze 126 | -n, --name string Resource name 127 | -s, --namespace string Resource namespace (default "default") 128 | -r, --resource string Resource type (default "pod") 129 | 130 | Global Flags: 131 | -c, --count-tokens Print tokens count 132 | -x, --max-iterations int Max iterations for the agent running (default 10) 133 | -t, --max-tokens int Max tokens for the GPT model (default 2048) 134 | -m, --model string OpenAI model to use (default "gpt-4o") 135 | -v, --verbose Enable verbose output 136 | ``` 137 | 138 |
139 | 140 |
141 | Audit Security Issues for Pod 142 | 143 | `kube-copilot audit --name [--namespace ]` will audit security issues for a Pod: 144 | 145 | ```sh 146 | Audit security issues for a Pod 147 | 148 | Usage: 149 | kube-copilot audit [flags] 150 | 151 | Flags: 152 | -h, --help help for audit 153 | -n, --name string Resource name 154 | -s, --namespace string Resource namespace (default "default") 155 | 156 | Global Flags: 157 | -c, --count-tokens Print tokens count 158 | -x, --max-iterations int Max iterations for the agent running (default 10) 159 | -t, --max-tokens int Max tokens for the GPT model (default 2048) 160 | -m, --model string OpenAI model to use (default "gpt-4o") 161 | -v, --verbose Enable verbose output 162 | ``` 163 | 164 |
165 | 166 |
167 | Diagnose Problems for Pod 168 | 169 | `kube-copilot diagnose --name [--namespace ]` will diagnose problems for a Pod: 170 | 171 | ```sh 172 | Diagnose problems for a Pod 173 | 174 | Usage: 175 | kube-copilot diagnose [flags] 176 | 177 | Flags: 178 | -h, --help help for diagnose 179 | -n, --name string Resource name 180 | -s, --namespace string Resource namespace (default "default") 181 | -p, --mcp-config string MCP configuration file 182 | 183 | Global Flags: 184 | -c, --count-tokens Print tokens count 185 | -x, --max-iterations int Max iterations for the agent running (default 10) 186 | -t, --max-tokens int Max tokens for the GPT model (default 2048) 187 | -m, --model string OpenAI model to use (default "gpt-4o") 188 | -v, --verbose Enable verbose output 189 | ``` 190 | 191 |
192 | 193 |
194 | Execute operations based on prompt instructions 195 | 196 | `kube-copilot execute --instructions ` will execute operations based on prompt instructions. 197 | It could also be used to ask any questions. 198 | 199 | ```sh 200 | Execute operations based on prompt instructions 201 | 202 | Usage: 203 | kube-copilot execute [flags] 204 | 205 | Flags: 206 | -h, --help help for execute 207 | -i, --instructions string instructions to execute 208 | -p, --mcp-config string MCP configuration file 209 | 210 | Global Flags: 211 | -c, --count-tokens Print tokens count 212 | -x, --max-iterations int Max iterations for the agent running (default 10) 213 | -t, --max-tokens int Max tokens for the GPT model (default 2048) 214 | -m, --model string OpenAI model to use (default "gpt-4o") 215 | -v, --verbose Enable verbose output 216 | ``` 217 | 218 |
219 | 220 |
221 | Generate Kubernetes Manifests 222 | 223 | Use the `kube-copilot generate --prompt ` command to create Kubernetes manifests based on 224 | the provided prompt instructions. After generating the manifests, you will be 225 | prompted to confirm whether you want to apply them. 226 | 227 | ```sh 228 | Generate Kubernetes manifests 229 | 230 | Usage: 231 | kube-copilot generate [flags] 232 | 233 | Flags: 234 | -h, --help help for generate 235 | -p, --prompt string Prompts to generate Kubernetes manifests 236 | 237 | Global Flags: 238 | -c, --count-tokens Print tokens count 239 | -x, --max-iterations int Max iterations for the agent running (default 10) 240 | -t, --max-tokens int Max tokens for the GPT model (default 2048) 241 | -m, --model string OpenAI model to use (default "gpt-4o") 242 | -v, --verbose Enable verbose output 243 | ``` 244 | 245 |
246 | 247 |
248 | Leverage Model Context Protocol (MCP) 249 | 250 | Kube-copilot integrates with external tools for issue diagnosis and instruction execution (via the diagnose and execute subcommands) using the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/). 251 | 252 | To use MCP tools: 253 | 254 | 1. Create a JSON configuration file for your MCP servers: 255 | 256 | ```json 257 | { 258 | "mcpServers": { 259 | "sequential-thinking": { 260 | "command": "npx", 261 | "args": [ 262 | "-y", 263 | "@modelcontextprotocol/server-sequential-thinking" 264 | ] 265 | }, 266 | "kubernetes": { 267 | "command": "uvx", 268 | "args": [ 269 | "mcp-kubernetes-server" 270 | ] 271 | } 272 | } 273 | } 274 | ``` 275 | 276 | 2. Run kube-copilot with the `--mcp-config` flag: 277 | 278 | ```sh 279 | kube-copilot execute --instructions "Your instructions" --mcp-config path/to/mcp-config.json 280 | ``` 281 | 282 | The MCP tools will be automatically discovered and made available to the LLM. 283 | 284 |
285 | 286 | ## Python Version 287 | 288 | Please refer [feiskyer/kube-copilot-python](https://github.com/feiskyer/kube-copilot-python) for the Python implementation of the same project. 289 | 290 | ## Contribution 291 | 292 | The project is opensource at github [feiskyer/kube-copilot](https://github.com/feiskyer/kube-copilot) (Go) and [feiskyer/kube-copilot-python](https://github.com/feiskyer/kube-copilot-python) (Python) with Apache License. 293 | 294 | If you would like to contribute to the project, please follow these guidelines: 295 | 296 | 1. Fork the repository and clone it to your local machine. 297 | 2. Create a new branch for your changes. 298 | 3. Make your changes and commit them with a descriptive commit message. 299 | 4. Push your changes to your forked repository. 300 | 5. Open a pull request to the main repository. 301 | -------------------------------------------------------------------------------- /cmd/kube-copilot/analyze.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/fatih/color" 22 | "github.com/feiskyer/kube-copilot/pkg/kubernetes" 23 | "github.com/feiskyer/kube-copilot/pkg/utils" 24 | "github.com/feiskyer/kube-copilot/pkg/workflows" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | var analysisName string 29 | var analysisNamespace string 30 | var analysisResource string 31 | 32 | func init() { 33 | analyzeCmd.PersistentFlags().StringVarP(&analysisName, "name", "n", "", "Resource name") 34 | analyzeCmd.PersistentFlags().StringVarP(&analysisNamespace, "namespace", "s", "default", "Resource namespace") 35 | analyzeCmd.PersistentFlags().StringVarP(&analysisResource, "resource", "r", "pod", "Resource type") 36 | analyzeCmd.MarkFlagRequired("name") 37 | } 38 | 39 | var analyzeCmd = &cobra.Command{ 40 | Use: "analyze", 41 | Short: "Analyze issues for a given resource", 42 | Run: func(cmd *cobra.Command, args []string) { 43 | if analysisName == "" && len(args) > 0 { 44 | analysisName = args[0] 45 | } 46 | if analysisName == "" { 47 | fmt.Println("Please provide a resource name") 48 | return 49 | } 50 | 51 | fmt.Printf("Analysing %s %s/%s\n", analysisResource, analysisNamespace, analysisName) 52 | 53 | manifests, err := kubernetes.GetYaml(analysisResource, analysisName, analysisNamespace) 54 | if err != nil { 55 | color.Red(err.Error()) 56 | return 57 | } 58 | 59 | response, err := workflows.AnalysisFlow(model, manifests, verbose) 60 | if err != nil { 61 | color.Red(err.Error()) 62 | return 63 | } 64 | 65 | utils.RenderMarkdown(response) 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /cmd/kube-copilot/audit.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/fatih/color" 22 | "github.com/feiskyer/kube-copilot/pkg/utils" 23 | "github.com/feiskyer/kube-copilot/pkg/workflows" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | var ( 28 | auditName string 29 | auditNamespace string 30 | ) 31 | 32 | func init() { 33 | auditCmd.PersistentFlags().StringVarP(&auditName, "name", "n", "", "Pod name") 34 | auditCmd.PersistentFlags().StringVarP(&auditNamespace, "namespace", "s", "default", "Pod namespace") 35 | auditCmd.MarkFlagRequired("name") 36 | } 37 | 38 | var auditCmd = &cobra.Command{ 39 | Use: "audit", 40 | Short: "Audit security issues for a Pod", 41 | Run: func(cmd *cobra.Command, args []string) { 42 | if auditName == "" && len(args) > 0 { 43 | auditName = args[0] 44 | } 45 | if auditName == "" { 46 | fmt.Println("Please provide a pod name") 47 | return 48 | } 49 | 50 | fmt.Printf("Auditing Pod %s/%s\n", auditNamespace, auditName) 51 | response, err := workflows.AuditFlow(model, auditNamespace, auditName, verbose) 52 | if err != nil { 53 | color.Red(err.Error()) 54 | return 55 | } 56 | 57 | utils.RenderMarkdown(response) 58 | }, 59 | } 60 | -------------------------------------------------------------------------------- /cmd/kube-copilot/diagnose.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/fatih/color" 22 | "github.com/feiskyer/kube-copilot/pkg/tools" 23 | "github.com/feiskyer/kube-copilot/pkg/workflows" 24 | "github.com/spf13/cobra" 25 | ) 26 | 27 | var ( 28 | diagnoseName string 29 | diagnoseNamespace string 30 | dignoseMCPFile string 31 | diagnoseDisableKubectl bool 32 | ) 33 | 34 | func init() { 35 | diagnoseCmd.PersistentFlags().StringVarP(&diagnoseName, "name", "n", "", "Pod name") 36 | diagnoseCmd.PersistentFlags().StringVarP(&diagnoseNamespace, "namespace", "s", "default", "Pod namespace") 37 | diagnoseCmd.PersistentFlags().StringVarP(&dignoseMCPFile, "mcp-config", "p", "", "MCP configuration file") 38 | diagnoseCmd.PersistentFlags().BoolVarP(&diagnoseDisableKubectl, "disable-kubectl", "d", false, "Disable kubectl tool (useful when using MCP server)") 39 | 40 | diagnoseCmd.MarkFlagRequired("name") 41 | } 42 | 43 | var diagnoseCmd = &cobra.Command{ 44 | Use: "diagnose", 45 | Short: "Diagnose problems for a Pod", 46 | Run: func(cmd *cobra.Command, args []string) { 47 | if diagnoseName == "" && len(args) > 0 { 48 | diagnoseName = args[0] 49 | } 50 | if diagnoseName == "" { 51 | fmt.Println("Please provide a pod name") 52 | return 53 | } 54 | 55 | fmt.Printf("Diagnosing Pod %s/%s\n", diagnoseNamespace, diagnoseName) 56 | 57 | clients, err := tools.InitTools(dignoseMCPFile, diagnoseDisableKubectl, verbose) 58 | if err != nil { 59 | color.Red("Failed to initialize tools: %v", err) 60 | return 61 | } 62 | 63 | defer func() { 64 | for _, client := range clients { 65 | go client.Close() 66 | } 67 | }() 68 | 69 | prompt := fmt.Sprintf("Diagnose the issues for Pod %s in namespace %s", diagnoseName, diagnoseNamespace) 70 | flow, err := workflows.NewReActFlow(model, prompt, tools.GetToolPrompt(), verbose, maxIterations) 71 | if err != nil { 72 | color.Red(err.Error()) 73 | return 74 | } 75 | 76 | response, err := flow.Run() 77 | if err != nil { 78 | color.Red(err.Error()) 79 | return 80 | } 81 | fmt.Println(response) 82 | }, 83 | } 84 | -------------------------------------------------------------------------------- /cmd/kube-copilot/execute.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "strings" 21 | 22 | "github.com/fatih/color" 23 | "github.com/feiskyer/kube-copilot/pkg/tools" 24 | "github.com/feiskyer/kube-copilot/pkg/workflows" 25 | "github.com/spf13/cobra" 26 | ) 27 | 28 | var ( 29 | executeInstructions string 30 | executeMCPFile string 31 | executeDisableKubectl bool 32 | ) 33 | 34 | func init() { 35 | executeCmd.PersistentFlags().StringVarP(&executeInstructions, "instructions", "i", "", "instructions to execute") 36 | executeCmd.PersistentFlags().StringVarP(&executeMCPFile, "mcp-config", "p", "", "MCP configuration file") 37 | executeCmd.PersistentFlags().BoolVarP(&executeDisableKubectl, "disable-kubectl", "d", false, "Disable kubectl tool (useful when using MCP server)") 38 | 39 | executeCmd.MarkFlagRequired("instructions") 40 | } 41 | 42 | var executeCmd = &cobra.Command{ 43 | Use: "execute", 44 | Short: "Execute operations based on prompt instructions", 45 | Run: func(cmd *cobra.Command, args []string) { 46 | if executeInstructions == "" && len(args) > 0 { 47 | executeInstructions = strings.Join(args, " ") 48 | } 49 | if executeInstructions == "" { 50 | fmt.Println("Please provide the instructions") 51 | return 52 | } 53 | 54 | clients, err := tools.InitTools(executeMCPFile, executeDisableKubectl, verbose) 55 | if err != nil { 56 | color.Red("Failed to initialize tools: %v", err) 57 | return 58 | } 59 | 60 | defer func() { 61 | for _, client := range clients { 62 | go client.Close() 63 | } 64 | }() 65 | 66 | flow, err := workflows.NewReActFlow(model, executeInstructions, tools.GetToolPrompt(), verbose, maxIterations) 67 | if err != nil { 68 | color.Red(err.Error()) 69 | return 70 | } 71 | 72 | response, err := flow.Run() 73 | if err != nil { 74 | color.Red(err.Error()) 75 | return 76 | } 77 | fmt.Println(response) 78 | }, 79 | } 80 | -------------------------------------------------------------------------------- /cmd/kube-copilot/generate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "bufio" 20 | "fmt" 21 | "os" 22 | "strings" 23 | 24 | "github.com/fatih/color" 25 | "github.com/feiskyer/kube-copilot/pkg/kubernetes" 26 | "github.com/feiskyer/kube-copilot/pkg/utils" 27 | "github.com/feiskyer/kube-copilot/pkg/workflows" 28 | "github.com/spf13/cobra" 29 | ) 30 | 31 | var generatePrompt string 32 | 33 | func init() { 34 | generateCmd.PersistentFlags().StringVarP(&generatePrompt, "prompt", "p", "", "Prompts to generate Kubernetes manifests") 35 | generateCmd.MarkFlagRequired("prompt") 36 | } 37 | 38 | var generateCmd = &cobra.Command{ 39 | Use: "generate", 40 | Short: "Generate Kubernetes manifests", 41 | Run: func(cmd *cobra.Command, args []string) { 42 | if generatePrompt == "" { 43 | color.Red("Please specify a prompt") 44 | return 45 | } 46 | 47 | response, err := workflows.GeneratorFlow(model, generatePrompt, verbose) 48 | if err != nil { 49 | color.Red(err.Error()) 50 | return 51 | } 52 | 53 | // Extract the yaml from the response 54 | yaml := response 55 | if strings.Contains(response, "```") { 56 | yaml = utils.ExtractYaml(response) 57 | } 58 | fmt.Printf("\nGenerated manifests:\n\n") 59 | color.New(color.FgGreen).Printf("%s\n\n", yaml) 60 | 61 | // apply the yaml to kubernetes cluster 62 | color.New(color.FgRed).Printf("Do you approve to apply the generated manifests to cluster? (y/n)") 63 | scanner := bufio.NewScanner(os.Stdin) 64 | for scanner.Scan() { 65 | approve := scanner.Text() 66 | if strings.ToLower(approve) != "y" && strings.ToLower(approve) != "yes" { 67 | break 68 | } 69 | 70 | if err := kubernetes.ApplyYaml(yaml); err != nil { 71 | color.Red(err.Error()) 72 | return 73 | } 74 | 75 | color.New(color.FgGreen).Printf("Applied the generated manifests to cluster successfully!") 76 | break 77 | } 78 | }, 79 | } 80 | -------------------------------------------------------------------------------- /cmd/kube-copilot/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | var ( 25 | // global flags 26 | model string 27 | maxTokens int 28 | countTokens bool 29 | verbose bool 30 | maxIterations int 31 | 32 | // rootCmd represents the base command when called without any subcommands 33 | rootCmd = &cobra.Command{ 34 | Use: "kube-copilot", 35 | Version: VERSION, 36 | Short: "Kubernetes Copilot powered by AI", 37 | } 38 | ) 39 | 40 | // init initializes the command line flags 41 | func init() { 42 | rootCmd.PersistentFlags().StringVarP(&model, "model", "m", "gpt-4o", "AI model to use") 43 | rootCmd.PersistentFlags().IntVarP(&maxTokens, "max-tokens", "t", 2048, "Max tokens for the AI model") 44 | rootCmd.PersistentFlags().BoolVarP(&countTokens, "count-tokens", "c", false, "Print tokens count") 45 | rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") 46 | rootCmd.PersistentFlags().IntVarP(&maxIterations, "max-iterations", "x", 30, "Max iterations for the agent running") 47 | 48 | rootCmd.AddCommand(analyzeCmd) 49 | rootCmd.AddCommand(auditCmd) 50 | rootCmd.AddCommand(diagnoseCmd) 51 | rootCmd.AddCommand(generateCmd) 52 | rootCmd.AddCommand(executeCmd) 53 | rootCmd.AddCommand(versionCmd) 54 | } 55 | 56 | func main() { 57 | if err := rootCmd.Execute(); err != nil { 58 | fmt.Println(err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cmd/kube-copilot/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | const ( 25 | // VERSION is the version of kube-copilot. 26 | VERSION = "v0.6.10" 27 | ) 28 | 29 | var versionCmd = &cobra.Command{ 30 | Use: "version", 31 | Short: "Print the version of kube-copilot", 32 | Run: func(cmd *cobra.Command, args []string) { 33 | fmt.Printf("kube-copiolt %s\n", VERSION) 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /examples/mcp-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "azure": { 4 | "command": "npx", 5 | "args": [ 6 | "-y", 7 | "@azure/mcp@latest", 8 | "server", 9 | "start" 10 | ] 11 | }, 12 | "kubernetes": { 13 | "command": "uvx", 14 | "args": [ 15 | "mcp-kubernetes-server" 16 | ] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/feiskyer/kube-copilot 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/charmbracelet/glamour v0.10.0 9 | github.com/fatih/color v1.18.0 10 | github.com/feiskyer/swarm-go v0.2.3 11 | github.com/mark3labs/mcp-go v0.27.0 12 | github.com/pkoukk/tiktoken-go v0.1.7 13 | github.com/sashabaranov/go-openai v1.39.1 14 | github.com/spf13/cobra v1.9.1 15 | golang.org/x/term v0.32.0 16 | google.golang.org/api v0.232.0 17 | gopkg.in/yaml.v2 v2.4.0 18 | k8s.io/apimachinery v0.33.0 19 | k8s.io/client-go v0.33.0 20 | ) 21 | 22 | require ( 23 | cloud.google.com/go/auth v0.16.1 // indirect 24 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 25 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 26 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect 27 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect 28 | github.com/alecthomas/chroma/v2 v2.17.2 // indirect 29 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 30 | github.com/aymerick/douceur v0.2.0 // indirect 31 | github.com/charmbracelet/colorprofile v0.3.1 // indirect 32 | github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect 33 | github.com/charmbracelet/x/ansi v0.9.2 // indirect 34 | github.com/charmbracelet/x/cellbuf v0.0.13 // indirect 35 | github.com/charmbracelet/x/exp/slice v0.0.0-20250509021451-13796e822d86 // indirect 36 | github.com/charmbracelet/x/term v0.2.1 // indirect 37 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 38 | github.com/dlclark/regexp2 v1.11.5 // indirect 39 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 40 | github.com/felixge/httpsnoop v1.0.4 // indirect 41 | github.com/fxamacker/cbor/v2 v2.8.0 // indirect 42 | github.com/go-logr/logr v1.4.2 // indirect 43 | github.com/go-logr/stdr v1.2.2 // indirect 44 | github.com/go-openapi/jsonpointer v0.21.1 // indirect 45 | github.com/go-openapi/jsonreference v0.21.0 // indirect 46 | github.com/go-openapi/swag v0.23.1 // indirect 47 | github.com/gogo/protobuf v1.3.2 // indirect 48 | github.com/golang/protobuf v1.5.4 // indirect 49 | github.com/google/gnostic-models v0.6.9 // indirect 50 | github.com/google/go-cmp v0.7.0 // indirect 51 | github.com/google/gofuzz v1.2.0 // indirect 52 | github.com/google/s2a-go v0.1.9 // indirect 53 | github.com/google/uuid v1.6.0 // indirect 54 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 55 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 56 | github.com/gorilla/css v1.0.1 // indirect 57 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 58 | github.com/josharian/intern v1.0.0 // indirect 59 | github.com/json-iterator/go v1.1.12 // indirect 60 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 61 | github.com/mailru/easyjson v0.9.0 // indirect 62 | github.com/mattn/go-colorable v0.1.14 // indirect 63 | github.com/mattn/go-isatty v0.0.20 // indirect 64 | github.com/mattn/go-runewidth v0.0.16 // indirect 65 | github.com/microcosm-cc/bluemonday v1.0.27 // indirect 66 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 67 | github.com/modern-go/reflect2 v1.0.2 // indirect 68 | github.com/muesli/reflow v0.3.0 // indirect 69 | github.com/muesli/termenv v0.16.0 // indirect 70 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 71 | github.com/openai/openai-go v0.1.0-beta.10 // indirect 72 | github.com/pkg/errors v0.9.1 // indirect 73 | github.com/rivo/uniseg v0.4.7 // indirect 74 | github.com/spf13/cast v1.8.0 // indirect 75 | github.com/spf13/pflag v1.0.6 // indirect 76 | github.com/tidwall/gjson v1.18.0 // indirect 77 | github.com/tidwall/match v1.1.1 // indirect 78 | github.com/tidwall/pretty v1.2.1 // indirect 79 | github.com/tidwall/sjson v1.2.5 // indirect 80 | github.com/x448/float16 v0.8.4 // indirect 81 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 82 | github.com/yosida95/uritemplate/v3 v3.0.2 // indirect 83 | github.com/yuin/goldmark v1.7.11 // indirect 84 | github.com/yuin/goldmark-emoji v1.0.6 // indirect 85 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 86 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 87 | go.opentelemetry.io/otel v1.35.0 // indirect 88 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 89 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 90 | golang.org/x/crypto v0.38.0 // indirect 91 | golang.org/x/net v0.40.0 // indirect 92 | golang.org/x/oauth2 v0.30.0 // indirect 93 | golang.org/x/sync v0.14.0 // indirect 94 | golang.org/x/sys v0.33.0 // indirect 95 | golang.org/x/text v0.25.0 // indirect 96 | golang.org/x/time v0.11.0 // indirect 97 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 // indirect 98 | google.golang.org/grpc v1.72.0 // indirect 99 | google.golang.org/protobuf v1.36.6 // indirect 100 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 101 | gopkg.in/inf.v0 v0.9.1 // indirect 102 | gopkg.in/yaml.v3 v3.0.1 // indirect 103 | k8s.io/api v0.33.0 // indirect 104 | k8s.io/klog/v2 v2.130.1 // indirect 105 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 106 | k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 // indirect 107 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 108 | sigs.k8s.io/randfill v1.0.0 // indirect 109 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect 110 | sigs.k8s.io/yaml v1.4.0 // indirect 111 | ) 112 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= 2 | cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= 3 | cloud.google.com/go/auth v0.16.0 h1:Pd8P1s9WkcrBE2n/PhAwKsdrR35V3Sg2II9B+ndM3CU= 4 | cloud.google.com/go/auth v0.16.0/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= 5 | cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= 6 | cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= 7 | cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= 8 | cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= 9 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 10 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 11 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo= 12 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4= 13 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= 14 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= 15 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= 16 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg= 17 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= 18 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= 19 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= 20 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= 21 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= 22 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 23 | github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= 24 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 25 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 26 | github.com/alecthomas/chroma/v2 v2.15.0 h1:LxXTQHFoYrstG2nnV9y2X5O94sOBzf0CIUpSTbpxvMc= 27 | github.com/alecthomas/chroma/v2 v2.15.0/go.mod h1:gUhVLrPDXPtp/f+L1jo9xepo9gL4eLwRuGAunSZMkio= 28 | github.com/alecthomas/chroma/v2 v2.16.0 h1:QC5ZMizk67+HzxFDjQ4ASjni5kWBTGiigRG1u23IGvA= 29 | github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 30 | github.com/alecthomas/chroma/v2 v2.17.2 h1:Rm81SCZ2mPoH+Q8ZCc/9YvzPUN/E7HgPiPJD8SLV6GI= 31 | github.com/alecthomas/chroma/v2 v2.17.2/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 32 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 33 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 34 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= 35 | github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= 36 | github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 37 | github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 38 | github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= 39 | github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= 40 | github.com/charmbracelet/colorprofile v0.3.0 h1:KtLh9uuu1RCt+Hml4s6Hz+kB1PfV3wi++1h5ia65yKQ= 41 | github.com/charmbracelet/colorprofile v0.3.0/go.mod h1:oHJ340RS2nmG1zRGPmhJKJ/jf4FPNNk0P39/wBPA1G0= 42 | github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40= 43 | github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0= 44 | github.com/charmbracelet/glamour v0.9.1 h1:11dEfiGP8q1BEqvGoIjivuc2rBk+5qEXdPtaQ2WoiCM= 45 | github.com/charmbracelet/glamour v0.9.1/go.mod h1:+SHvIS8qnwhgTpVMiXwn7OfGomSqff1cHBCI8jLOetk= 46 | github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY= 47 | github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk= 48 | github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= 49 | github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= 50 | github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE= 51 | github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA= 52 | github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= 53 | github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= 54 | github.com/charmbracelet/x/ansi v0.9.2 h1:92AGsQmNTRMzuzHEYfCdjQeUzTrgE1vfO5/7fEVoXdY= 55 | github.com/charmbracelet/x/ansi v0.9.2/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= 56 | github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= 57 | github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= 58 | github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30= 59 | github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 60 | github.com/charmbracelet/x/exp/slice v0.0.0-20250420095733-3f4ae29caa07 h1:JvpXmP5IGAlMyy5uSachquncYSha4b6DiBCKw0hh1N8= 61 | github.com/charmbracelet/x/exp/slice v0.0.0-20250420095733-3f4ae29caa07/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU= 62 | github.com/charmbracelet/x/exp/slice v0.0.0-20250505150409-97991a1f17d1 h1:zth/BuC/A9Jcl0lODslgcaYT18oH0Ex6kNTiGqxG/7U= 63 | github.com/charmbracelet/x/exp/slice v0.0.0-20250505150409-97991a1f17d1/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms= 64 | github.com/charmbracelet/x/exp/slice v0.0.0-20250509021451-13796e822d86 h1:rPD20hp2xzbFR70KaFNEFSzOyOI4dnwqn7Xtxsf6YOM= 65 | github.com/charmbracelet/x/exp/slice v0.0.0-20250509021451-13796e822d86/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms= 66 | github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= 67 | github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= 68 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 69 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 70 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 71 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 72 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 73 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 74 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 75 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 76 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 77 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 78 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 79 | github.com/feiskyer/swarm-go v0.2.2 h1:Dzd0IWTUpfP5bt0Wil89GWeJQoUBXoqtsrLMPvB3Ss4= 80 | github.com/feiskyer/swarm-go v0.2.2/go.mod h1:tfFm89nsVxpjpW2v+nabmudxPV1ukAwA1Bv/j55sweg= 81 | github.com/feiskyer/swarm-go v0.2.3 h1:SpGH7C3e2Qbzm5sxMT/6lJ2jJHjiZ1KhhQKnwcit2fU= 82 | github.com/feiskyer/swarm-go v0.2.3/go.mod h1:eeKh5+7GvXS0oMqOnCINijtx2b0Pegb4Xh0JwnW54mg= 83 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 84 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 85 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 86 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 87 | github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU= 88 | github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 89 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 90 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 91 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 92 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 93 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 94 | github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= 95 | github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= 96 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 97 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 98 | github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 99 | github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 100 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 101 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 102 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 103 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 104 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 105 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 106 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 107 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 108 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 109 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 110 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 111 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 112 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 113 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 114 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 115 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 116 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 117 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 118 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 119 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 120 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 121 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 122 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= 123 | github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= 124 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 125 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 126 | github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= 127 | github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= 128 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 129 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 130 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 131 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 132 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 133 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 134 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 135 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 136 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 137 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 138 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 139 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 140 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 141 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 142 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 143 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 144 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 145 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 146 | github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 147 | github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= 148 | github.com/mark3labs/mcp-go v0.25.0 h1:UUpcMT3L5hIhuDy7aifj4Bphw4Pfx1Rf8mzMXDe8RQw= 149 | github.com/mark3labs/mcp-go v0.25.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= 150 | github.com/mark3labs/mcp-go v0.27.0 h1:iok9kU4DUIU2/XVLgFS2Q9biIDqstC0jY4EQTK2Erzc= 151 | github.com/mark3labs/mcp-go v0.27.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4= 152 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 153 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 154 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 155 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 156 | github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 157 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 158 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 159 | github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= 160 | github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= 161 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 162 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 163 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 164 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 165 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 166 | github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= 167 | github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= 168 | github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= 169 | github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= 170 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 171 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 172 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 173 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 174 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 175 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 176 | github.com/openai/openai-go v0.1.0-beta.2 h1:Ra5nCFkbEl9w+UJwAciC4kqnIBUCcJazhmMA0/YN894= 177 | github.com/openai/openai-go v0.1.0-beta.2/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= 178 | github.com/openai/openai-go v0.1.0-beta.3 h1:bbnQaLsLvqabuhNBbTLjz//Br59FHxJderqHd/4R4iM= 179 | github.com/openai/openai-go v0.1.0-beta.3/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= 180 | github.com/openai/openai-go v0.1.0-beta.10 h1:CknhGXe8aXQMRuqg255PFnWzgRY9nEryMxoNIBBM9tU= 181 | github.com/openai/openai-go v0.1.0-beta.10/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y= 182 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= 183 | github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= 184 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 185 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 186 | github.com/pkoukk/tiktoken-go v0.1.7 h1:qOBHXX4PHtvIvmOtyg1EeKlwFRiMKAcoMp4Q+bLQDmw= 187 | github.com/pkoukk/tiktoken-go v0.1.7/go.mod h1:9NiV+i9mJKGj1rYOT+njbv+ZwA/zJxYdewGl6qVatpg= 188 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 189 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 190 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 191 | github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 192 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 193 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 194 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 195 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 196 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 197 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 198 | github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8= 199 | github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 200 | github.com/sashabaranov/go-openai v1.38.2 h1:akrssjj+6DY3lWuDwHv6cBvJ8Z+FZDM9XEaaYFt0Auo= 201 | github.com/sashabaranov/go-openai v1.38.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 202 | github.com/sashabaranov/go-openai v1.39.1 h1:TMD4w77Iy9WTFlgnjNaxbAASdsCJ9R/rMdzL+SN14oU= 203 | github.com/sashabaranov/go-openai v1.39.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 204 | github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= 205 | github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 206 | github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= 207 | github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 208 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 209 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 210 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 211 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 212 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 213 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 214 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 215 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 216 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 217 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= 218 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 219 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 220 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 221 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 222 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 223 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 224 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 225 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 226 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 227 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 228 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 229 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 230 | github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= 231 | github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= 232 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 233 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 234 | github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 235 | github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= 236 | github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 237 | github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI= 238 | github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 239 | github.com/yuin/goldmark v1.7.11 h1:ZCxLyDMtz0nT2HFfsYG8WZ47Trip2+JyLysKcMYE5bo= 240 | github.com/yuin/goldmark v1.7.11/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= 241 | github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk= 242 | github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= 243 | github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs= 244 | github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA= 245 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 246 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 247 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= 248 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= 249 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 250 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 251 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 252 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 253 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 254 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 255 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 256 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 257 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 258 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 259 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 260 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 261 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 262 | golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= 263 | golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 264 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 265 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 266 | golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= 267 | golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= 268 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 269 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 270 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 271 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 272 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 273 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 274 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 275 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 276 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 277 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 278 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 279 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 280 | golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= 281 | golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= 282 | golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= 283 | golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 284 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 285 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 286 | golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= 287 | golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= 288 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 289 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 290 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 291 | golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= 292 | golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 293 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 294 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 295 | golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= 296 | golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 297 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 298 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 301 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 302 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 303 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 304 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 305 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 306 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 307 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 308 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 309 | golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= 310 | golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 311 | golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg= 312 | golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= 313 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 314 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 315 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 316 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 317 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 318 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 319 | golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= 320 | golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= 321 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 322 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 323 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 324 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 325 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 326 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 327 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 328 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 329 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 330 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 331 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 332 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 333 | google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs= 334 | google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4= 335 | google.golang.org/api v0.229.0 h1:p98ymMtqeJ5i3lIBMj5MpR9kzIIgzpHHh8vQ+vgAzx8= 336 | google.golang.org/api v0.229.0/go.mod h1:wyDfmq5g1wYJWn29O22FDWN48P7Xcz0xz+LBpptYvB0= 337 | google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= 338 | google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= 339 | google.golang.org/api v0.232.0 h1:qGnmaIMf7KcuwHOlF3mERVzChloDYwRfOJOrHt8YC3I= 340 | google.golang.org/api v0.232.0/go.mod h1:p9QCfBWZk1IJETUdbTKloR5ToFdKbYh2fkjsUL6vNoY= 341 | google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 h1:GVIKPyP/kLIyVOgOnTwFOrvQaQUzOzGMCxgFUOEmm24= 342 | google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422/go.mod h1:b6h1vNKhxaSoEI+5jc3PJUCustfli/mRab7295pY7rw= 343 | google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= 344 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g= 345 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 346 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e h1:ztQaXfzEXTmCBvbtWYRhJxW+0iJcz2qXfd38/e9l7bA= 347 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 348 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2 h1:IqsN8hx+lWLqlN+Sc3DoMy/watjofWiU8sRFgQ8fhKM= 349 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250505200425-f936aa4a68b2/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 350 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= 351 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 352 | google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= 353 | google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= 354 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 355 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 356 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 357 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 358 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 359 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 360 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 361 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 362 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 363 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 364 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 365 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 366 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 367 | k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls= 368 | k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k= 369 | k8s.io/api v0.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU= 370 | k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM= 371 | k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U= 372 | k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= 373 | k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= 374 | k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 375 | k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= 376 | k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= 377 | k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98= 378 | k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg= 379 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 380 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 381 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 382 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 383 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro= 384 | k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 385 | k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979 h1:jgJW5IePPXLGB8e/1wvd0Ich9QE97RvvF3a8J3fP/Lg= 386 | k8s.io/utils v0.0.0-20250502105355-0f33e8f1c979/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 387 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 388 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 389 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 390 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 391 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 392 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 393 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 394 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= 395 | sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 396 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 397 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 398 | -------------------------------------------------------------------------------- /pkg/assistants/simple.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package assistants provides a simple AI assistant using OpenAI's GPT models. 18 | package assistants 19 | 20 | import ( 21 | "encoding/json" 22 | "fmt" 23 | "strings" 24 | 25 | "github.com/fatih/color" 26 | "github.com/feiskyer/kube-copilot/pkg/llms" 27 | "github.com/feiskyer/kube-copilot/pkg/tools" 28 | "github.com/sashabaranov/go-openai" 29 | ) 30 | 31 | const ( 32 | defaultMaxIterations = 10 33 | ) 34 | 35 | // ToolPrompt is the JSON format for the prompt. 36 | type ToolPrompt struct { 37 | Question string `json:"question"` 38 | Thought string `json:"thought,omitempty"` 39 | Action struct { 40 | Name string `json:"name"` 41 | Input interface{} `json:"input"` 42 | } `json:"action,omitempty"` 43 | Observation string `json:"observation,omitempty"` 44 | FinalAnswer string `json:"final_answer,omitempty"` 45 | } 46 | 47 | // Assistant is the simplest AI assistant. 48 | // Deprecated: Use ReActFlow instead. 49 | func Assistant(model string, prompts []openai.ChatCompletionMessage, maxTokens int, countTokens bool, verbose bool, maxIterations int) (result string, chatHistory []openai.ChatCompletionMessage, err error) { 50 | chatHistory = prompts 51 | if len(prompts) == 0 { 52 | return "", nil, fmt.Errorf("prompts cannot be empty") 53 | } 54 | 55 | client, err := llms.NewOpenAIClient() 56 | if err != nil { 57 | return "", nil, fmt.Errorf("unable to get OpenAI client: %v", err) 58 | } 59 | 60 | defer func() { 61 | if countTokens { 62 | count := llms.NumTokensFromMessages(chatHistory, model) 63 | color.Green("Total tokens: %d\n\n", count) 64 | } 65 | }() 66 | 67 | if verbose { 68 | color.Blue("Iteration 1): chatting with LLM\n") 69 | } 70 | 71 | resp, err := client.Chat(model, maxTokens, chatHistory) 72 | if err != nil { 73 | return "", chatHistory, fmt.Errorf("chat completion error: %v", err) 74 | } 75 | 76 | chatHistory = append(chatHistory, openai.ChatCompletionMessage{ 77 | Role: openai.ChatMessageRoleAssistant, 78 | Content: string(resp), 79 | }) 80 | 81 | if verbose { 82 | color.Cyan("Initial response from LLM:\n%s\n\n", resp) 83 | } 84 | 85 | var toolPrompt ToolPrompt 86 | if err = json.Unmarshal([]byte(resp), &toolPrompt); err != nil { 87 | if verbose { 88 | color.Cyan("Unable to parse tool from prompt, assuming got final answer.\n\n", resp) 89 | } 90 | return resp, chatHistory, nil 91 | } 92 | 93 | iterations := 0 94 | if maxIterations <= 0 { 95 | maxIterations = defaultMaxIterations 96 | } 97 | for { 98 | iterations++ 99 | 100 | if verbose { 101 | color.Cyan("Thought: %s\n\n", toolPrompt.Thought) 102 | } 103 | 104 | if iterations > maxIterations { 105 | color.Red("Max iterations reached") 106 | return toolPrompt.FinalAnswer, chatHistory, nil 107 | } 108 | 109 | if toolPrompt.FinalAnswer != "" { 110 | if verbose { 111 | color.Cyan("Final answer: %v\n\n", toolPrompt.FinalAnswer) 112 | } 113 | return toolPrompt.FinalAnswer, chatHistory, nil 114 | } 115 | 116 | if toolPrompt.Action.Name != "" { 117 | input, ok := toolPrompt.Action.Input.(string) 118 | if !ok { 119 | inputBytes, err := json.Marshal(toolPrompt.Action.Input) 120 | if err != nil { 121 | return "", chatHistory, fmt.Errorf("failed to marshal tool input: %v", err) 122 | } 123 | input = string(inputBytes) 124 | } 125 | 126 | var observation string 127 | if verbose { 128 | color.Blue("Iteration %d): executing tool %s\n", iterations, toolPrompt.Action.Name) 129 | color.Cyan("Invoking %s tool with inputs: \n============\n%s\n============\n\n", toolPrompt.Action.Name, input) 130 | } 131 | if toolFunc, ok := tools.CopilotTools[toolPrompt.Action.Name]; ok { 132 | ret, err := toolFunc.ToolFunc(input) 133 | observation = strings.TrimSpace(ret) 134 | if err != nil { 135 | observation = fmt.Sprintf("Tool %s failed with error %s. Considering refine the inputs for the tool.", toolPrompt.Action.Name, ret) 136 | } 137 | } else { 138 | observation = fmt.Sprintf("Tool %s is not available. Considering switch to other supported tools.", toolPrompt.Action.Name) 139 | } 140 | if verbose { 141 | color.Cyan("Observation: %s\n\n", observation) 142 | } 143 | 144 | // Constrict the prompt to the max tokens allowed by the model. 145 | // This is required because the tool may have generated a long output. 146 | observation = llms.ConstrictPrompt(observation, model) 147 | toolPrompt.Observation = observation 148 | assistantMessage, _ := json.Marshal(toolPrompt) 149 | chatHistory = append(chatHistory, openai.ChatCompletionMessage{ 150 | Role: openai.ChatMessageRoleUser, 151 | Content: string(assistantMessage), 152 | }) 153 | // Constrict the chat history to the max tokens allowed by the model. 154 | // This is required because the chat history may have grown too large. 155 | chatHistory = llms.ConstrictMessages(chatHistory, model) 156 | 157 | // Start next iteration of LLM chat. 158 | if verbose { 159 | color.Blue("Iteration %d): chatting with LLM\n", iterations) 160 | } 161 | 162 | resp, err := client.Chat(model, maxTokens, chatHistory) 163 | if err != nil { 164 | return "", chatHistory, fmt.Errorf("chat completion error: %v", err) 165 | } 166 | 167 | chatHistory = append(chatHistory, openai.ChatCompletionMessage{ 168 | Role: openai.ChatMessageRoleAssistant, 169 | Content: string(resp), 170 | }) 171 | if verbose { 172 | color.Cyan("Intermediate response from LLM: %s\n\n", resp) 173 | } 174 | 175 | // extract the tool prompt from the LLM response. 176 | if err = json.Unmarshal([]byte(resp), &toolPrompt); err != nil { 177 | if verbose { 178 | color.Cyan("Unable to parse tools from LLM (%s), summarizing the final answer.\n\n", err.Error()) 179 | } 180 | 181 | chatHistory = append(chatHistory, openai.ChatCompletionMessage{ 182 | Role: openai.ChatMessageRoleUser, 183 | Content: "Summarize all the chat history and respond to original question with final answer", 184 | }) 185 | 186 | resp, err = client.Chat(model, maxTokens, chatHistory) 187 | if err != nil { 188 | return "", chatHistory, fmt.Errorf("chat completion error: %v", err) 189 | } 190 | 191 | return resp, chatHistory, nil 192 | } 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /pkg/kubernetes/apply.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package kubernetes 17 | 18 | import ( 19 | "bytes" 20 | "context" 21 | "io" 22 | "path/filepath" 23 | 24 | "k8s.io/apimachinery/pkg/api/meta" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | yamlserializer "k8s.io/apimachinery/pkg/runtime/serializer/yaml" 29 | "k8s.io/apimachinery/pkg/util/yaml" 30 | "k8s.io/client-go/dynamic" 31 | "k8s.io/client-go/kubernetes" 32 | "k8s.io/client-go/rest" 33 | "k8s.io/client-go/restmapper" 34 | "k8s.io/client-go/tools/clientcmd" 35 | "k8s.io/client-go/util/homedir" 36 | ) 37 | 38 | // GetKubeConfig gets kubeconfig. 39 | func GetKubeConfig() (*rest.Config, error) { 40 | config, err := rest.InClusterConfig() 41 | if err != nil { 42 | kubeconfig := filepath.Join(homedir.HomeDir(), ".kube", "config") 43 | config, err = clientcmd.BuildConfigFromFlags("", kubeconfig) 44 | if err != nil { 45 | return nil, err 46 | } 47 | } 48 | 49 | return config, nil 50 | } 51 | 52 | // ApplyYaml applies the manifests into Kubernetes cluster. 53 | func ApplyYaml(manifests string) error { 54 | config, err := GetKubeConfig() 55 | if err != nil { 56 | return err 57 | } 58 | 59 | // Create a new clientset which include all needed client APIs 60 | clientset, err := kubernetes.NewForConfig(config) 61 | if err != nil { 62 | return err 63 | } 64 | dynamicclient, err := dynamic.NewForConfig(config) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | // Decode the yaml file into a Kubernetes object 70 | decode := yaml.NewYAMLOrJSONDecoder(bytes.NewReader([]byte(manifests)), 100) 71 | for { 72 | var rawObj runtime.RawExtension 73 | if err = decode.Decode(&rawObj); err != nil { 74 | if err == io.EOF { 75 | break 76 | } 77 | return err 78 | } 79 | 80 | obj, gvk, err := yamlserializer.NewDecodingSerializer(unstructured.UnstructuredJSONScheme).Decode(rawObj.Raw, nil, nil) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | unstructuredObj := &unstructured.Unstructured{Object: unstructuredMap} 91 | if unstructuredObj.GetNamespace() == "" { 92 | unstructuredObj.SetNamespace("default") 93 | } 94 | 95 | grs, err := restmapper.GetAPIGroupResources(clientset.Discovery()) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | mapping, err := restmapper.NewDiscoveryRESTMapper(grs).RESTMapping(gvk.GroupKind(), gvk.Version) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | var dri dynamic.ResourceInterface 106 | if mapping.Scope.Name() == meta.RESTScopeNameNamespace { 107 | dri = dynamicclient.Resource(mapping.Resource).Namespace(unstructuredObj.GetNamespace()) 108 | } else { 109 | dri = dynamicclient.Resource(mapping.Resource) 110 | } 111 | 112 | if _, err := dri.Apply(context.Background(), unstructuredObj.GetName(), unstructuredObj, metav1.ApplyOptions{FieldManager: "application/apply-patch"}); err != nil { 113 | return err 114 | } 115 | } 116 | 117 | return nil 118 | } 119 | -------------------------------------------------------------------------------- /pkg/kubernetes/get.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package kubernetes 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | 22 | "gopkg.in/yaml.v2" 23 | "k8s.io/apimachinery/pkg/api/meta" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime/schema" 26 | "k8s.io/client-go/dynamic" 27 | "k8s.io/client-go/kubernetes" 28 | "k8s.io/client-go/restmapper" 29 | ) 30 | 31 | // GetYaml gets the yaml of a resource. 32 | func GetYaml(resource, name, namespace string) (string, error) { 33 | config, err := GetKubeConfig() 34 | if err != nil { 35 | return "", err 36 | } 37 | 38 | // Create a new clientset which include all needed client APIs 39 | clientset, err := kubernetes.NewForConfig(config) 40 | if err != nil { 41 | return "", err 42 | } 43 | 44 | dynamicclient, err := dynamic.NewForConfig(config) 45 | if err != nil { 46 | return "", err 47 | } 48 | 49 | grs, err := restmapper.GetAPIGroupResources(clientset.Discovery()) 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | mapper := restmapper.NewDiscoveryRESTMapper(grs) 55 | gvks, err := mapper.KindsFor(schema.GroupVersionResource{Resource: resource}) 56 | if err != nil { 57 | return "", err 58 | } 59 | 60 | if len(gvks) == 0 { 61 | return "", fmt.Errorf("no kind found for %s", resource) 62 | } 63 | 64 | gvk := gvks[0] 65 | mapping, err := restmapper.NewDiscoveryRESTMapper(grs).RESTMapping(gvk.GroupKind(), gvk.Version) 66 | if err != nil { 67 | return "", err 68 | } 69 | 70 | var dri dynamic.ResourceInterface 71 | if mapping.Scope.Name() == meta.RESTScopeNameNamespace { 72 | if namespace == "" { 73 | namespace = "default" 74 | } 75 | dri = dynamicclient.Resource(mapping.Resource).Namespace(namespace) 76 | } else { 77 | dri = dynamicclient.Resource(mapping.Resource) 78 | } 79 | 80 | res, err := dri.Get(context.Background(), name, metav1.GetOptions{}) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | data, err := yaml.Marshal(res.Object) 86 | if err != nil { 87 | return "", err 88 | } 89 | 90 | return string(data), nil 91 | } 92 | -------------------------------------------------------------------------------- /pkg/llms/openai.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package llms 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "fmt" 22 | "math" 23 | "os" 24 | "regexp" 25 | "time" 26 | 27 | "github.com/sashabaranov/go-openai" 28 | ) 29 | 30 | type OpenAIClient struct { 31 | *openai.Client 32 | 33 | Retries int 34 | Backoff time.Duration 35 | } 36 | 37 | // NewOpenAIClient returns an OpenAI client. 38 | func NewOpenAIClient() (*OpenAIClient, error) { 39 | apiKey := os.Getenv("OPENAI_API_KEY") 40 | if apiKey != "" { 41 | config := openai.DefaultConfig(apiKey) 42 | baseURL := os.Getenv("OPENAI_API_BASE") 43 | if baseURL != "" { 44 | config.BaseURL = baseURL 45 | } 46 | 47 | return &OpenAIClient{ 48 | Retries: 5, 49 | Backoff: time.Second, 50 | Client: openai.NewClientWithConfig(config), 51 | }, nil 52 | } 53 | 54 | azureAPIKey := os.Getenv("AZURE_OPENAI_API_KEY") 55 | azureAPIBase := os.Getenv("AZURE_OPENAI_API_BASE") 56 | azureAPIVersion := os.Getenv("AZURE_OPENAI_API_VERSION") 57 | if azureAPIVersion == "" { 58 | azureAPIVersion = "2025-03-01-preview" 59 | } 60 | if azureAPIKey != "" && azureAPIBase != "" { 61 | config := openai.DefaultConfig(azureAPIKey) 62 | config.BaseURL = azureAPIBase 63 | config.APIVersion = azureAPIVersion 64 | config.APIType = openai.APITypeAzure 65 | config.AzureModelMapperFunc = func(model string) string { 66 | return regexp.MustCompile(`[.:]`).ReplaceAllString(model, "") 67 | } 68 | 69 | return &OpenAIClient{ 70 | Retries: 5, 71 | Backoff: time.Second, 72 | Client: openai.NewClientWithConfig(config), 73 | }, nil 74 | } 75 | 76 | return nil, fmt.Errorf("OPENAI_API_KEY or AZURE_OPENAI_API_KEY is not set") 77 | } 78 | 79 | func (c *OpenAIClient) Chat(model string, maxTokens int, prompts []openai.ChatCompletionMessage) (string, error) { 80 | req := openai.ChatCompletionRequest{ 81 | Model: model, 82 | MaxTokens: maxTokens, 83 | Temperature: math.SmallestNonzeroFloat32, 84 | Messages: prompts, 85 | } 86 | if model == "o1-mini" || model == "o3-mini" || model == "o1" || model == "o3" { 87 | req = openai.ChatCompletionRequest{ 88 | Model: model, 89 | MaxCompletionTokens: maxTokens, 90 | Messages: prompts, 91 | } 92 | } 93 | 94 | backoff := c.Backoff 95 | for try := 0; try < c.Retries; try++ { 96 | resp, err := c.Client.CreateChatCompletion(context.Background(), req) 97 | if err == nil { 98 | return string(resp.Choices[0].Message.Content), nil 99 | } 100 | 101 | e := &openai.APIError{} 102 | 103 | if errors.As(err, &e) { 104 | switch e.HTTPStatusCode { 105 | case 401: 106 | return "", err 107 | case 429, 500: 108 | time.Sleep(backoff) 109 | backoff *= 2 110 | continue 111 | default: 112 | return "", err 113 | } 114 | } 115 | 116 | return "", err 117 | } 118 | 119 | return "", fmt.Errorf("OpenAI request throttled after retrying %d times", c.Retries) 120 | } 121 | -------------------------------------------------------------------------------- /pkg/llms/tokens.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package llms 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "math" 23 | "strings" 24 | 25 | "github.com/pkoukk/tiktoken-go" 26 | "github.com/sashabaranov/go-openai" 27 | ) 28 | 29 | var tokenLimitsPerModel = map[string]int{ 30 | "code-davinci-002": 4096, 31 | "gpt-3.5-turbo-0301": 4096, 32 | "gpt-3.5-turbo-0613": 4096, 33 | "gpt-3.5-turbo-1106": 16385, 34 | "gpt-3.5-turbo-16k-0613": 16385, 35 | "gpt-3.5-turbo-16k": 16385, 36 | "gpt-3.5-turbo-instruct": 4096, 37 | "gpt-3.5-turbo": 4096, 38 | "gpt-4-0314": 8192, 39 | "gpt-4-0613": 8192, 40 | "gpt-4-1106-preview": 128000, 41 | "gpt-4-32k-0314": 32768, 42 | "gpt-4-32k-0613": 32768, 43 | "gpt-4-32k": 32768, 44 | "gpt-4-vision-preview": 128000, 45 | "gpt-4": 8192, 46 | "gpt-4-turbo": 128000, 47 | "text-davinci-002": 4096, 48 | "text-davinci-003": 4096, 49 | "gpt-4o": 128000, 50 | "gpt-4o-mini": 128000, 51 | "o1-mini": 128000, 52 | "o3-mini": 200000, 53 | "o1": 200000, 54 | } 55 | 56 | // GetTokenLimits returns the maximum number of tokens for the given model. 57 | func GetTokenLimits(model string) int { 58 | model = strings.ToLower(model) 59 | if maxTokens, ok := tokenLimitsPerModel[model]; ok { 60 | return maxTokens 61 | } 62 | 63 | return 8192 64 | } 65 | 66 | // NumTokensFromMessages returns the number of tokens in the given messages. 67 | // OpenAI Cookbook: https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb 68 | func NumTokensFromMessages(messages []openai.ChatCompletionMessage, model string) (numTokens int) { 69 | encodingModel := model 70 | if model == "o1-mini" || model == "o3-mini" || model == "o1" || model == "o3" { 71 | encodingModel = "gpt-4o" 72 | } 73 | tkm, err := tiktoken.EncodingForModel(encodingModel) 74 | if err != nil { 75 | err = fmt.Errorf("encoding for model: %v", err) 76 | log.Println(err) 77 | return 78 | } 79 | 80 | tokensPerMessage := 3 81 | tokensPerName := 1 82 | if model == "gpt-3.5-turbo-0301" { 83 | tokensPerMessage = 4 // every message follows <|start|>{role/name}\n{content}<|end|>\n 84 | tokensPerName = -1 // if there's a name, the role is omitted 85 | } 86 | 87 | for _, message := range messages { 88 | numTokens += tokensPerMessage 89 | numTokens += len(tkm.Encode(message.Content, nil, nil)) 90 | numTokens += len(tkm.Encode(message.Role, nil, nil)) 91 | if message.Name != "" { 92 | numTokens += len(tkm.Encode(message.Name, nil, nil)) 93 | numTokens += tokensPerName 94 | } 95 | } 96 | numTokens += 3 // every reply is primed with <|start|>assistant<|message|> 97 | return numTokens 98 | } 99 | 100 | // ConstrictMessages returns the messages that fit within the token limit. 101 | func ConstrictMessages(messages []openai.ChatCompletionMessage, model string) []openai.ChatCompletionMessage { 102 | tokenLimits := GetTokenLimits(model) 103 | 104 | for { 105 | numTokens := NumTokensFromMessages(messages, model) 106 | if numTokens <= tokenLimits { 107 | return messages 108 | } 109 | 110 | // If no messages, return empty 111 | if len(messages) == 0 { 112 | return messages 113 | } 114 | 115 | // If only one message or we can't reduce further 116 | if len(messages) <= 1 { 117 | return messages 118 | } 119 | 120 | // When over the limit, try keeping only the system prompt (first message) 121 | // and the most recent message 122 | if len(messages) > 2 { 123 | // Try with just system and last message 124 | systemAndLatest := []openai.ChatCompletionMessage{ 125 | messages[0], 126 | messages[len(messages)-1], 127 | } 128 | messages = systemAndLatest 129 | } else { 130 | // We have exactly 2 messages and they're still over the limit 131 | // Keep only the first message (usually system prompt) 132 | messages = messages[:1] 133 | } 134 | } 135 | } 136 | 137 | // ConstrictPrompt returns the prompt that fits within the token limit. 138 | func ConstrictPrompt(prompt string, model string) string { 139 | tokenLimits := GetTokenLimits(model) 140 | 141 | for { 142 | numTokens := NumTokensFromMessages([]openai.ChatCompletionMessage{{Content: prompt}}, model) 143 | if numTokens < tokenLimits { 144 | return prompt 145 | } 146 | 147 | // Remove the first third percent lines 148 | lines := strings.Split(prompt, "\n") 149 | lines = lines[int64(math.Ceil(float64(len(lines))/3)):] 150 | prompt = strings.Join(lines, "\n") 151 | 152 | if strings.TrimSpace(prompt) == "" { 153 | return "" 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /pkg/llms/tokens_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package llms 18 | 19 | import ( 20 | "reflect" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/sashabaranov/go-openai" 25 | ) 26 | 27 | func TestGetTokenLimits(t *testing.T) { 28 | tests := []struct { 29 | name string 30 | model string 31 | expected int 32 | }{ 33 | {"gpt-3.5-turbo", "gpt-3.5-turbo", 4096}, 34 | {"gpt-4", "gpt-4", 8192}, 35 | {"gpt-4-turbo", "gpt-4-turbo", 128000}, 36 | {"case insensitive", "GPT-4", 8192}, 37 | {"unknown model", "unknown-model", 8192}, 38 | {"claude model", "o1-mini", 128000}, 39 | {"claude o1", "o1", 200000}, 40 | } 41 | 42 | for _, tt := range tests { 43 | t.Run(tt.name, func(t *testing.T) { 44 | got := GetTokenLimits(tt.model) 45 | if got != tt.expected { 46 | t.Errorf("GetTokenLimits(%s) = %d, want %d", tt.model, got, tt.expected) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestNumTokensFromMessages(t *testing.T) { 53 | tests := []struct { 54 | name string 55 | messages []openai.ChatCompletionMessage 56 | model string 57 | minTokens int // Use min/max range since exact token counts can be version-dependent 58 | maxTokens int 59 | }{ 60 | { 61 | name: "empty message", 62 | messages: []openai.ChatCompletionMessage{ 63 | {Role: "user", Content: ""}, 64 | }, 65 | model: "gpt-4", 66 | minTokens: 3, 67 | maxTokens: 8, 68 | }, 69 | { 70 | name: "simple message", 71 | messages: []openai.ChatCompletionMessage{ 72 | {Role: "user", Content: "Hello, world!"}, 73 | }, 74 | model: "gpt-4", 75 | minTokens: 8, 76 | maxTokens: 12, 77 | }, 78 | { 79 | name: "multiple messages", 80 | messages: []openai.ChatCompletionMessage{ 81 | {Role: "system", Content: "You are a helpful assistant."}, 82 | {Role: "user", Content: "Tell me about AI."}, 83 | {Role: "assistant", Content: "AI stands for artificial intelligence."}, 84 | }, 85 | model: "gpt-4", 86 | minTokens: 25, 87 | maxTokens: 35, 88 | }, 89 | { 90 | name: "message with name", 91 | messages: []openai.ChatCompletionMessage{ 92 | {Role: "user", Content: "Hello", Name: "John"}, 93 | }, 94 | model: "gpt-4", 95 | minTokens: 8, 96 | maxTokens: 12, 97 | }, 98 | { 99 | name: "o1-mini model", 100 | messages: []openai.ChatCompletionMessage{ 101 | {Role: "user", Content: "Hello, world!"}, 102 | }, 103 | model: "o1-mini", 104 | minTokens: 8, 105 | maxTokens: 12, 106 | }, 107 | } 108 | 109 | for _, tt := range tests { 110 | t.Run(tt.name, func(t *testing.T) { 111 | got := NumTokensFromMessages(tt.messages, tt.model) 112 | if got < tt.minTokens || got > tt.maxTokens { 113 | t.Errorf("NumTokensFromMessages() = %d, want between %d and %d", 114 | got, tt.minTokens, tt.maxTokens) 115 | } 116 | }) 117 | } 118 | } 119 | 120 | func TestConstrictMessages(t *testing.T) { 121 | systemMsg := openai.ChatCompletionMessage{Role: "system", Content: "You are a helpful assistant."} 122 | 123 | // Create a long message that will exceed token limits 124 | longContent := strings.Repeat("This is a very long message that will exceed token limits. ", 1000) 125 | longMsg := openai.ChatCompletionMessage{Role: "user", Content: longContent} 126 | 127 | shortMsg1 := openai.ChatCompletionMessage{Role: "user", Content: "Short message 1"} 128 | shortMsg2 := openai.ChatCompletionMessage{Role: "assistant", Content: "Short reply 1"} 129 | shortMsg3 := openai.ChatCompletionMessage{Role: "user", Content: "Short message 2"} 130 | 131 | tests := []struct { 132 | name string 133 | messages []openai.ChatCompletionMessage 134 | model string 135 | expectedLen int 136 | checkContent bool // whether to check exact content or just length 137 | }{ 138 | { 139 | name: "under limit", 140 | messages: []openai.ChatCompletionMessage{systemMsg, shortMsg1, shortMsg2}, 141 | model: "gpt-4", 142 | expectedLen: 3, 143 | checkContent: true, 144 | }, 145 | { 146 | name: "over limit with multiple messages", 147 | messages: []openai.ChatCompletionMessage{systemMsg, longMsg, shortMsg1, shortMsg2, shortMsg3}, 148 | model: "gpt-3.5-turbo", 149 | expectedLen: 2, // Should keep system and latest message 150 | checkContent: false, 151 | }, 152 | { 153 | name: "empty messages", 154 | messages: []openai.ChatCompletionMessage{}, 155 | model: "gpt-4", 156 | expectedLen: 0, 157 | checkContent: true, 158 | }, 159 | { 160 | name: "single message over limit", 161 | messages: []openai.ChatCompletionMessage{longMsg}, 162 | model: "gpt-3.5-turbo", 163 | expectedLen: 1, // Still keeps the message even if it's over limit 164 | checkContent: true, 165 | }, 166 | { 167 | name: "system and long message over limit", 168 | messages: []openai.ChatCompletionMessage{systemMsg, longMsg}, 169 | model: "gpt-3.5-turbo", 170 | expectedLen: 1, // Should keep only system message 171 | checkContent: false, 172 | }, 173 | } 174 | 175 | for _, tt := range tests { 176 | t.Run(tt.name, func(t *testing.T) { 177 | result := ConstrictMessages(tt.messages, tt.model) 178 | 179 | if len(result) != tt.expectedLen { 180 | t.Errorf("ConstrictMessages() returned %d messages, want %d", 181 | len(result), tt.expectedLen) 182 | } 183 | 184 | if tt.checkContent && tt.expectedLen > 0 && !reflect.DeepEqual(result, tt.messages) { 185 | t.Errorf("ConstrictMessages() modified messages when it shouldn't have") 186 | } 187 | 188 | // For over limit cases with system message, verify system is preserved 189 | if tt.expectedLen > 0 && len(tt.messages) > 0 && 190 | tt.messages[0].Role == "system" && !tt.checkContent { 191 | if result[0].Role != "system" { 192 | t.Errorf("ConstrictMessages() didn't preserve system message") 193 | } 194 | } 195 | }) 196 | } 197 | } 198 | 199 | func TestConstrictPrompt(t *testing.T) { 200 | shortPrompt := "This is a short prompt." 201 | 202 | // Create prompts of different lengths 203 | mediumPrompt := strings.Repeat("This is a medium length prompt that should still fit. ", 50) 204 | longPrompt := strings.Repeat("This is a very long prompt that will need to be trimmed. ", 2000) 205 | 206 | tests := []struct { 207 | name string 208 | prompt string 209 | model string 210 | shouldBeSame bool 211 | shouldBeEmpty bool 212 | }{ 213 | { 214 | name: "short prompt", 215 | prompt: shortPrompt, 216 | model: "gpt-4", 217 | shouldBeSame: true, 218 | shouldBeEmpty: false, 219 | }, 220 | { 221 | name: "medium prompt", 222 | prompt: mediumPrompt, 223 | model: "gpt-4", 224 | shouldBeSame: true, 225 | shouldBeEmpty: false, 226 | }, 227 | { 228 | name: "long prompt", 229 | prompt: longPrompt, 230 | model: "gpt-3.5-turbo", 231 | shouldBeSame: false, 232 | shouldBeEmpty: false, 233 | }, 234 | { 235 | name: "empty prompt", 236 | prompt: "", 237 | model: "gpt-4", 238 | shouldBeSame: true, 239 | shouldBeEmpty: true, 240 | }, 241 | } 242 | 243 | for _, tt := range tests { 244 | t.Run(tt.name, func(t *testing.T) { 245 | result := ConstrictPrompt(tt.prompt, tt.model) 246 | 247 | if tt.shouldBeSame && result != tt.prompt { 248 | t.Errorf("ConstrictPrompt() modified prompt when it shouldn't have") 249 | } 250 | 251 | if !tt.shouldBeSame && result == tt.prompt && tt.prompt != "" { 252 | t.Errorf("ConstrictPrompt() didn't modify prompt when it should have") 253 | } 254 | 255 | if tt.shouldBeEmpty && result != "" { 256 | t.Errorf("ConstrictPrompt() returned non-empty result for empty prompt") 257 | } 258 | 259 | // Test that the modified prompt is actually shorter 260 | if !tt.shouldBeSame && !tt.shouldBeEmpty { 261 | if len(result) >= len(tt.prompt) { 262 | t.Errorf("ConstrictPrompt() didn't make prompt shorter") 263 | } 264 | } 265 | 266 | // Verify the result fits within token limit 267 | numTokens := NumTokensFromMessages([]openai.ChatCompletionMessage{{Content: result}}, tt.model) 268 | tokenLimit := GetTokenLimits(tt.model) 269 | if numTokens >= tokenLimit { 270 | t.Errorf("ConstrictPrompt() returned result with %d tokens, exceeding limit of %d", 271 | numTokens, tokenLimit) 272 | } 273 | }) 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /pkg/tools/googlesearch.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package tools 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | 23 | customsearch "google.golang.org/api/customsearch/v1" 24 | option "google.golang.org/api/option" 25 | ) 26 | 27 | // GoogleSearch returns the results of a Google search for the given query. 28 | func GoogleSearch(query string) (string, error) { 29 | svc, err := customsearch.NewService(context.Background(), option.WithAPIKey(os.Getenv("GOOGLE_API_KEY"))) 30 | if err != nil { 31 | return "", err 32 | } 33 | 34 | resp, err := svc.Cse.List().Cx(os.Getenv("GOOGLE_CSE_ID")).Q(query).Do() 35 | if err != nil { 36 | return "", err 37 | } 38 | 39 | results := "" 40 | for _, result := range resp.Items { 41 | results += fmt.Sprintf("%s: %s\n", result.Title, result.Snippet) 42 | } 43 | return results, nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/tools/kubectl.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package tools 17 | 18 | import ( 19 | "errors" 20 | "os/exec" 21 | "strings" 22 | ) 23 | 24 | // Kubectl runs the given kubectl command and returns the output. 25 | func Kubectl(command string) (string, error) { 26 | if strings.HasPrefix(command, "kubectl") { 27 | command = strings.TrimSpace(strings.TrimPrefix(command, "kubectl")) 28 | } 29 | 30 | if strings.HasPrefix(command, "edit") { 31 | return "", errors.New("interactive command kubectl edit is not supported") 32 | } 33 | 34 | args := parseCommandWithQuotes(command) 35 | cmd := exec.Command("kubectl", args...) 36 | output, err := cmd.CombinedOutput() 37 | if err != nil { 38 | return strings.TrimSpace(string(output)), err 39 | } 40 | 41 | return strings.TrimSpace(string(output)), nil 42 | } 43 | 44 | // parseCommandWithQuotes splits a command string into arguments, respecting quoted strings 45 | func parseCommandWithQuotes(command string) []string { 46 | var args []string 47 | var currentArg strings.Builder 48 | inQuotes := false 49 | quoteChar := rune(0) 50 | 51 | for _, ch := range command { 52 | switch { 53 | case ch == '\'' || ch == '"': 54 | if inQuotes && ch == quoteChar { 55 | // End of quoted section 56 | inQuotes = false 57 | quoteChar = rune(0) 58 | } else if !inQuotes { 59 | // Start of quoted section 60 | inQuotes = true 61 | quoteChar = ch 62 | } else { 63 | // We're in quotes but found a different quote character, add it 64 | currentArg.WriteRune(ch) 65 | } 66 | case ch == ' ' && !inQuotes: 67 | // Space outside quotes - end of argument 68 | if currentArg.Len() > 0 { 69 | args = append(args, currentArg.String()) 70 | currentArg.Reset() 71 | } 72 | default: 73 | // Add character to current argument 74 | currentArg.WriteRune(ch) 75 | } 76 | } 77 | 78 | // Add the last argument if there is one 79 | if currentArg.Len() > 0 { 80 | args = append(args, currentArg.String()) 81 | } 82 | 83 | return args 84 | } 85 | -------------------------------------------------------------------------------- /pkg/tools/mcp.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2025 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package tools 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "net/http" 23 | "os" 24 | "strings" 25 | "time" 26 | 27 | "github.com/fatih/color" 28 | "github.com/mark3labs/mcp-go/client" 29 | "github.com/mark3labs/mcp-go/client/transport" 30 | "github.com/mark3labs/mcp-go/mcp" 31 | ) 32 | 33 | // MCPConfig is the configuration for the MCP server. 34 | type MCPConfig struct { 35 | MCPServers map[string]MCPServer `json:"mcpServers"` 36 | } 37 | 38 | // MCPServer is the configuration for a single MCP server. 39 | type MCPServer struct { 40 | Type string `json:"type,omitempty"` 41 | Command string `json:"command,omitempty"` 42 | Args []string `json:"args,omitempty"` 43 | URL string `json:"url,omitempty"` 44 | Env map[string]string `json:"env,omitempty"` 45 | Headers map[string]string `json:"headers,omitempty"` 46 | Timeout int `json:"timeout,omitempty"` 47 | } 48 | 49 | // MCPTool is a tool that uses the MCP protocol. 50 | type MCPTool struct { 51 | name string 52 | description string 53 | inputSchema string 54 | toolFunc func(input string) (string, error) 55 | } 56 | 57 | // Description returns the description of the tool. 58 | func (t MCPTool) Description() string { 59 | return t.description 60 | } 61 | 62 | // InputSchema returns the input schema for the tool. 63 | func (t MCPTool) InputSchema() string { 64 | return t.inputSchema 65 | } 66 | 67 | // ToolFunc is the function that will be called to execute the tool. 68 | func (t MCPTool) ToolFunc(input string) (string, error) { 69 | return t.toolFunc(input) 70 | } 71 | 72 | // GetMCPTools returns the MCP tools. 73 | func GetMCPTools(configFile string, verbose bool) (map[string]Tool, map[string]client.MCPClient, error) { 74 | var config MCPConfig 75 | mcpTools := make(map[string]Tool) 76 | mcpClients := make(map[string]client.MCPClient) 77 | 78 | // Read the config file 79 | content, err := os.ReadFile(configFile) 80 | if err != nil { 81 | return nil, nil, fmt.Errorf("failed to read config file: %v", err) 82 | } 83 | 84 | // Parse the config file 85 | if err := json.Unmarshal(content, &config); err != nil { 86 | return nil, nil, fmt.Errorf("failed to parse config file: %v", err) 87 | } 88 | 89 | // Create a client for each MCP server 90 | for name, server := range config.MCPServers { 91 | if verbose { 92 | color.Green("Creating client for %s", name) 93 | } 94 | 95 | var c client.MCPClient 96 | envs := make([]string, 0) 97 | for _, env := range os.Environ() { 98 | envs = append(envs, env) 99 | } 100 | for k, v := range server.Env { 101 | envs = append(envs, fmt.Sprintf("%s=%s", k, v)) 102 | } 103 | if server.Command != "" || server.Type == "stdio" { 104 | c, err = client.NewStdioMCPClient( 105 | server.Command, 106 | envs, 107 | server.Args..., 108 | ) 109 | if err != nil { 110 | return nil, nil, fmt.Errorf("failed to create client for %s: %v", name, err) 111 | } 112 | } else if server.URL != "" { 113 | if strings.Contains(server.URL, "sse") || strings.EqualFold(server.Type, "sse") { 114 | c, err = client.NewSSEMCPClient(server.URL, transport.WithHeaders(server.Headers), transport.WithHTTPClient(&http.Client{Timeout: time.Duration(server.Timeout) * time.Second})) 115 | } else { 116 | c, err = client.NewStreamableHttpClient(server.URL, transport.WithHTTPHeaders(server.Headers), transport.WithHTTPTimeout(time.Duration(server.Timeout)*time.Second)) 117 | } 118 | if err != nil { 119 | return nil, nil, fmt.Errorf("failed to create client for %s: %v", name, err) 120 | } 121 | } else { 122 | return nil, nil, fmt.Errorf("no command or URL specified for %s", name) 123 | } 124 | 125 | tools, err := createMCPTools(c, name, verbose) 126 | if err != nil { 127 | c.Close() 128 | return nil, nil, fmt.Errorf("failed to create client for %s: %v", name, err) 129 | } 130 | 131 | mcpClients[name] = c 132 | for toolName, tool := range tools { 133 | mcpTools[toolName] = tool 134 | } 135 | } 136 | 137 | return mcpTools, mcpClients, nil 138 | } 139 | 140 | func createMCPTools(c client.MCPClient, name string, verbose bool) (map[string]Tool, error) { 141 | if verbose { 142 | color.Green("Initializing client for %s", name) 143 | } 144 | mcpTools := make(map[string]Tool) 145 | 146 | // Initialize the client 147 | ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) 148 | defer cancel() 149 | initRequest := mcp.InitializeRequest{} 150 | initRequest.Params.ProtocolVersion = mcp.LATEST_PROTOCOL_VERSION 151 | _, err := c.Initialize(ctx, initRequest) 152 | if err != nil { 153 | return nil, fmt.Errorf("failed to initialize: %v", err) 154 | } 155 | 156 | // List tools 157 | if verbose { 158 | color.Green("Listing tools for %s", name) 159 | } 160 | toolsRequest := mcp.ListToolsRequest{} 161 | tools, err := c.ListTools(ctx, toolsRequest) 162 | if err != nil { 163 | return nil, fmt.Errorf("failed to list tools: %v", err) 164 | } 165 | 166 | for _, tool := range tools.Tools { 167 | toolName := fmt.Sprintf("%s_%s", name, tool.Name) 168 | inputSchema, err := json.Marshal(tool.InputSchema) 169 | if err != nil { 170 | return nil, fmt.Errorf("failed to marshal input schema: %v", err) 171 | } 172 | 173 | mcpTools[toolName] = MCPTool{ 174 | name: toolName, 175 | description: tool.Description, 176 | inputSchema: fmt.Sprintf("JSON Schema: %s", inputSchema), 177 | toolFunc: func(input string) (string, error) { 178 | callRequest := mcp.CallToolRequest{} 179 | callRequest.Params.Name = tool.Name 180 | 181 | var args map[string]interface{} 182 | err := json.Unmarshal([]byte(input), &args) 183 | if err != nil { 184 | if len(tool.InputSchema.Required) > 0 { 185 | args[tool.InputSchema.Required[0]] = input 186 | } else { 187 | args["input"] = input 188 | } 189 | } 190 | callRequest.Params.Arguments = args 191 | 192 | callResult, err := c.CallTool(context.Background(), callRequest) 193 | if err != nil { 194 | return "", fmt.Errorf("failed to call tool %s: %v", tool.Name, err) 195 | } 196 | // color.Green("Got tool %s result: %q", toolName, callResult.Content) 197 | 198 | var contentStrings []string 199 | for _, content := range callResult.Content { 200 | if textContent, ok := content.(mcp.TextContent); ok { 201 | contentStrings = append(contentStrings, textContent.Text) 202 | } 203 | } 204 | return strings.Join(contentStrings, "\n"), nil 205 | }, 206 | } 207 | } 208 | return mcpTools, nil 209 | } 210 | -------------------------------------------------------------------------------- /pkg/tools/python.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package tools 17 | 18 | import ( 19 | "os/exec" 20 | "strings" 21 | ) 22 | 23 | // PythonREPL runs the given Python script and returns the output. 24 | func PythonREPL(script string) (string, error) { 25 | cmd := exec.Command("python3", "-c", script) 26 | 27 | output, err := cmd.CombinedOutput() 28 | if err != nil { 29 | return strings.TrimSpace(string(output)), err 30 | } 31 | 32 | return strings.TrimSpace(string(output)), nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/tools/python_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package tools 17 | 18 | import ( 19 | "strings" 20 | "testing" 21 | ) 22 | 23 | func TestPythonREPL(t *testing.T) { 24 | type args struct { 25 | script string 26 | } 27 | tests := []struct { 28 | name string 29 | args string 30 | want string 31 | wantErr bool 32 | }{ 33 | { 34 | name: "normal test", 35 | args: "print('hello world')", 36 | want: "hello world", 37 | wantErr: false, 38 | }, 39 | { 40 | name: "error test", 41 | args: "print('hello world'", 42 | want: "SyntaxError: '(' was never closed", 43 | wantErr: true, 44 | }, 45 | } 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | got, err := PythonREPL(tt.args) 49 | if (err != nil) != tt.wantErr { 50 | t.Errorf("PythonREPL() error = %v, wantErr %v", err, tt.wantErr) 51 | return 52 | } 53 | if got != tt.want && !strings.Contains(got, tt.want) { 54 | t.Errorf("PythonREPL() = %v, want %v", got, tt.want) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/tools/tool.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package tools 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | "strings" 22 | 23 | "github.com/fatih/color" 24 | "github.com/mark3labs/mcp-go/client" 25 | ) 26 | 27 | // Tool is an interface that defines the methods for a tool. 28 | type Tool interface { 29 | Description() string 30 | InputSchema() string 31 | ToolFunc(input string) (string, error) 32 | } 33 | 34 | // CopilotTools is a map of tool names to tools. 35 | var CopilotTools = map[string]Tool{ 36 | // "python": PythonREPLTool{}, 37 | "trivy": TrivyTool{}, 38 | "kubectl": KubectlTool{}, 39 | } 40 | 41 | // PythonREPLTool executes Python code in a REPL environment. 42 | type PythonREPLTool struct{} 43 | 44 | // Description returns the description of the tool. 45 | func (p PythonREPLTool) Description() string { 46 | return "Execute Python code in a REPL environment" 47 | } 48 | 49 | // InputSchema returns the input schema for the tool. 50 | func (p PythonREPLTool) InputSchema() string { 51 | return "Python code in string format to execute" 52 | } 53 | 54 | // ToolFunc executes the provided Python code and returns the result. 55 | func (p PythonREPLTool) ToolFunc(script string) (string, error) { 56 | return PythonREPL(script) 57 | } 58 | 59 | // TrivyTool scans container images for vulnerabilities using Trivy. 60 | type TrivyTool struct{} 61 | 62 | // Description returns the description of the tool. 63 | func (t TrivyTool) Description() string { 64 | return "Scan container images for vulnerabilities using Trivy" 65 | } 66 | 67 | // InputSchema returns the input schema for the tool. 68 | func (t TrivyTool) InputSchema() string { 69 | return "Container image in string format to scan" 70 | } 71 | 72 | // ToolFunc scans the provided container image and returns the result. 73 | func (t TrivyTool) ToolFunc(image string) (string, error) { 74 | return Trivy(image) 75 | } 76 | 77 | // KubectlTool executes kubectl commands against a Kubernetes cluster. 78 | type KubectlTool struct{} 79 | 80 | // Description returns the description of the tool. 81 | func (k KubectlTool) Description() string { 82 | return "Execute kubectl commands against a Kubernetes cluster." 83 | } 84 | 85 | // InputSchema returns the input schema for the tool. 86 | func (k KubectlTool) InputSchema() string { 87 | return "kubectl command in string format to execute. Ensure command is a single kubectl and shell pipe (|) and redirect (>) are not supported." 88 | } 89 | 90 | // ToolFunc executes the provided kubectl command and returns the result. 91 | func (k KubectlTool) ToolFunc(command string) (string, error) { 92 | return Kubectl(command) 93 | } 94 | 95 | // GoogleSearchTool performs web searches using the Google Search API. 96 | type GoogleSearchTool struct{} 97 | 98 | // Description returns the description of the tool. 99 | func (g GoogleSearchTool) Description() string { 100 | return "Search the web using Google" 101 | } 102 | 103 | // InputSchema returns the input schema for the tool. 104 | func (g GoogleSearchTool) InputSchema() string { 105 | return "Search query in string format" 106 | } 107 | 108 | // ToolFunc performs a web search using the provided query and returns the result. 109 | func (g GoogleSearchTool) ToolFunc(query string) (string, error) { 110 | return GoogleSearch(query) 111 | } 112 | 113 | // InitTools initializes the tools. 114 | func InitTools(mcpConfigFile string, disableKubectl, verbose bool) (map[string]client.MCPClient, error) { 115 | if os.Getenv("GOOGLE_API_KEY") != "" && os.Getenv("GOOGLE_CSE_ID") != "" { 116 | CopilotTools["search"] = GoogleSearchTool{} 117 | } 118 | if disableKubectl { 119 | delete(CopilotTools, "kubectl") 120 | } 121 | 122 | if mcpConfigFile != "" { 123 | mcpTools, mcpClients, err := GetMCPTools(mcpConfigFile, verbose) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | if verbose { 129 | tools := "" 130 | for toolName := range mcpTools { 131 | tools += fmt.Sprintf("%s, ", toolName) 132 | } 133 | color.Green("Enabled MCP tools: %s", strings.TrimRight(tools, ", ")) 134 | } 135 | 136 | for toolName, tool := range mcpTools { 137 | CopilotTools[toolName] = tool 138 | } 139 | 140 | return mcpClients, nil 141 | } 142 | 143 | return nil, nil 144 | } 145 | 146 | // GetToolPrompt returns the tool prompt. 147 | func GetToolPrompt() string { 148 | tools := "" 149 | for toolName, tool := range CopilotTools { 150 | tools += fmt.Sprintf("- %s: %s, input schema: %s\n", toolName, tool.Description(), tool.InputSchema()) 151 | } 152 | 153 | return tools 154 | } 155 | -------------------------------------------------------------------------------- /pkg/tools/trivy.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package tools 17 | 18 | import ( 19 | "os/exec" 20 | "strings" 21 | ) 22 | 23 | // Trivy runs trivy against the image and returns the output 24 | func Trivy(image string) (string, error) { 25 | image = strings.TrimSpace(image) 26 | if strings.HasPrefix(image, "trivy ") { 27 | image = strings.TrimPrefix(image, "trivy ") 28 | } 29 | 30 | if strings.HasPrefix(image, "image ") { 31 | image = strings.TrimPrefix(image, "image ") 32 | } 33 | 34 | cmd := exec.Command("trivy", "image", image, "--scanners", "vuln") 35 | output, err := cmd.CombinedOutput() 36 | if err != nil { 37 | return strings.TrimSpace(string(output)), err 38 | } 39 | 40 | return strings.TrimSpace(string(output)), nil 41 | } 42 | -------------------------------------------------------------------------------- /pkg/utils/term.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package utils 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/charmbracelet/glamour" 22 | "golang.org/x/term" 23 | ) 24 | 25 | // RenderMarkdown renders markdown to the terminal. 26 | func RenderMarkdown(md string) error { 27 | width, _, _ := term.GetSize(0) 28 | styler, err := glamour.NewTermRenderer( 29 | glamour.WithAutoStyle(), 30 | glamour.WithWordWrap(width), 31 | ) 32 | if err != nil { 33 | fmt.Println(md) 34 | return err 35 | } 36 | 37 | out, err := styler.Render(md) 38 | if err != nil { 39 | fmt.Println(md) 40 | return err 41 | } 42 | 43 | fmt.Println(out) 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /pkg/utils/yaml.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package utils 17 | 18 | import ( 19 | "regexp" 20 | "strings" 21 | ) 22 | 23 | // ExtractYaml extracts yaml from a markdown message. 24 | func ExtractYaml(message string) string { 25 | r1 := regexp.MustCompile("(?s)```yaml(.*?)```") 26 | matches := r1.FindStringSubmatch(strings.TrimSpace(message)) 27 | if len(matches) > 1 { 28 | return matches[1] 29 | } 30 | 31 | r2 := regexp.MustCompile("(?s)```(.*?)```") 32 | matches = r2.FindStringSubmatch(strings.TrimSpace(message)) 33 | if len(matches) > 1 { 34 | return matches[1] 35 | } 36 | 37 | return "" 38 | } 39 | -------------------------------------------------------------------------------- /pkg/workflows/analyze.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package workflows 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/feiskyer/swarm-go" 24 | ) 25 | 26 | const analysisPrompt = `As an expert on Kubernetes, your task is analyzing the given Kubernetes manifests, figure out the issues and provide solutions in a human-readable format. 27 | For each identified issue, document the analysis and solution in everyday language, employing simple analogies to clarify technical points. 28 | 29 | # Steps 30 | 31 | 1. **Identify Clues**: Treat each piece of YAML configuration data like a clue in a mystery. Explain how it helps to understand the issue, similar to a detective piecing together a case. 32 | 2. **Analysis with Analogies**: Translate your technical findings into relatable scenarios. Use everyday analogies to explain concepts, avoiding complex jargon. This makes episodes like 'pod failures' or 'service disruptions' simple to grasp. 33 | 3. **Solution as a DIY Guide**: Offer a step-by-step solution akin to guiding someone through a household fix-up. Instructions should be straightforward, logical, and accessible. 34 | 4. **Document Findings**: 35 | - Separate analysis and solution clearly for each issue, detailing them in non-technical language. 36 | 37 | # Output Format 38 | 39 | Provide the output in structured markdown, using clear and concise language. 40 | 41 | # Examples 42 | 43 | ## 1. 44 | 45 | - **Findings**: The YAML configuration doesn't specify the memory limit for the pod. 46 | - **How to resolve**: Set memory limit in Pod spec. 47 | 48 | ## 2. HIGH Severity: CVE-2024-10963 49 | 50 | - **Findings**: The Pod is running with CVE pam: Improper Hostname Interpretation in pam_access Leads to Access Control Bypass. 51 | - **How to resolve**: Update package libpam-modules to fixed version (>=1.5.3) in the image. (leave the version number to empty if you don't know it) 52 | 53 | # Notes 54 | 55 | - Keep your language concise and simple. 56 | - Ensure key points are included, e.g. CVE number, error code, versions. 57 | - Relatable analogies should help in visualizing the problem and solution. 58 | - Ensure explanations are self-contained, enough for newcomers without previous technical exposure to understand. 59 | ` 60 | 61 | // AnalysisFlow runs a workflow to analyze Kubernetes issues and provide solutions in a human-readable format. 62 | func AnalysisFlow(model string, manifest string, verbose bool) (string, error) { 63 | analysisWorkflow := &swarm.SimpleFlow{ 64 | Name: "analysis-workflow", 65 | Model: model, 66 | MaxTurns: 30, 67 | Verbose: verbose, 68 | System: "You are an expert on Kubernetes helping user to analyze issues and provide solutions.", 69 | Steps: []swarm.SimpleFlowStep{ 70 | { 71 | Name: "analyze", 72 | Instructions: analysisPrompt, 73 | Inputs: map[string]interface{}{ 74 | "k8s_manifest": manifest, 75 | }, 76 | Functions: []swarm.AgentFunction{kubectlFunc}, 77 | }, 78 | }, 79 | } 80 | 81 | // Create OpenAI client 82 | client, err := NewSwarm() 83 | if err != nil { 84 | fmt.Printf("Failed to create client: %v\n", err) 85 | os.Exit(1) 86 | } 87 | 88 | // Initialize and run workflow 89 | analysisWorkflow.Initialize() 90 | result, _, err := analysisWorkflow.Run(context.Background(), client) 91 | if err != nil { 92 | return "", err 93 | } 94 | 95 | return result, nil 96 | } 97 | -------------------------------------------------------------------------------- /pkg/workflows/audit.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package workflows 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/feiskyer/swarm-go" 24 | ) 25 | 26 | const auditPrompt = `Conduct a structured security audit of a Kubernetes environment using a Chain of Thought (CoT) approach, ensuring each technical step is clearly connected to solutions with easy-to-understand explanations. 27 | 28 | ## Plan of Action 29 | 30 | **1. Security Auditing:** 31 | - **Retrieve Pod Configuration:** 32 | - Use "kubectl get -n {namespace} pod {pod} -o yaml" to obtain pod YAML configuration. 33 | - **Explain YAML:** 34 | - Breakdown what YAML is and its importance in understanding a pod's security posture, using analogies for clarity. 35 | 36 | - **Analyze YAML for Misconfigurations:** 37 | - Look for common security misconfigurations or risky settings within the YAML. 38 | - Connect issues to relatable concepts for non-technical users (e.g., likening insecure settings to an unlocked door). 39 | 40 | **2. Vulnerability Scanning:** 41 | - **Extract and Scan Image:** 42 | - Extract the container image from the YAML configuration obtained during last step. 43 | - Perform a scan using "trivy image <image>". 44 | - Summerize Vulnerability Scans results with CVE numbers, severity, and descriptions. 45 | 46 | **3. Issue Identification and Solution Formulation:** 47 | - Document each issue clearly and concisely. 48 | - Provide the recommendations to fix each issue. 49 | 50 | ## Provide the output in structured markdown, using clear and concise language. 51 | 52 | Example output: 53 | 54 | ## 1. <title of the issue or potential problem> 55 | 56 | - **Findings**: The YAML configuration doesn't specify the memory limit for the pod. 57 | - **How to resolve**: Set memory limit in Pod spec. 58 | 59 | ## 2. HIGH Severity: CVE-2024-10963 60 | 61 | - **Findings**: The Pod is running with CVE pam: Improper Hostname Interpretation in pam_access Leads to Access Control Bypass. 62 | - **How to resolve**: Update package libpam-modules to fixed version (>=1.5.3) in the image. (leave the version number to empty if you don't know it) 63 | 64 | # Notes 65 | 66 | - Keep your language concise and simple. 67 | - Ensure key points are included, e.g. CVE number, error code, versions. 68 | - Relatable analogies should help in visualizing the problem and solution. 69 | - Ensure explanations are self-contained, enough for newcomers without previous technical exposure to understand. 70 | ` 71 | 72 | // AuditFlow conducts a structured security audit of a Kubernetes Pod. 73 | func AuditFlow(model string, namespace string, name string, verbose bool) (string, error) { 74 | auditWorkflow := &swarm.SimpleFlow{ 75 | Name: "audit-workflow", 76 | Model: model, 77 | MaxTurns: 30, 78 | Verbose: verbose, 79 | System: "You are an expert on Kubernetes helping user to audit the security issues for a given Pod.", 80 | Steps: []swarm.SimpleFlowStep{ 81 | { 82 | Name: "audit", 83 | Instructions: auditPrompt, 84 | Inputs: map[string]interface{}{ 85 | "pod_namespace": namespace, 86 | "pod_name": name, 87 | }, 88 | Functions: []swarm.AgentFunction{trivyFunc, kubectlFunc}, 89 | }, 90 | }, 91 | } 92 | 93 | // Create OpenAI client 94 | client, err := NewSwarm() 95 | if err != nil { 96 | fmt.Printf("Failed to create client: %v\n", err) 97 | os.Exit(1) 98 | } 99 | 100 | // Initialize and run workflow 101 | auditWorkflow.Initialize() 102 | result, _, err := auditWorkflow.Run(context.Background(), client) 103 | if err != nil { 104 | return "", err 105 | } 106 | 107 | return result, nil 108 | } 109 | -------------------------------------------------------------------------------- /pkg/workflows/generate.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package workflows 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/feiskyer/swarm-go" 24 | ) 25 | 26 | const generatePrompt = `As a skilled technical specialist in Kubernetes and cloud-native technologies, your task is to create Kubernetes YAML manifests by following these detailed steps: 27 | 28 | 1. Review the instructions provided to generate Kubernetes YAML manifests. Ensure that these manifests adhere to current security protocols and best practices. If an instruction lacks a specific image, choose the most commonly used one from reputable sources. 29 | 2. Utilize your expertise to scrutinize the YAML manifests. Conduct a thorough step-by-step analysis to identify any issues. Resolve these issues, ensuring the YAML manifests are accurate and secure. 30 | 3. After fixing and verifying the manifests, compile them in their raw form. For multiple YAML files, use '---' as a separator. 31 | 32 | # Steps 33 | 34 | 1. **Understand the Instructions:** 35 | - Evaluate the intended use and environment for each manifest as per instructions provided. 36 | 37 | 2. **Security and Best Practices Assessment:** 38 | - Assess the security aspects of each component, ensuring alignment with current standards and best practices. 39 | - Perform a comprehensive analysis of the YAML structure and configurations. 40 | 41 | 3. **Document and Address Discrepancies:** 42 | - Document and justify any discrepancies or issues you find, in a sequential manner. 43 | - Implement robust solutions that enhance the manifests' performance and security, utilizing best practices and recommended images. 44 | 45 | 4. **Finalize the YAML Manifests:** 46 | - Ensure the final manifests are syntactically correct, properly formatted, and deployment-ready. 47 | 48 | # Output Format 49 | 50 | - Present only the final YAML manifests in raw format, separated by "---" for multiple files. 51 | - Exclude any comments or additional annotations within the YAML files. 52 | 53 | Your expertise ensures these manifests are not only functional but also compliant with the highest standards in Kubernetes and cloud-native technologies.` 54 | 55 | // GeneratorFlow runs a workflow to generate Kubernetes YAML manifests based on the provided instructions. 56 | func GeneratorFlow(model string, instructions string, verbose bool) (string, error) { 57 | generatorWorkflow := &swarm.SimpleFlow{ 58 | Name: "generator-workflow", 59 | Model: model, 60 | MaxTurns: 30, 61 | Verbose: verbose, 62 | System: "You are an expert on Kubernetes helping user to generate Kubernetes YAML manifests.", 63 | Steps: []swarm.SimpleFlowStep{ 64 | { 65 | Name: "generator", 66 | Instructions: generatePrompt, 67 | Inputs: map[string]interface{}{ 68 | "instructions": instructions, 69 | }, 70 | }, 71 | }, 72 | } 73 | 74 | // Create OpenAI client 75 | client, err := NewSwarm() 76 | if err != nil { 77 | fmt.Printf("Failed to create client: %v\n", err) 78 | os.Exit(1) 79 | } 80 | 81 | // Initialize and run workflow 82 | generatorWorkflow.Initialize() 83 | result, _, err := generatorWorkflow.Run(context.Background(), client) 84 | if err != nil { 85 | return "", err 86 | } 87 | 88 | return result, nil 89 | } 90 | -------------------------------------------------------------------------------- /pkg/workflows/prompts.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package workflows 17 | 18 | const outputPrompt = ` 19 | 20 | # Output Format 21 | 22 | Your final output must strictly adhere to this JSON structure: 23 | 24 | { 25 | "question": "<input question>", 26 | "thought": "<your detailed thought process>", 27 | "steps": [ 28 | { 29 | "name": "<descriptive name of step 1>", 30 | "description": "<detailed description of what this step will do>", 31 | "action": { 32 | "name": "<tool to call for current step>", 33 | "input": "<exact command or script with all required context>" 34 | }, 35 | "status": "<one of: pending, in_progress, completed, failed>", 36 | "observation": "<result from the tool call of the action, to be filled in after action execution>", 37 | }, 38 | { 39 | "name": "<descriptive name of step 2>", 40 | "description": "<detailed description of what this step will do>", 41 | "action": { 42 | "name": "<tool to call for current step>", 43 | "input": "<exact command or script with all required context>" 44 | }, 45 | "observation": "<result from the tool call of the action, to be filled in after action execution>", 46 | "status": "<status of this step>" 47 | }, 48 | ...more steps... 49 | ], 50 | "current_step_index": <index of the current step being executed, zero-based>, 51 | "final_answer": "<your final findings; only fill this when no further actions are required>" 52 | } 53 | 54 | # Important: 55 | - Always use function calls via the 'action' field for tool invocations. NEVER output plain text instructions for the user to run a command manually. 56 | - Ensure that the chain-of-thought (fields 'thought' and 'steps') is clear and concise, leading logically to the tool call if needed. 57 | - The final answer should only be provided when all necessary tool invocations have been completed and the issue is fully resolved. 58 | - The 'steps' array should contain ALL steps needed to solve the problem, with appropriate status updates as you progress (simulated data shouldn't be used here). 59 | - NEVER remove steps from the 'steps' array once added, only update their status. 60 | - Initial step statuses should be "pending", change to "in_progress" when starting a step, and then "completed" or "failed" when done. 61 | ` 62 | 63 | const kubectlManual = ` 64 | 65 | # Kubectl manual 66 | 67 | kubectl get services # List all services in the namespace 68 | kubectl get pods --all-namespaces # List all pods in all namespaces 69 | kubectl get pods -o wide # List all pods in the current namespace, with more details 70 | kubectl get deployment my-dep # List a particular deployment 71 | kubectl get pods # List all pods in the namespace 72 | kubectl get pod my-pod -o yaml # Get a pod's YAML 73 | 74 | // List pods Sorted by Restart Count 75 | kubectl get pods --sort-by='.status.containerStatuses[0].restartCount' 76 | // List PersistentVolumes sorted by capacity 77 | kubectl get pv --sort-by=.spec.capacity.storage 78 | // All images running in a cluster 79 | // List all warning events 80 | kubectl events --types=Warning 81 | kubectl get pods -A -o=custom-columns='DATA:spec.containers[*].image' 82 | // All images running in namespace: default, grouped by Pod 83 | kubectl get pods --namespace default --output=custom-columns="NAME:.metadata.name,IMAGE:.spec.containers[*].image" 84 | // dump Pod logs for a Deployment (single-container case) 85 | kubectl logs deploy/my-deployment 86 | // dump Pod logs for a Deployment (multi-container case) 87 | kubectl logs deploy/my-deployment -c my-container 88 | // dump pod logs (stdout, DO NOT USE -f) 89 | kubectl logs my-pod 90 | // dump pod container logs (stdout, multi-container case, DO NOT USE -f) 91 | kubectl logs my-pod -c my-container 92 | // Partially update a node 93 | kubectl patch node k8s-node-1 -p '{"spec":{"unschedulable":true}}' 94 | // Update a container's image; spec.containers[*].name is required because it's a merge key 95 | kubectl patch pod valid-pod -p '{"spec":{"containers":[{"name":"kubernetes-serve-hostname","image":"new image"}]}}' 96 | // Update a container's image using a json patch with positional arrays 97 | kubectl patch pod valid-pod --type='json' -p='[{"op": "replace", "path": "/spec/containers/0/image", "value":"new image"}]' 98 | // Disable a deployment livenessProbe using a json patch with positional arrays 99 | kubectl patch deployment valid-deployment --type json -p='[{"op": "remove", "path": "/spec/template/spec/containers/0/livenessProbe"}]' 100 | // Add a new element to a positional array 101 | kubectl patch sa default --type='json' -p='[{"op": "add", "path": "/secrets/1", "value": {"name": "whatever" } }]' 102 | // Update a deployment's replica count by patching its scale subresource 103 | kubectl patch deployment nginx-deployment --subresource='scale' --type='merge' -p '{"spec":{"replicas":2}}' 104 | // Rolling update "www" containers of "frontend" deployment, updating the image 105 | kubectl set image deployment/frontend www=image:v2 106 | 107 | ` 108 | 109 | const planPrompt = ` 110 | You are an expert Planning Agent tasked with solving Kubernetes and cloud-native networking problems efficiently through structured plans. 111 | Your job is to: 112 | 113 | 1. Analyze the user's instruction and their intent carefully to understand the issue or goal. 114 | 2. Create a clear and actionable plan to achieve the goal and user intent. Document this plan in the 'steps' field as a structured array. 115 | 3. For any troubleshooting step that requires tool execution, include a function call by populating the 'action' field with: 116 | - 'name': one of supported tools below. 117 | - 'input': the exact command or script, including any required context (e.g., raw YAML, error logs, image name). 118 | 4. Track progress and adapt plans when necessary 119 | 5. Do not set the 'final_answer' field when a tool call is pending; only set 'final_answer' when no further tool calls are required. 120 | 121 | 122 | # Available Tools 123 | 124 | {{TOOLS}} 125 | 126 | ` + outputPrompt 127 | 128 | const nextStepPrompt = `You are an expert Planning Agent tasked with solving Kubernetes and cloud-native networking problems efficiently through structured plans. 129 | Your job is to: 130 | 131 | 1. Review the tool execution results and the current plan. 132 | 2. Fix the tool parameters if the tool call failed (e.g. refer the kubectl manual to fix the kubectl command). 133 | 3. Determine if the plan is sufficient, or if it needs refinement. 134 | 4. Choose the most efficient path forward and update the plan accordingly (e.g. update the action inputs for next step or add new steps). 135 | 5. If the task is complete, set 'final_answer' right away. 136 | 137 | Be concise in your reasoning, then select the appropriate tool or action. 138 | ` + kubectlManual + outputPrompt 139 | 140 | const reactPrompt = `As a technical expert in Kubernetes and cloud-native networking, you are required to help user to resolve their problem using a detailed chain-of-thought methodology. 141 | Your responses must follow a strict JSON format and simulate tool execution via function calls without instructing the user to manually run any commands. 142 | 143 | # Available Tools 144 | 145 | {{TOOLS}} 146 | 147 | # Guidelines 148 | 149 | 1. Analyze the user's instruction and their intent carefully to understand the issue or goal. 150 | 2. Formulate a detailed, step-by-step plan to achieve the goal and user intent. Document this plan in the 'steps' field as a structured array. 151 | 3. For any troubleshooting step that requires tool execution, include a function call by populating the 'action' field with: 152 | - 'name': one of available tools. 153 | - 'input': the exact command or script, including any required context (e.g., raw YAML, error logs, image name). 154 | 4. DO NOT instruct the user to manually run any commands. All tool calls must be performed by the assistant through the 'action' field. 155 | 5. After a tool is invoked, analyze its result (which will be provided in the 'observation' field) and update your chain-of-thought accordingly. 156 | 6. Do not set the 'final_answer' field when a tool call is pending; only set 'final_answer' when no further tool calls are required. 157 | 7. Maintain a clear and concise chain-of-thought in the 'thought' field. Include a detailed, step-by-step process in the 'steps' field. 158 | 8. Your entire response must be a valid JSON object with exactly the following keys: 'question', 'thought', 'steps', 'current_step_index', 'action', 'observation', and 'final_answer'. Do not include any additional text or markdown formatting. 159 | ` + outputPrompt 160 | -------------------------------------------------------------------------------- /pkg/workflows/reactflow.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package workflows provides the ReAct (Reason + Act) workflow for AI assistants. 18 | package workflows 19 | 20 | import ( 21 | "context" 22 | "encoding/json" 23 | "fmt" 24 | "regexp" 25 | "strings" 26 | "time" 27 | 28 | "github.com/fatih/color" 29 | "github.com/feiskyer/kube-copilot/pkg/llms" 30 | "github.com/feiskyer/kube-copilot/pkg/tools" 31 | "github.com/feiskyer/swarm-go" 32 | ) 33 | 34 | // ReactAction is the JSON format for the react action. 35 | type ReactAction struct { 36 | Question string `json:"question"` 37 | Thought string `json:"thought,omitempty"` 38 | Steps []StepDetail `json:"steps,omitempty"` 39 | CurrentStepIndex int `json:"current_step_index,omitempty"` 40 | FinalAnswer string `json:"final_answer,omitempty"` 41 | } 42 | 43 | // StepDetail represents a detailed step in the plan 44 | type StepDetail struct { 45 | Name string `json:"name"` 46 | Description string `json:"description"` 47 | Action struct { 48 | Name string `json:"name"` 49 | Input interface{} `json:"input"` 50 | } `json:"action,omitempty"` 51 | Observation string `json:"observation,omitempty"` 52 | Status string `json:"status"` // pending, in_progress, completed, failed 53 | } 54 | 55 | // PlanTracker keeps track of the execution plan and its progress 56 | type PlanTracker struct { 57 | PlanID string `json:"plan_id"` 58 | Steps []StepDetail `json:"steps"` 59 | CurrentStep int `json:"current_step"` 60 | LastError string `json:"last_error,omitempty"` 61 | FinalAnswer string `json:"final_answer,omitempty"` 62 | HasValidPlan bool `json:"has_valid_plan"` 63 | ExecutionTimeout time.Duration `json:"execution_timeout"` 64 | } 65 | 66 | // NewPlanTracker creates a new plan tracker 67 | func NewPlanTracker() *PlanTracker { 68 | return &PlanTracker{ 69 | PlanID: fmt.Sprintf("plan_%d", time.Now().Unix()), 70 | Steps: []StepDetail{}, 71 | CurrentStep: 0, 72 | ExecutionTimeout: 30 * time.Minute, 73 | } 74 | } 75 | 76 | // ParsePlan parses the plan string into structured steps 77 | func (pt *PlanTracker) ParsePlan(planStr string) error { 78 | if planStr == "" { 79 | return fmt.Errorf("empty plan string") 80 | } 81 | 82 | lines := strings.Split(planStr, "\n") 83 | steps := []StepDetail{} 84 | 85 | stepPattern := regexp.MustCompile(`^(\d+\.|\*|Step \d+:|[-•])\s*(.+)$`) 86 | 87 | for i, line := range lines { 88 | line = strings.TrimSpace(line) 89 | if line == "" { 90 | continue 91 | } 92 | 93 | matches := stepPattern.FindStringSubmatch(line) 94 | if len(matches) >= 3 { 95 | description := strings.TrimSpace(matches[2]) 96 | steps = append(steps, StepDetail{ 97 | Name: fmt.Sprintf("Step %d", i+1), 98 | Description: description, 99 | Status: "pending", 100 | }) 101 | } 102 | } 103 | 104 | if len(steps) == 0 { 105 | // Fallback: If no steps were found, try to extract sentences as steps 106 | sentencePattern := regexp.MustCompile(`[.!?]+\s+|\n+|$`) 107 | sentences := sentencePattern.Split(planStr, -1) 108 | 109 | for i, sentence := range sentences { 110 | sentence = strings.TrimSpace(sentence) 111 | if sentence != "" && len(sentence) > 10 { // Minimum length for a sentence to be a step 112 | steps = append(steps, StepDetail{ 113 | Name: fmt.Sprintf("Step %d", i+1), 114 | Description: sentence, 115 | Status: "pending", 116 | }) 117 | } 118 | } 119 | } 120 | 121 | if len(steps) == 0 { 122 | return fmt.Errorf("no steps could be extracted from plan") 123 | } 124 | 125 | pt.Steps = steps 126 | pt.HasValidPlan = true 127 | return nil 128 | } 129 | 130 | // UpdateStepStatus updates the status of a step 131 | func (pt *PlanTracker) UpdateStepStatus(stepIndex int, status string, toolCall string, result string) { 132 | if stepIndex >= 0 && stepIndex < len(pt.Steps) { 133 | pt.Steps[stepIndex].Status = status 134 | if toolCall != "" { 135 | pt.Steps[stepIndex].Action.Name = toolCall 136 | } 137 | if result != "" { 138 | // Truncate long results to prevent memory issues 139 | pt.Steps[stepIndex].Observation = result 140 | } 141 | } 142 | } 143 | 144 | // GetCurrentStep returns the current step 145 | func (pt *PlanTracker) GetCurrentStep() *StepDetail { 146 | if pt.CurrentStep >= 0 && pt.CurrentStep < len(pt.Steps) { 147 | return &pt.Steps[pt.CurrentStep] 148 | } 149 | return nil 150 | } 151 | 152 | // MoveToNextStep moves to the next step 153 | func (pt *PlanTracker) MoveToNextStep() bool { 154 | // If we're already at the last step, we can't move forward 155 | if pt.CurrentStep >= len(pt.Steps)-1 && len(pt.Steps) > 0 { 156 | // Just mark the current (last) step as completed if not already failed 157 | if pt.Steps[pt.CurrentStep].Status != "failed" { 158 | pt.Steps[pt.CurrentStep].Status = "completed" 159 | } 160 | return false 161 | } 162 | 163 | // Track the original step index before moving 164 | originalStep := pt.CurrentStep 165 | 166 | // Mark the current step as completed if it's not already marked as failed 167 | // and only if we have steps (avoid index out of range) 168 | if len(pt.Steps) > 0 && originalStep >= 0 && originalStep < len(pt.Steps) { 169 | if pt.Steps[originalStep].Status != "failed" { 170 | pt.Steps[originalStep].Status = "completed" 171 | } 172 | } 173 | 174 | // First try to find the next pending step 175 | foundNextStep := false 176 | 177 | // Look for pending steps after the current step first 178 | for i := originalStep + 1; i < len(pt.Steps); i++ { 179 | // If step is pending, move to it 180 | if pt.Steps[i].Status == "pending" { 181 | pt.CurrentStep = i 182 | pt.Steps[i].Status = "in_progress" 183 | foundNextStep = true 184 | break 185 | } 186 | } 187 | 188 | // If no pending steps found after current, check from beginning up to current 189 | if !foundNextStep { 190 | for i := 0; i < originalStep; i++ { 191 | if pt.Steps[i].Status == "pending" { 192 | pt.CurrentStep = i 193 | pt.Steps[i].Status = "in_progress" 194 | foundNextStep = true 195 | break 196 | } 197 | } 198 | } 199 | 200 | // If still no pending steps, look for in_progress steps that aren't the current one 201 | if !foundNextStep { 202 | for i := 0; i < len(pt.Steps); i++ { 203 | if i != originalStep && pt.Steps[i].Status == "in_progress" { 204 | pt.CurrentStep = i 205 | foundNextStep = true 206 | break 207 | } 208 | } 209 | } 210 | 211 | // If we still couldn't find a pending or in_progress step, take one of two actions: 212 | if !foundNextStep { 213 | // If the original step was valid and we were simply moving to the next step in sequence, 214 | // follow the sequence 215 | if originalStep >= 0 && originalStep < len(pt.Steps)-1 { 216 | pt.CurrentStep = originalStep + 1 217 | // Only mark as in_progress if it's not already completed or failed 218 | if pt.Steps[pt.CurrentStep].Status != "completed" && pt.Steps[pt.CurrentStep].Status != "failed" { 219 | pt.Steps[pt.CurrentStep].Status = "in_progress" 220 | } 221 | return true 222 | } 223 | 224 | // If we had an invalid original step or were already at the end, 225 | // set to the last step in the plan 226 | if len(pt.Steps) > 0 { 227 | pt.CurrentStep = len(pt.Steps) - 1 228 | } else { 229 | pt.CurrentStep = 0 // Handle empty step list 230 | } 231 | return false 232 | } 233 | 234 | return true 235 | } 236 | 237 | // IsComplete returns true if all steps are completed 238 | func (pt *PlanTracker) IsComplete() bool { 239 | for _, step := range pt.Steps { 240 | if step.Status != "completed" && step.Status != "failed" { 241 | return false 242 | } 243 | } 244 | return len(pt.Steps) > 0 245 | } 246 | 247 | func formatObservationOutputs(observation string) string { 248 | if observation == "" { 249 | return "" 250 | } 251 | 252 | lines := strings.Split(observation, "\n") 253 | formattedLines := make([]string, len(lines)) 254 | for i, line := range lines { 255 | formattedLines[i] = " " + line 256 | } 257 | return strings.Join(formattedLines, "\n") 258 | } 259 | 260 | // GetPlanStatus returns a string representation of the plan status 261 | func (pt *PlanTracker) GetPlanStatus() string { 262 | var sb strings.Builder 263 | sb.WriteString(fmt.Sprintf("Plan ID: %s\n\n", pt.PlanID)) 264 | 265 | for i, step := range pt.Steps { 266 | statusSymbol := "⏳" 267 | if step.Status == "completed" { 268 | statusSymbol = "✅" 269 | } else if step.Status == "in_progress" { 270 | statusSymbol = "🔄" 271 | } else if step.Status == "failed" { 272 | statusSymbol = "❌" 273 | } 274 | 275 | sb.WriteString(fmt.Sprintf("%s Step %d: %s [%s]\n", statusSymbol, i+1, step.Description, step.Status)) 276 | if step.Observation != "" { 277 | formattedObservation := formatObservationOutputs(step.Observation) 278 | sb.WriteString(fmt.Sprintf(" Observation:\n%s\n", formattedObservation)) 279 | } 280 | } 281 | 282 | return sb.String() 283 | } 284 | 285 | // ParsePlanFromReactAction parses the plan from ReactAction 286 | func (pt *PlanTracker) ParsePlanFromReactAction(reactAction *ReactAction) error { 287 | if reactAction == nil { 288 | return fmt.Errorf("nil ReactAction provided") 289 | } 290 | 291 | if len(reactAction.Steps) == 0 { 292 | // Fallback to parsing from thought if steps is empty 293 | if reactAction.Thought != "" { 294 | return pt.ParsePlan(reactAction.Thought) 295 | } 296 | return fmt.Errorf("no steps found in ReactAction") 297 | } 298 | 299 | steps := []StepDetail{} 300 | 301 | for _, step := range reactAction.Steps { 302 | // Ensure status is set to a valid value 303 | status := step.Status 304 | if status == "" { 305 | status = "pending" 306 | } 307 | 308 | // Copy step with proper status 309 | steps = append(steps, StepDetail{ 310 | Name: step.Name, 311 | Description: step.Description, 312 | Status: status, 313 | Action: step.Action, 314 | Observation: step.Observation, 315 | }) 316 | } 317 | 318 | if len(steps) == 0 { 319 | return fmt.Errorf("no valid steps could be extracted") 320 | } 321 | 322 | pt.Steps = steps 323 | pt.HasValidPlan = true 324 | 325 | // If current step index is specified and valid, use it 326 | if reactAction.CurrentStepIndex >= 0 && reactAction.CurrentStepIndex < len(steps) { 327 | pt.CurrentStep = reactAction.CurrentStepIndex 328 | // Make sure the current step is marked as in_progress 329 | if pt.Steps[pt.CurrentStep].Status == "pending" { 330 | pt.Steps[pt.CurrentStep].Status = "in_progress" 331 | } 332 | } else { 333 | // Default to starting at first step 334 | pt.CurrentStep = 0 335 | // Mark first step as in_progress 336 | if len(pt.Steps) > 0 && pt.Steps[0].Status == "pending" { 337 | pt.Steps[0].Status = "in_progress" 338 | } 339 | } 340 | 341 | // Store final answer if provided 342 | if reactAction.FinalAnswer != "" { 343 | pt.FinalAnswer = reactAction.FinalAnswer 344 | } 345 | 346 | return nil 347 | } 348 | 349 | // SyncStepsWithReactAction synchronizes steps from ReactAction with our tracker 350 | func (pt *PlanTracker) SyncStepsWithReactAction(reactAction *ReactAction) { 351 | if reactAction == nil || len(reactAction.Steps) == 0 { 352 | return 353 | } 354 | 355 | // First, ensure our step arrays are of the same length 356 | // If reactAction has more steps, copy them to our tracker 357 | for i := len(pt.Steps); i < len(reactAction.Steps); i++ { 358 | pt.Steps = append(pt.Steps, reactAction.Steps[i]) 359 | } 360 | 361 | // Record the state of steps before updating 362 | completedSteps := make(map[int]bool) 363 | failedSteps := make(map[int]bool) 364 | for i, step := range pt.Steps { 365 | if step.Status == "completed" { 366 | completedSteps[i] = true 367 | } else if step.Status == "failed" { 368 | failedSteps[i] = true 369 | } 370 | } 371 | 372 | // Update existing steps - but preserve completion status 373 | for i, step := range reactAction.Steps { 374 | if i < len(pt.Steps) { 375 | // Keep track of original status for comparison 376 | originalStatus := pt.Steps[i].Status 377 | 378 | // Don't override completed or failed status from our tracker 379 | if completedSteps[i] || failedSteps[i] { 380 | // Only copy additional information without changing status 381 | if pt.Steps[i].Action.Name == "" && step.Action.Name != "" { 382 | pt.Steps[i].Action = step.Action 383 | } 384 | 385 | if pt.Steps[i].Description == "" && step.Description != "" { 386 | pt.Steps[i].Description = step.Description 387 | } 388 | 389 | if pt.Steps[i].Name == "" && step.Name != "" { 390 | pt.Steps[i].Name = step.Name 391 | } 392 | } else { 393 | // For steps that aren't completed or failed, sync all data 394 | pt.Steps[i].Name = step.Name 395 | pt.Steps[i].Description = step.Description 396 | 397 | // Only update action if it has content 398 | if step.Action.Name != "" { 399 | pt.Steps[i].Action = step.Action 400 | } 401 | 402 | // Only update status if it's not empty and would be a valid transition 403 | if step.Status != "" { 404 | // Don't allow pending→completed without going through in_progress 405 | if !(originalStatus == "pending" && step.Status == "completed") { 406 | pt.Steps[i].Status = step.Status 407 | } 408 | } 409 | 410 | // Only update observation if not empty 411 | if step.Observation != "" { 412 | pt.Steps[i].Observation = step.Observation 413 | } 414 | } 415 | } 416 | } 417 | 418 | // Sync current step index if it's within bounds 419 | if reactAction.CurrentStepIndex >= 0 && reactAction.CurrentStepIndex < len(pt.Steps) { 420 | // We don't want to move backward from a completed step to an earlier step 421 | // UNLESS that earlier step still needs execution (e.g., has a new action) 422 | shouldUpdateCurrentStep := false 423 | 424 | // Always update if moving forward 425 | if reactAction.CurrentStepIndex > pt.CurrentStep { 426 | shouldUpdateCurrentStep = true 427 | } else if reactAction.CurrentStepIndex < pt.CurrentStep { 428 | // Only move backwards if current step is completed/failed AND 429 | // the target step is not completed/failed AND has an action 430 | if (pt.Steps[pt.CurrentStep].Status == "completed" || 431 | pt.Steps[pt.CurrentStep].Status == "failed") && 432 | (pt.Steps[reactAction.CurrentStepIndex].Status != "completed" && 433 | pt.Steps[reactAction.CurrentStepIndex].Status != "failed") && 434 | pt.Steps[reactAction.CurrentStepIndex].Action.Name != "" { 435 | shouldUpdateCurrentStep = true 436 | } 437 | } else { 438 | // Same step index, no update needed 439 | } 440 | 441 | if shouldUpdateCurrentStep { 442 | pt.CurrentStep = reactAction.CurrentStepIndex 443 | 444 | // Ensure the current step is marked as in_progress 445 | if pt.Steps[pt.CurrentStep].Status == "pending" { 446 | pt.Steps[pt.CurrentStep].Status = "in_progress" 447 | } 448 | } 449 | } 450 | } 451 | 452 | // ReActFlow orchestrates the ReAct (Reason + Act) workflow 453 | type ReActFlow struct { 454 | Model string 455 | Instructions string 456 | Verbose bool 457 | MaxIterations int 458 | ToolPrompt string 459 | PlanTracker *PlanTracker 460 | Client *swarm.Swarm 461 | ChatHistory interface{} 462 | } 463 | 464 | // NewReActFlow creates a new ReActFlow instance 465 | func NewReActFlow(model string, instructions string, toolPrompt string, verbose bool, maxIterations int) (*ReActFlow, error) { 466 | // Create OpenAI client 467 | client, err := NewSwarm() 468 | if err != nil { 469 | return nil, fmt.Errorf("failed to initialize client: %v", err) 470 | } 471 | 472 | return &ReActFlow{ 473 | Model: model, 474 | Instructions: instructions, 475 | MaxIterations: maxIterations, 476 | PlanTracker: NewPlanTracker(), 477 | ToolPrompt: toolPrompt, 478 | Verbose: verbose, 479 | Client: client, 480 | ChatHistory: nil, 481 | }, nil 482 | } 483 | 484 | // Run executes the complete ReAct workflow 485 | func (r *ReActFlow) Run() (string, error) { 486 | // Set a reasonable default response in case of early failures 487 | defaultResponse := "I was unable to complete the task due to technical issues. Please try again or simplify your request." 488 | 489 | // Set a context with timeout for the entire flow 490 | ctx, cancel := context.WithTimeout(context.Background(), 60*time.Minute) 491 | defer cancel() 492 | 493 | // Step 1: Create initial plan 494 | if err := r.Plan(ctx); err != nil { 495 | r.PlanTracker.LastError = fmt.Sprintf("Planning phase failed: %v", err) 496 | return defaultResponse, err 497 | } 498 | 499 | // Step 2: Execute plan steps in a loop 500 | return r.ExecutePlan(ctx) 501 | } 502 | 503 | // Plan creates the initial plan for solving the problem 504 | func (r *ReActFlow) Plan(ctx context.Context) error { 505 | if r.Verbose { 506 | color.Blue("Planning phase: creating a detailed plan\n") 507 | } 508 | 509 | // Initialize the first step to create a plan 510 | reactFlow := &swarm.SimpleFlow{ 511 | Name: "plan", 512 | Model: r.Model, 513 | MaxTurns: 30, 514 | JSONMode: true, 515 | Steps: []swarm.SimpleFlowStep{ 516 | { 517 | Name: "plan-step", 518 | Instructions: strings.Replace(planPrompt, "{{TOOLS}}", r.ToolPrompt, 1), 519 | Inputs: map[string]interface{}{ 520 | "instructions": fmt.Sprintf("First, create a clear and actionable step-by-step plan to solve this problem: %s", r.Instructions), 521 | }, 522 | }, 523 | }, 524 | } 525 | 526 | // Initialize and run workflow 527 | reactFlow.Initialize() 528 | 529 | result, chatHistory, err := reactFlow.Run(ctx, r.Client) 530 | if err != nil { 531 | return err 532 | } 533 | 534 | // Save chat history for future steps 535 | r.ChatHistory = limitChatHistory(chatHistory, 20) 536 | 537 | if r.Verbose { 538 | color.Cyan("Planning phase response:\n%s\n\n", result) 539 | } 540 | 541 | // Parse the initial plan 542 | return r.ParsePlanResult(result) 543 | } 544 | 545 | func extractReactAction(text string, reactAction *ReactAction) error { 546 | text = strings.TrimSpace(text) 547 | 548 | // For responses with prefix ```json 549 | if strings.HasPrefix(text, "```") { 550 | text = strings.TrimPrefix(text, "```") 551 | text = strings.TrimPrefix(text, "json") 552 | text = strings.TrimSuffix(text, "```") 553 | } 554 | 555 | // For responses with prefix <think> 556 | if strings.HasPrefix(text, "<think>") { 557 | text = strings.Split(text, "</think>")[1] 558 | text = strings.TrimSpace(text) 559 | } 560 | 561 | if err := json.Unmarshal([]byte(text), reactAction); err != nil { 562 | return fmt.Errorf("failed to parse LLM response to ReactAction: %v", err) 563 | } 564 | return nil 565 | } 566 | 567 | // ParsePlanResult parses the planning phase result 568 | func (r *ReActFlow) ParsePlanResult(result string) error { 569 | var reactAction ReactAction 570 | if err := extractReactAction(result, &reactAction); err != nil { 571 | if r.Verbose { 572 | color.Red("Unable to parse response as JSON: %v\n", err) 573 | } 574 | 575 | if !r.PlanTracker.HasValidPlan { 576 | return fmt.Errorf("couldn't create a proper plan") 577 | } 578 | } else { 579 | // Parse plan from the structured ReactAction 580 | err := r.PlanTracker.ParsePlanFromReactAction(&reactAction) 581 | if err != nil && r.Verbose { 582 | color.Red("Failed to parse plan from ReactAction: %v\n", err) 583 | 584 | // Fallback: Try to parse from Thought field if it exists (backwards compatibility) 585 | if reactAction.Thought != "" { 586 | err = r.PlanTracker.ParsePlan(reactAction.Thought) 587 | if err != nil && r.Verbose { 588 | color.Red("Failed to parse plan from Thought: %v\n", err) 589 | } 590 | } 591 | } 592 | 593 | // Check for final answer 594 | if reactAction.FinalAnswer != "" { 595 | r.PlanTracker.FinalAnswer = reactAction.FinalAnswer 596 | } 597 | } 598 | 599 | // Verify that we have a valid plan 600 | if !r.PlanTracker.HasValidPlan || len(r.PlanTracker.Steps) == 0 { 601 | if r.Verbose { 602 | color.Red("No valid plan could be created\n") 603 | } 604 | return fmt.Errorf("no valid plan could be created") 605 | } 606 | 607 | if r.Verbose { 608 | color.Cyan("Extracted plan with %d steps\n", len(r.PlanTracker.Steps)) 609 | color.Cyan("Plan status:\n%s\n", r.PlanTracker.GetPlanStatus()) 610 | } 611 | 612 | return nil 613 | } 614 | 615 | // ExecutePlan runs the execution phase of the workflow 616 | func (r *ReActFlow) ExecutePlan(ctx context.Context) (string, error) { 617 | // Make sure we have a valid plan 618 | if len(r.PlanTracker.Steps) == 0 || !r.PlanTracker.HasValidPlan { 619 | return "", fmt.Errorf("no valid plan to execute") 620 | } 621 | 622 | // Set execution timeout 623 | execCtx, execCancel := context.WithTimeout(ctx, r.PlanTracker.ExecutionTimeout) 624 | defer execCancel() 625 | 626 | // Keep track of iterations 627 | iteration := 0 628 | 629 | // Track step stability (detect oscillation) 630 | previousStepIndices := make([]int, 3) // track last 3 steps to detect oscillation 631 | for i := range previousStepIndices { 632 | previousStepIndices[i] = -1 // initialize with invalid indices 633 | } 634 | 635 | // Initialize first step if needed 636 | if r.PlanTracker.CurrentStep < 0 || r.PlanTracker.CurrentStep >= len(r.PlanTracker.Steps) { 637 | r.PlanTracker.CurrentStep = 0 638 | r.PlanTracker.Steps[0].Status = "in_progress" 639 | } 640 | 641 | for { 642 | // Check if we've exceeded the maximum number of iterations 643 | if iteration >= r.MaxIterations { 644 | if r.Verbose { 645 | color.Yellow("Reached maximum number of iterations (%d)\n", r.MaxIterations) 646 | } 647 | break 648 | } 649 | 650 | // Check if we're out of time 651 | if execCtx.Err() != nil { 652 | return "", fmt.Errorf("execution timed out after %s", r.PlanTracker.ExecutionTimeout) 653 | } 654 | 655 | // Check for step oscillation (repeating between the same steps) 656 | oscillationDetected := false 657 | if iteration >= 3 { 658 | // Shift previous indices 659 | previousStepIndices[0] = previousStepIndices[1] 660 | previousStepIndices[1] = previousStepIndices[2] 661 | previousStepIndices[2] = r.PlanTracker.CurrentStep 662 | 663 | // Check for A-B-A pattern 664 | if previousStepIndices[0] == previousStepIndices[2] && 665 | previousStepIndices[0] != previousStepIndices[1] && 666 | previousStepIndices[0] != -1 { 667 | oscillationDetected = true 668 | if r.Verbose { 669 | color.Red("Oscillation detected between steps %d and %d. Forcing forward progress.", 670 | previousStepIndices[0]+1, previousStepIndices[1]+1) 671 | } 672 | 673 | // Force mark the current step as completed to break the cycle 674 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "completed", "", 675 | "Automatic completion to break oscillation") 676 | } 677 | } else { 678 | // For early iterations, just record the step index 679 | previousStepIndices[iteration] = r.PlanTracker.CurrentStep 680 | } 681 | 682 | // Check if the plan is complete - all steps must be completed or failed 683 | isComplete := true 684 | for i, step := range r.PlanTracker.Steps { 685 | // If any step isn't completed or failed, the plan isn't complete 686 | if step.Status != "completed" && step.Status != "failed" { 687 | isComplete = false 688 | 689 | // If we find a step with pending or in_progress status that comes before 690 | // our current step, we should consider moving back to execute it 691 | if i < r.PlanTracker.CurrentStep && !oscillationDetected { 692 | // Only move back if we have reason to (it has an action) 693 | if step.Action.Name != "" { 694 | if r.Verbose { 695 | color.Yellow("Found earlier step %d in %s state with action, moving back to execute it", 696 | i+1, step.Status) 697 | } 698 | r.PlanTracker.CurrentStep = i 699 | r.PlanTracker.Steps[i].Status = "in_progress" 700 | break 701 | } 702 | } 703 | } 704 | } 705 | 706 | if isComplete { 707 | if r.Verbose { 708 | color.Green("Plan execution complete - all steps are completed or failed\n") 709 | } 710 | break 711 | } 712 | 713 | // Validate current step index is within bounds 714 | if r.PlanTracker.CurrentStep < 0 || r.PlanTracker.CurrentStep >= len(r.PlanTracker.Steps) { 715 | if r.Verbose { 716 | color.Red("Invalid current step index %d. Resetting to first incomplete step.", 717 | r.PlanTracker.CurrentStep) 718 | } 719 | 720 | // Find the first non-completed step 721 | for i, step := range r.PlanTracker.Steps { 722 | if step.Status != "completed" && step.Status != "failed" { 723 | r.PlanTracker.CurrentStep = i 724 | break 725 | } 726 | } 727 | 728 | // If all steps are complete/failed but we didn't detect it above, force to last step 729 | if r.PlanTracker.CurrentStep < 0 || r.PlanTracker.CurrentStep >= len(r.PlanTracker.Steps) { 730 | r.PlanTracker.CurrentStep = len(r.PlanTracker.Steps) - 1 731 | } 732 | } 733 | 734 | // Get the current step 735 | currentStep := r.PlanTracker.GetCurrentStep() 736 | if currentStep == nil { 737 | return "", fmt.Errorf("invalid current step") 738 | } 739 | 740 | // Mark the current step as in progress if it's pending 741 | if currentStep.Status == "pending" { 742 | currentStep.Status = "in_progress" 743 | } 744 | 745 | if r.Verbose { 746 | color.Blue("[Step %d: %s] %s [%s]\n", r.PlanTracker.CurrentStep+1, 747 | currentStep.Name, currentStep.Description, currentStep.Status) 748 | } 749 | 750 | if err := r.ExecuteStep(execCtx, iteration, currentStep); err != nil { 751 | r.PlanTracker.LastError = err.Error() 752 | // Mark the step as failed and try to move to the next step 753 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "failed", "", err.Error()) 754 | // If we can't move to the next step, we're done 755 | if !r.PlanTracker.MoveToNextStep() { 756 | if r.PlanTracker.FinalAnswer != "" { 757 | // If we have a final answer, consider the plan successful anyway 758 | break 759 | } 760 | return "", fmt.Errorf("plan execution failed: %v", err) 761 | } 762 | } 763 | 764 | // Check if we've reached our last step 765 | if r.PlanTracker.CurrentStep >= len(r.PlanTracker.Steps)-1 { 766 | // Only exit if the last step is completed or failed 767 | lastStep := r.PlanTracker.Steps[len(r.PlanTracker.Steps)-1] 768 | if lastStep.Status == "completed" || lastStep.Status == "failed" { 769 | if r.PlanTracker.FinalAnswer != "" { 770 | if r.Verbose { 771 | color.Green("Final answer: %s\n", r.PlanTracker.FinalAnswer) 772 | } 773 | } else { 774 | if r.Verbose { 775 | color.Yellow("No final answer provided, but plan execution is complete.\n") 776 | } 777 | } 778 | break 779 | } else { 780 | // Last step isn't completed yet, continue execution 781 | if r.Verbose { 782 | color.Yellow("At last step but status is %s, continuing execution", lastStep.Status) 783 | } 784 | } 785 | } 786 | 787 | // Increment iteration counter 788 | iteration++ 789 | } 790 | 791 | // Generate the final summary 792 | return generateFinalSummary(r.PlanTracker), nil 793 | } 794 | 795 | // ExecuteStep executes a single step in the plan 796 | func (r *ReActFlow) ExecuteStep(ctx context.Context, iteration int, currentStep *StepDetail) error { 797 | // Validate we have steps to execute 798 | if len(r.PlanTracker.Steps) == 0 { 799 | return fmt.Errorf("no steps in execution plan") 800 | } 801 | 802 | // Validate the current step index is within bounds 803 | if r.PlanTracker.CurrentStep < 0 || r.PlanTracker.CurrentStep >= len(r.PlanTracker.Steps) { 804 | return fmt.Errorf("current step index %d is out of bounds (0-%d)", 805 | r.PlanTracker.CurrentStep, len(r.PlanTracker.Steps)-1) 806 | } 807 | 808 | // Get the current step from our tracker 809 | trackerCurrentStep := r.PlanTracker.GetCurrentStep() 810 | if trackerCurrentStep == nil { 811 | return fmt.Errorf("invalid current step - nil returned from GetCurrentStep") 812 | } 813 | 814 | // If the passed step doesn't match our tracker's current step, log warning and use our tracker's step 815 | if currentStep != trackerCurrentStep { 816 | if r.Verbose { 817 | color.Yellow("Step mismatch: passed step doesn't match tracker's current step. Using tracker's step.") 818 | } 819 | currentStep = trackerCurrentStep 820 | } 821 | 822 | // Ensure the current step is marked as in_progress 823 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "in_progress", "", "") 824 | 825 | if r.Verbose { 826 | color.Blue("[Step %d: %s] Executing step [current status: %s]\n", 827 | r.PlanTracker.CurrentStep+1, 828 | currentStep.Name, 829 | currentStep.Description, 830 | r.PlanTracker.Steps[r.PlanTracker.CurrentStep].Status) 831 | color.Cyan("Current plan status:\n%s\n", r.PlanTracker.GetPlanStatus()) 832 | } 833 | 834 | // Think about the step 835 | stepResult, err := r.ThinkAboutStep(ctx, currentStep) 836 | if err != nil { 837 | if r.Verbose { 838 | color.Red("Error thinking about step %d: %v\n", r.PlanTracker.CurrentStep+1, err) 839 | } 840 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "failed", "", fmt.Sprintf("Error: %v", err)) 841 | 842 | // Try to recover by moving to the next step 843 | if !r.PlanTracker.MoveToNextStep() { 844 | r.PlanTracker.LastError = fmt.Sprintf("Step thinking failed: %v", err) 845 | return err 846 | } 847 | return nil 848 | } 849 | 850 | // Parse the step result 851 | var stepAction ReactAction 852 | if err = extractReactAction(stepResult, &stepAction); err != nil { 853 | if r.Verbose { 854 | color.Red("Unable to parse step response as JSON: %v\n", err) 855 | } 856 | // Try to extract a final answer from the raw response 857 | potentialAnswer := extractAnswerFromText(stepResult) 858 | if potentialAnswer != "" { 859 | r.PlanTracker.FinalAnswer = potentialAnswer 860 | } 861 | 862 | // Mark step as failed 863 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "failed", "", fmt.Sprintf("Error parsing response: %v", err)) 864 | // Try to move to next step 865 | if !r.PlanTracker.MoveToNextStep() { 866 | if r.PlanTracker.FinalAnswer != "" { 867 | return nil 868 | } 869 | return fmt.Errorf("couldn't parse the response for step %d", r.PlanTracker.CurrentStep+1) 870 | } 871 | return nil 872 | } 873 | 874 | // Store original step index for comparison 875 | originalStepIndex := r.PlanTracker.CurrentStep 876 | 877 | // Sync steps from the model's response with our tracker 878 | r.PlanTracker.SyncStepsWithReactAction(&stepAction) 879 | 880 | // Verify we're still working on a valid step after sync 881 | if r.PlanTracker.CurrentStep < 0 || r.PlanTracker.CurrentStep >= len(r.PlanTracker.Steps) { 882 | if r.Verbose { 883 | color.Red("After step sync, current step index %d is invalid. Resetting to original step %d.", 884 | r.PlanTracker.CurrentStep, originalStepIndex) 885 | } 886 | // Reset to original step if current became invalid 887 | r.PlanTracker.CurrentStep = originalStepIndex 888 | } 889 | 890 | // Check if the model and our tracker are on different steps now 891 | if stepAction.CurrentStepIndex != r.PlanTracker.CurrentStep { 892 | if r.Verbose { 893 | color.Yellow("Step index drift after sync: model=%d, tracker=%d", 894 | stepAction.CurrentStepIndex, r.PlanTracker.CurrentStep) 895 | } 896 | } 897 | 898 | // Check if we have a final answer 899 | if stepAction.FinalAnswer != "" { 900 | r.PlanTracker.FinalAnswer = stepAction.FinalAnswer 901 | if r.Verbose { 902 | color.Cyan("Final answer received: %s\n", r.PlanTracker.FinalAnswer) 903 | } 904 | 905 | // Mark current step as completed 906 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "completed", "", "Final answer provided") 907 | 908 | // If this is the last step, we're done 909 | if r.PlanTracker.CurrentStep >= len(r.PlanTracker.Steps)-1 { 910 | return nil 911 | } 912 | 913 | // Move to next step even if we have a final answer but more steps remain 914 | r.PlanTracker.MoveToNextStep() 915 | return nil 916 | } 917 | 918 | // Execute tool if needed 919 | return r.ExecuteToolIfNeeded(ctx, &stepAction) 920 | } 921 | 922 | // ThinkAboutStep uses the LLM to think about how to execute the current step 923 | func (r *ReActFlow) ThinkAboutStep(ctx context.Context, currentStep *StepDetail) (string, error) { 924 | // Validate our current step pointer and index 925 | if currentStep == nil { 926 | return "", fmt.Errorf("current step is nil") 927 | } 928 | 929 | // Validate the current step index is within bounds 930 | if r.PlanTracker.CurrentStep < 0 || r.PlanTracker.CurrentStep >= len(r.PlanTracker.Steps) { 931 | return "", fmt.Errorf("current step index %d is out of bounds (0-%d)", 932 | r.PlanTracker.CurrentStep, len(r.PlanTracker.Steps)-1) 933 | } 934 | 935 | // Get the current step from our tracker (not the parameter passed in) 936 | trackerCurrentStep := r.PlanTracker.GetCurrentStep() 937 | 938 | // Double check that our pointer and tracker step are in sync 939 | if currentStep != trackerCurrentStep { 940 | if r.Verbose { 941 | color.Yellow("Current step mismatch in ThinkAboutStep. Using tracker's step.") 942 | } 943 | currentStep = trackerCurrentStep 944 | } 945 | 946 | // Mark current step as in_progress if not already 947 | if r.PlanTracker.Steps[r.PlanTracker.CurrentStep].Status == "pending" { 948 | r.PlanTracker.Steps[r.PlanTracker.CurrentStep].Status = "in_progress" 949 | } 950 | 951 | // Prepare a deep copy of the steps to send to the LLM 952 | stepsCopy := make([]StepDetail, len(r.PlanTracker.Steps)) 953 | copy(stepsCopy, r.PlanTracker.Steps) 954 | 955 | // Prepare the current ReactAction with updated steps status 956 | currentReactAction := ReactAction{ 957 | Question: r.Instructions, 958 | Thought: "Executing the next step in the plan", 959 | Steps: stepsCopy, 960 | CurrentStepIndex: r.PlanTracker.CurrentStep, 961 | } 962 | 963 | // Create a new flow for this step 964 | currentReactActionJSON, _ := json.MarshalIndent(currentReactAction, "", " ") 965 | stepFlow := &swarm.SimpleFlow{ 966 | Name: "think", 967 | Model: r.Model, 968 | MaxTurns: 30, 969 | JSONMode: true, 970 | Steps: []swarm.SimpleFlowStep{ 971 | { 972 | Name: "think-step", 973 | Instructions: strings.Replace(reactPrompt, "{{TOOLS}}", r.ToolPrompt, 1), 974 | Inputs: map[string]interface{}{ 975 | "instructions": fmt.Sprintf("User input: %s\n\nCurrent plan and status:\n%s\n\nExecute the current step (index %d) of the plan.", 976 | r.Instructions, string(currentReactActionJSON), r.PlanTracker.CurrentStep), 977 | "chatHistory": r.ChatHistory, 978 | }, 979 | }, 980 | }, 981 | } 982 | 983 | // Initialize the workflow for this step 984 | stepFlow.Initialize() 985 | 986 | // Create a context with timeout for this step 987 | stepCtx, stepCancel := context.WithTimeout(ctx, 5*time.Minute) 988 | if r.Verbose { 989 | color.Blue("[Step %d: %s] Running the step %s [current status: %s]\n", 990 | r.PlanTracker.CurrentStep+1, 991 | currentStep.Name, 992 | currentStep.Description, 993 | r.PlanTracker.Steps[r.PlanTracker.CurrentStep].Status) 994 | } 995 | 996 | stepResult, stepChatHistory, err := stepFlow.Run(stepCtx, r.Client) 997 | stepCancel() 998 | 999 | // Update chat history 1000 | r.ChatHistory = limitChatHistory(stepChatHistory, 20) 1001 | if r.Verbose && err == nil { 1002 | color.Cyan("[Step %d: %s] Step result:\n%s\n\n", r.PlanTracker.CurrentStep+1, currentStep.Name, stepResult) 1003 | } 1004 | 1005 | return stepResult, err 1006 | } 1007 | 1008 | // ExecuteToolIfNeeded executes a tool if the current step requires it 1009 | func (r *ReActFlow) ExecuteToolIfNeeded(ctx context.Context, stepAction *ReactAction) error { 1010 | // Ensure our internal step tracker and the stepAction's index are fully synchronized 1011 | if stepAction.CurrentStepIndex != r.PlanTracker.CurrentStep { 1012 | if r.Verbose { 1013 | color.Yellow("Step index mismatch: PlanTracker.CurrentStep=%d, stepAction.CurrentStepIndex=%d, syncing to PlanTracker's value", 1014 | r.PlanTracker.CurrentStep, stepAction.CurrentStepIndex) 1015 | } 1016 | } 1017 | 1018 | // Always use our plan tracker's current step as the source of truth 1019 | currentStepIndex := r.PlanTracker.CurrentStep 1020 | 1021 | // Validate bounds for both tracking mechanisms 1022 | if currentStepIndex < 0 || currentStepIndex >= len(r.PlanTracker.Steps) { 1023 | return fmt.Errorf("invalid current step index: %d (out of bounds)", currentStepIndex) 1024 | } 1025 | 1026 | // Check if we have a valid action to execute 1027 | actionExists := false 1028 | var actionName, actionInput string 1029 | 1030 | // First check our plan tracker for action info 1031 | if currentStepIndex < len(r.PlanTracker.Steps) && 1032 | r.PlanTracker.Steps[currentStepIndex].Action.Name != "" { 1033 | actionExists = true 1034 | actionName = r.PlanTracker.Steps[currentStepIndex].Action.Name 1035 | if input, ok := r.PlanTracker.Steps[currentStepIndex].Action.Input.(string); ok { 1036 | actionInput = input 1037 | } else { 1038 | inputBytes, _ := json.Marshal(r.PlanTracker.Steps[currentStepIndex].Action.Input) 1039 | actionInput = string(inputBytes) 1040 | } 1041 | } 1042 | 1043 | // If no action in our tracker, check if stepAction provides one 1044 | if !actionExists && currentStepIndex < len(stepAction.Steps) && 1045 | stepAction.Steps[currentStepIndex].Action.Name != "" { 1046 | actionExists = true 1047 | actionName = stepAction.Steps[currentStepIndex].Action.Name 1048 | if input, ok := stepAction.Steps[currentStepIndex].Action.Input.(string); ok { 1049 | actionInput = input 1050 | } else { 1051 | inputBytes, _ := json.Marshal(stepAction.Steps[currentStepIndex].Action.Input) 1052 | actionInput = string(inputBytes) 1053 | } 1054 | 1055 | // Sync this action back to our plan tracker 1056 | r.PlanTracker.Steps[currentStepIndex].Action.Name = actionName 1057 | r.PlanTracker.Steps[currentStepIndex].Action.Input = actionInput 1058 | } 1059 | 1060 | // If no tool execution needed, mark step as completed and move on 1061 | if !actionExists { 1062 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "completed", "", "Step completed without tool execution") 1063 | r.PlanTracker.MoveToNextStep() 1064 | return nil 1065 | } 1066 | 1067 | // Execute the tool and get tool response 1068 | toolResponse := r.ExecuteTool(actionName, actionInput) 1069 | 1070 | // Get the current step - initialize with a stub first 1071 | tempStep := StepDetail{ 1072 | Name: fmt.Sprintf("Step %d", currentStepIndex+1), 1073 | Description: fmt.Sprintf("Executing %s tool", actionName), 1074 | Status: "in_progress", 1075 | } 1076 | tempStep.Action.Name = actionName 1077 | tempStep.Action.Input = actionInput 1078 | 1079 | // Only try to use stepAction's step if the index is valid 1080 | currentStep := &tempStep 1081 | if currentStepIndex < len(stepAction.Steps) { 1082 | currentStep = &stepAction.Steps[currentStepIndex] 1083 | } 1084 | 1085 | // Process the tool observation 1086 | return r.ProcessToolObservation(ctx, currentStep, toolResponse) 1087 | } 1088 | 1089 | // ExecuteTool executes the specified tool and returns the observation 1090 | func (r *ReActFlow) ExecuteTool(toolName string, toolInput string) string { 1091 | if r.Verbose { 1092 | color.Blue("Executing tool %s\n", toolName) 1093 | color.Cyan("Invoking %s tool with inputs: \n============\n%s\n============\n\n", toolName, toolInput) 1094 | } 1095 | 1096 | // Execute the tool with timeout 1097 | toolFunc, ok := tools.CopilotTools[toolName] 1098 | if !ok { 1099 | toolResponse := fmt.Sprintf("Tool %s is not available. Considering switch to other supported tools.", toolName) 1100 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "failed", toolName, toolResponse) 1101 | return toolResponse 1102 | } 1103 | 1104 | // Execute tool with timeout 1105 | toolResultCh := make(chan struct { 1106 | result string 1107 | err error 1108 | }) 1109 | 1110 | go func() { 1111 | result, err := toolFunc.ToolFunc(toolInput) 1112 | toolResultCh <- struct { 1113 | result string 1114 | err error 1115 | }{result, err} 1116 | }() 1117 | 1118 | // Wait for tool execution with timeout 1119 | var toolResponse string 1120 | select { 1121 | case toolResult := <-toolResultCh: 1122 | toolResponse = strings.TrimSpace(toolResult.result) 1123 | if toolResult.err != nil { 1124 | toolResponse = fmt.Sprintf("Tool %s failed with result: %s error: %v. Considering refine the inputs for the tool.", 1125 | toolName, toolResult.result, toolResult.err) 1126 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "failed", toolName, toolResponse) 1127 | } else { 1128 | // Update step with tool call info 1129 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "in_progress", toolName, toolResult.result) 1130 | } 1131 | case <-time.After(r.PlanTracker.ExecutionTimeout): 1132 | toolResponse = fmt.Sprintf("Tool %s execution timed out after %v seconds. Try with a simpler query or different tool.", 1133 | toolName, r.PlanTracker.ExecutionTimeout.Seconds()) 1134 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "failed", toolName, toolResponse) 1135 | } 1136 | 1137 | if toolResponse == "" { 1138 | toolResponse = "Empty result returned from the tool." 1139 | } 1140 | if r.Verbose { 1141 | color.Cyan("Tool %s result:\n %s\n\n", toolName, toolResponse) 1142 | } 1143 | return toolResponse 1144 | } 1145 | 1146 | // ProcessToolObservation processes the observation from a tool execution 1147 | func (r *ReActFlow) ProcessToolObservation(ctx context.Context, currentStep *StepDetail, toolResponse string) error { 1148 | // Truncate the prompt to the max tokens allowed by the model. 1149 | // This is required because the tool may have generated a long output. 1150 | toolResponse = llms.ConstrictPrompt(toolResponse, r.Model) 1151 | // Update the truncated toolResponse 1152 | currentStep.Observation = toolResponse 1153 | 1154 | // Update our plan tracker with the toolResponse 1155 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "in_progress", currentStep.Action.Name, toolResponse) 1156 | 1157 | // Create a new flow for processing the toolResponse observation 1158 | observationActionJSON, _ := json.MarshalIndent(currentStep, "", " ") 1159 | observationFlow := &swarm.SimpleFlow{ 1160 | Name: "tool-call", 1161 | Model: r.Model, 1162 | MaxTurns: 30, 1163 | JSONMode: true, 1164 | Steps: []swarm.SimpleFlowStep{ 1165 | { 1166 | Name: "tool-call-step", 1167 | Instructions: nextStepPrompt, 1168 | Inputs: map[string]interface{}{ 1169 | "instructions": fmt.Sprintf("User input: %s\n\nCurrent plan with tool execution result:\n%s\n", 1170 | r.Instructions, string(observationActionJSON)), 1171 | "chatHistory": r.ChatHistory, 1172 | }, 1173 | }, 1174 | }, 1175 | } 1176 | 1177 | // Initialize the workflow for processing the observation 1178 | observationFlow.Initialize() 1179 | if r.Verbose { 1180 | color.Blue("[Step %d: %s] Processing tool observation\n", r.PlanTracker.CurrentStep+1, currentStep.Name) 1181 | } 1182 | 1183 | // Run the observation processing 1184 | obsCtx, obsCancel := context.WithTimeout(ctx, 5*time.Minute) 1185 | observationResult, observationChatHistory, err := observationFlow.Run(obsCtx, r.Client) 1186 | obsCancel() 1187 | 1188 | if err != nil { 1189 | if r.Verbose { 1190 | color.Red("Error processing tool observation: %v\n", err) 1191 | } 1192 | // Mark step with the appropriate status based on tool execution 1193 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "failed", currentStep.Action.Name, toolResponse) 1194 | 1195 | // Try to move to the next step regardless of the error 1196 | r.PlanTracker.MoveToNextStep() 1197 | return nil 1198 | } 1199 | 1200 | // Update bounded chat history 1201 | r.ChatHistory = limitChatHistory(observationChatHistory, 20) 1202 | if r.Verbose { 1203 | color.Cyan("[Step %d: %s] Observation processing response:\n%s\n\n", r.PlanTracker.CurrentStep+1, currentStep.Name, observationResult) 1204 | } 1205 | 1206 | // Parse the observation result 1207 | var observationAction ReactAction 1208 | if err = extractReactAction(observationResult, &observationAction); err != nil { 1209 | if r.Verbose { 1210 | color.Red("Unable to parse observation response as JSON: %v\n", err) 1211 | } 1212 | // Try to extract a final answer from the raw response 1213 | potentialAnswer := extractAnswerFromText(observationResult) 1214 | if potentialAnswer != "" { 1215 | r.PlanTracker.FinalAnswer = potentialAnswer 1216 | } 1217 | 1218 | // Mark step with the determined status and move on 1219 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "completed", currentStep.Action.Name, toolResponse) 1220 | r.PlanTracker.MoveToNextStep() 1221 | return nil 1222 | } 1223 | 1224 | // Update the step's observation with the thought from observationAction 1225 | observationThought := observationAction.Thought 1226 | if observationThought != "" { 1227 | r.PlanTracker.Steps[r.PlanTracker.CurrentStep].Observation = observationThought 1228 | if r.PlanTracker.CurrentStep < len(observationAction.Steps) { 1229 | observationAction.Steps[r.PlanTracker.CurrentStep].Observation = observationThought 1230 | } 1231 | } 1232 | 1233 | // Sync steps from observation action with our tracker 1234 | r.PlanTracker.SyncStepsWithReactAction(&observationAction) 1235 | 1236 | // Check if we have a final answer from observation processing 1237 | if observationAction.FinalAnswer != "" { 1238 | r.PlanTracker.FinalAnswer = observationAction.FinalAnswer 1239 | if r.Verbose { 1240 | color.Cyan("Final answer received from observation processing: %s\n", r.PlanTracker.FinalAnswer) 1241 | } 1242 | 1243 | // Mark current step as completed 1244 | r.PlanTracker.UpdateStepStatus(r.PlanTracker.CurrentStep, "completed", currentStep.Action.Name, observationThought) 1245 | 1246 | // Even if we have a final answer, we should still move to the next step 1247 | // if we're not at the last step 1248 | r.PlanTracker.MoveToNextStep() 1249 | return nil 1250 | } 1251 | 1252 | // Store the current step before any changes for comparison 1253 | originalStepIndex := r.PlanTracker.CurrentStep 1254 | 1255 | // Get the observation action's current step index 1256 | observationStepIndex := observationAction.CurrentStepIndex 1257 | 1258 | // First verify the observation step index is valid 1259 | if observationStepIndex < 0 || observationStepIndex >= len(observationAction.Steps) { 1260 | if r.Verbose { 1261 | color.Yellow("Observation action has invalid step index %d. Using our current step %d instead.", 1262 | observationStepIndex, r.PlanTracker.CurrentStep) 1263 | } 1264 | observationStepIndex = r.PlanTracker.CurrentStep 1265 | } 1266 | 1267 | // Check if the observation indicates we should run a different action for the current step 1268 | if observationStepIndex == originalStepIndex && 1269 | observationAction.Steps[observationStepIndex].Action.Name != "" { 1270 | // Update the current step with new action info but keep the same index 1271 | if r.Verbose { 1272 | color.Yellow("Updating current step %d with new action: %s", 1273 | originalStepIndex+1, observationAction.Steps[observationStepIndex].Action.Name) 1274 | } 1275 | 1276 | r.PlanTracker.Steps[originalStepIndex].Observation = observationThought 1277 | r.PlanTracker.Steps[originalStepIndex].Action = observationAction.Steps[observationStepIndex].Action 1278 | r.PlanTracker.Steps[originalStepIndex].Status = "in_progress" 1279 | return nil // Continue with the same step but with a new action 1280 | } 1281 | 1282 | // If model suggests a later step with an action, move there 1283 | if observationStepIndex > originalStepIndex { 1284 | // Check if that step has an action defined 1285 | if observationStepIndex < len(observationAction.Steps) && 1286 | observationAction.Steps[observationStepIndex].Action.Name != "" { 1287 | 1288 | // Mark current step as completed 1289 | r.PlanTracker.UpdateStepStatus(originalStepIndex, "completed", currentStep.Action.Name, observationThought) 1290 | 1291 | // Make sure we're not stepping beyond our plan's steps 1292 | if observationStepIndex < len(r.PlanTracker.Steps) { 1293 | // Update the target step's action info before moving to it 1294 | r.PlanTracker.Steps[observationStepIndex].Action = observationAction.Steps[observationStepIndex].Action 1295 | r.PlanTracker.Steps[observationStepIndex].Status = "pending" 1296 | 1297 | // Jump directly to that step 1298 | r.PlanTracker.CurrentStep = observationStepIndex 1299 | 1300 | if r.Verbose { 1301 | color.Yellow("Jumping forward to step %d to execute action: %s", 1302 | observationStepIndex+1, r.PlanTracker.Steps[observationStepIndex].Action.Name) 1303 | } 1304 | return nil 1305 | } 1306 | } 1307 | } 1308 | 1309 | // Check all steps for any actions to execute 1310 | for i := 0; i < len(observationAction.Steps); i++ { 1311 | // Skip the current step we just processed 1312 | if i == originalStepIndex { 1313 | continue 1314 | } 1315 | 1316 | // If we find a step with an action to execute, move to it 1317 | if observationAction.Steps[i].Action.Name != "" && 1318 | i < len(r.PlanTracker.Steps) && 1319 | (r.PlanTracker.Steps[i].Status == "pending" || 1320 | r.PlanTracker.Steps[i].Status == "in_progress") { 1321 | 1322 | // Mark current step as completed 1323 | r.PlanTracker.UpdateStepStatus(originalStepIndex, "completed", currentStep.Action.Name, observationThought) 1324 | 1325 | // Update the target step's action and move to it 1326 | r.PlanTracker.Steps[i].Action = observationAction.Steps[i].Action 1327 | r.PlanTracker.CurrentStep = i 1328 | 1329 | if r.Verbose { 1330 | color.Yellow("Moving to step %d to execute action: %s", 1331 | i+1, r.PlanTracker.Steps[i].Action.Name) 1332 | } 1333 | return nil 1334 | } 1335 | } 1336 | 1337 | // Default case: mark current step as completed and move to next 1338 | r.PlanTracker.UpdateStepStatus(originalStepIndex, "completed", currentStep.Action.Name, observationThought) 1339 | r.PlanTracker.MoveToNextStep() 1340 | return nil 1341 | } 1342 | 1343 | // extractAnswerFromText attempts to extract a final answer from unstructured text 1344 | func extractAnswerFromText(text string) string { 1345 | // Look for common answer patterns 1346 | answerPatterns := []string{ 1347 | `(?i)(?:^|\n)(?:answer|conclusion|result|summary):?\s*(.+?)(?:\n\n|\z)`, 1348 | `(?i)(?:^|\n)(?:finally|in conclusion|to summarize|in summary):?\s*(.+?)(?:\n\n|\z)`, 1349 | `(?i)(?:^|\n)(?:the solution is|the result is|we found that):?\s*(.+?)(?:\n\n|\z)`, 1350 | } 1351 | 1352 | for _, pattern := range answerPatterns { 1353 | re := regexp.MustCompile(pattern) 1354 | matches := re.FindStringSubmatch(text) 1355 | if len(matches) > 1 { 1356 | return matches[1] 1357 | } 1358 | } 1359 | 1360 | // If no answer pattern found, take the last paragraph as a fallback 1361 | paragraphs := strings.Split(text, "\n\n") 1362 | if len(paragraphs) > 0 { 1363 | return paragraphs[len(paragraphs)-1] 1364 | } 1365 | 1366 | return text 1367 | } 1368 | 1369 | // generateFinalSummary creates a summary from all completed steps 1370 | func generateFinalSummary(pt *PlanTracker) string { 1371 | if pt.FinalAnswer != "" { 1372 | return pt.FinalAnswer 1373 | } 1374 | 1375 | var sb strings.Builder 1376 | sb.WriteString("I've completed all the steps in the plan. Here's a summary of what I did:\n\n") 1377 | 1378 | for i, step := range pt.Steps { 1379 | sb.WriteString(fmt.Sprintf("Step %d: %s [status: %s]\n", i+1, step.Description, step.Status)) 1380 | observation := step.Observation 1381 | if len(observation) > 200 { 1382 | observation = observation[:200] + " <truncated>" 1383 | } 1384 | formattedObs := formatObservationOutputs(observation) 1385 | sb.WriteString(fmt.Sprintf("Observation:\n%s\n\n", formattedObs)) 1386 | } 1387 | 1388 | return sb.String() 1389 | } 1390 | 1391 | // limitChatHistory ensures chat history doesn't grow too large 1392 | func limitChatHistory(history interface{}, maxMessages int) interface{} { 1393 | if history == nil { 1394 | return nil 1395 | } 1396 | 1397 | // Handle map type history 1398 | if mapHistory, ok := history.(map[string]interface{}); ok { 1399 | // Create a deep copy to avoid modifying the original 1400 | result := make(map[string]interface{}) 1401 | for k, v := range mapHistory { 1402 | result[k] = v 1403 | } 1404 | 1405 | // If there's a messages array, limit its size 1406 | if messages, ok := result["messages"].([]interface{}); ok && len(messages) > maxMessages { 1407 | result["messages"] = messages[len(messages)-maxMessages:] 1408 | } 1409 | 1410 | return result 1411 | } 1412 | 1413 | // Handle slice type history 1414 | if sliceHistory, ok := history.([]map[string]interface{}); ok { 1415 | if len(sliceHistory) <= maxMessages { 1416 | return sliceHistory 1417 | } 1418 | 1419 | // Take only the last maxMessages items 1420 | return sliceHistory[len(sliceHistory)-maxMessages:] 1421 | } 1422 | 1423 | // Return unchanged if unknown type 1424 | return history 1425 | } 1426 | -------------------------------------------------------------------------------- /pkg/workflows/simpleflow.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package workflows 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/feiskyer/swarm-go" 24 | ) 25 | 26 | const assistantPrompt = `As a Kubernetes expert, guide the user according to the given instructions to solve their problem or achieve their objective. 27 | 28 | Understand the nature of their request, clarify any complex concepts, and provide step-by-step guidance tailored to their specific needs. Ensure that your explanations are comprehensive, using precise Kubernetes terminology and concepts. 29 | 30 | # Steps 31 | 32 | 1. **Interpret User Intent**: Carefully analyze the user's instructions or questions to understand their goal. 33 | 2. **Concepts Explanation**: If necessary, break down complex Kubernetes concepts into simpler terms. 34 | 3. **Step-by-Step Solution**: Provide a detailed, clear step-by-step process to achieve the desired outcome. 35 | 4. **Troubleshooting**: Suggest potential solutions for common issues and pitfalls when working with Kubernetes. 36 | 5. **Best Practices**: Mention any relevant Kubernetes best practices that should be followed. 37 | 38 | # Output Format 39 | 40 | Provide a concise Markdown response in a clear, logical order. Each step should be concise, using bullet points or numbered lists if necessary. Include code snippets in markdown code blocks where relevant. 41 | 42 | # Notes 43 | 44 | - Assume the user has basic knowledge of Kubernetes. 45 | - Use precise terminology and include explanations only as needed based on the complexity of the task. 46 | - Ensure instructions are applicable across major cloud providers (GKE, EKS, AKS) unless specified otherwise.` 47 | 48 | // SimpleFlow runs a simple workflow by following the given instructions. 49 | func SimpleFlow(model string, systemPrompt string, instructions string, verbose bool) (string, error) { 50 | simpleFlow := &swarm.SimpleFlow{ 51 | Name: "simple-workflow", 52 | Model: model, 53 | MaxTurns: 30, 54 | // Verbose: verbose, 55 | Steps: []swarm.SimpleFlowStep{ 56 | { 57 | Name: "simple", 58 | Instructions: systemPrompt, 59 | Inputs: map[string]interface{}{ 60 | "instructions": instructions, 61 | }, 62 | }, 63 | }, 64 | } 65 | 66 | // Create OpenAI client 67 | client, err := NewSwarm() 68 | if err != nil { 69 | fmt.Printf("Failed to create client: %v\n", err) 70 | os.Exit(1) 71 | } 72 | 73 | // Initialize and run workflow 74 | simpleFlow.Initialize() 75 | result, _, err := simpleFlow.Run(context.Background(), client) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | return result, nil 81 | } 82 | 83 | // AssistantFlow runs a simple workflow with kubernetes assistant prompt. 84 | func AssistantFlow(model string, instructions string, verbose bool) (string, error) { 85 | return SimpleFlow(model, assistantPrompt, instructions, verbose) 86 | } 87 | -------------------------------------------------------------------------------- /pkg/workflows/swarm.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 - Present, Pengfei Ni 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | package workflows 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | "reflect" 22 | 23 | "github.com/feiskyer/kube-copilot/pkg/tools" 24 | "github.com/feiskyer/swarm-go" 25 | ) 26 | 27 | var ( 28 | // auditFunc is a Swarm function that conducts a structured security audit of a Kubernetes Pod. 29 | trivyFunc = swarm.NewAgentFunction( 30 | "trivy", 31 | "Run trivy image scanning for a given image", 32 | func(args map[string]interface{}) (interface{}, error) { 33 | image, ok := args["image"].(string) 34 | if !ok { 35 | return nil, fmt.Errorf("image not provided") 36 | } 37 | 38 | result, err := tools.Trivy(image) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | return result, nil 44 | }, 45 | []swarm.Parameter{ 46 | {Name: "image", Type: reflect.TypeOf(""), Required: true}, 47 | }, 48 | ) 49 | 50 | // kubectlFunc is a Swarm function that runs kubectl command. 51 | kubectlFunc = swarm.NewAgentFunction( 52 | "kubectl", 53 | "Run kubectl command. Ensure command is a single kubectl and shell pipe (|) and redirect (>) are not supported.", 54 | func(args map[string]interface{}) (interface{}, error) { 55 | command, ok := args["command"].(string) 56 | if !ok { 57 | return nil, fmt.Errorf("command not provided") 58 | } 59 | 60 | result, err := tools.Kubectl(command) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | return result, nil 66 | }, 67 | []swarm.Parameter{ 68 | {Name: "command", Type: reflect.TypeOf(""), Required: true}, 69 | }, 70 | ) 71 | 72 | pythonFunc = swarm.NewAgentFunction( 73 | "python", 74 | "Run python code", 75 | func(args map[string]interface{}) (interface{}, error) { 76 | code, ok := args["code"].(string) 77 | if !ok { 78 | return nil, fmt.Errorf("code not provided") 79 | } 80 | 81 | result, err := tools.PythonREPL(code) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | return result, nil 87 | }, 88 | []swarm.Parameter{ 89 | {Name: "code", Type: reflect.TypeOf(""), Required: true}, 90 | }, 91 | ) 92 | ) 93 | 94 | // NewSwarm creates a new Swarm client. 95 | func NewSwarm() (*swarm.Swarm, error) { 96 | apiKey := os.Getenv("OPENAI_API_KEY") 97 | if apiKey != "" { 98 | baseURL := os.Getenv("OPENAI_API_BASE") 99 | if baseURL == "" { 100 | return swarm.NewSwarm(swarm.NewOpenAIClient(apiKey)), nil 101 | } 102 | 103 | // OpenAI compatible LLM 104 | return swarm.NewSwarm(swarm.NewOpenAIClientWithBaseURL(apiKey, baseURL)), nil 105 | } 106 | 107 | azureAPIKey := os.Getenv("AZURE_OPENAI_API_KEY") 108 | azureAPIBase := os.Getenv("AZURE_OPENAI_API_BASE") 109 | azureAPIVersion := os.Getenv("AZURE_OPENAI_API_VERSION") 110 | if azureAPIVersion == "" { 111 | azureAPIVersion = "2025-03-01-preview" 112 | } 113 | if azureAPIKey != "" && azureAPIBase != "" { 114 | return swarm.NewSwarm(swarm.NewAzureOpenAIClient(azureAPIKey, azureAPIBase, azureAPIVersion)), nil 115 | } 116 | 117 | return nil, fmt.Errorf("OPENAI_API_KEY or AZURE_OPENAI_API_KEY is not set") 118 | } 119 | --------------------------------------------------------------------------------