├── .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 ".
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.
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": "",
26 | "thought": "",
27 | "steps": [
28 | {
29 | "name": "",
30 | "description": "",
31 | "action": {
32 | "name": "",
33 | "input": ""
34 | },
35 | "status": "",
36 | "observation": "",
37 | },
38 | {
39 | "name": "",
40 | "description": "",
41 | "action": {
42 | "name": "",
43 | "input": ""
44 | },
45 | "observation": "",
46 | "status": ""
47 | },
48 | ...more steps...
49 | ],
50 | "current_step_index": ,
51 | "final_answer": ""
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
556 | if strings.HasPrefix(text, "") {
557 | text = strings.Split(text, "")[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] + " "
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 |
--------------------------------------------------------------------------------